Skip to content

[DB-1971] Pass claims principal and require leader flag when writing using ISystemClient#5568

Open
timothycoleman wants to merge 2 commits intomasterfrom
timothycoleman/system-client-user-improvement
Open

[DB-1971] Pass claims principal and require leader flag when writing using ISystemClient#5568
timothycoleman wants to merge 2 commits intomasterfrom
timothycoleman/system-client-user-improvement

Conversation

@timothycoleman
Copy link
Contributor

@timothycoleman timothycoleman commented Mar 26, 2026

Allows these to be more flexible, V2 projections will use them.

Note that if requireLeader is false we might forward the write to the leader, but the credentials will only be preserved if the principal is SystemAccounts.System or contains a DelegatedClaimsIdentity (such as when received via an API). see ClientWriteTcpDispatcher.CreateWriteRequestPackage.

@timothycoleman timothycoleman requested a review from a team as a code owner March 26, 2026 09:54
Copilot AI review requested due to automatic review settings March 26, 2026 09:54
@linear
Copy link

linear bot commented Mar 26, 2026

@qodo-code-review
Copy link
Contributor

Review Summary by Qodo

Pass claims principal parameter to ISystemClient write operations

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add ClaimsPrincipal parameter to IWriteOperations write methods
• Pass SystemAccounts.System principal in all write operation calls
• Update method signatures across publisher extensions and system client
• Remove hardcoded system account dependency from publisher write extensions
Diagram
flowchart LR
  A["IWriteOperations Interface"] -->|"Add principal parameter"| B["WriteEvents Methods"]
  B -->|"Updated signatures"| C["PublisherWriteExtensions"]
  C -->|"Pass principal"| D["IPublisher.WriteEvents"]
  D -->|"Use principal instead of hardcoded"| E["WriteEventsCommand"]
  F["Call Sites"] -->|"Pass SystemAccounts.System"| B
Loading

Grey Divider

File Changes

1. src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs ✨ Enhancement +5/-2

Add principal parameter to write extensions

• Add System.Security.Claims using statement
• Remove KurrentDB.Core.Services.UserManagement using statement
• Add ClaimsPrincipal principal parameter to both WriteEvents extension methods
• Replace hardcoded SystemAccounts.System with principal parameter in command creation

src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs


2. src/KurrentDB.Core/Bus/Extensions/SystemClient.cs ✨ Enhancement +16/-4

Add principal parameter to system client write interface

• Add System.Security.Claims using statement
• Add ClaimsPrincipal principal parameter to all three WriteEvents method signatures in
 IWriteOperations interface
• Update WriteOperations record implementation to accept and pass principal parameter
• Forward principal to underlying publisher write calls

src/KurrentDB.Core/Bus/Extensions/SystemClient.cs


3. src/KurrentDB.Core/Bus/Extensions/PublisherManagementExtensions.cs ✨ Enhancement +1/-1

Pass system principal to metadata write

• Pass SystemAccounts.System as principal argument to WriteEvents call
• Update method call to match new signature with principal parameter

src/KurrentDB.Core/Bus/Extensions/PublisherManagementExtensions.cs


View more (5)
4. src/KurrentDB.Core.XUnit.Tests/TransactionLog/MultiStreamWrites/MultiStreamWritesTests.cs 🧪 Tests +1/-1

Pass system principal in test write call

• Pass SystemAccounts.System as principal argument to WriteEvents call in test
• Update test to match new method signature

src/KurrentDB.Core.XUnit.Tests/TransactionLog/MultiStreamWrites/MultiStreamWritesTests.cs


5. src/KurrentDB.SecondaryIndexing.LoadTesting/Environments/InMemory/PublisherBasedMessageBatchAppender.cs ✨ Enhancement +2/-1

Pass system principal in batch appender

• Add KurrentDB.Core.Services.UserManagement using statement
• Pass SystemAccounts.System as principal argument to WriteEvents call

src/KurrentDB.SecondaryIndexing.LoadTesting/Environments/InMemory/PublisherBasedMessageBatchAppender.cs


6. src/KurrentDB.SecondaryIndexing.Tests/Fixtures/SecondaryIndexingFixture.cs 🧪 Tests +2/-1

Pass system principal in test fixture

• Add KurrentDB.Core.Services.UserManagement using statement
• Pass SystemAccounts.System as principal argument to WriteEvents call in AppendToStream
 method

src/KurrentDB.SecondaryIndexing.Tests/Fixtures/SecondaryIndexingFixture.cs


7. src/KurrentDB.SecondaryIndexing/Indexes/User/Management/UserIndexEventStore.cs ✨ Enhancement +2/-1

Pass system principal in user index store

• Add KurrentDB.Core.Services.UserManagement using statement
• Pass SystemAccounts.System as principal argument to WriteEvents call for transactional write

src/KurrentDB.SecondaryIndexing/Indexes/User/Management/UserIndexEventStore.cs


8. src/KurrentDB.Surge/Producers/SystemProducer.cs ✨ Enhancement +2/-1

Pass system principal in surge producer

• Add KurrentDB.Core.Services.UserManagement using statement
• Pass SystemAccounts.System as principal argument to WriteEvents call in WriteEvents helper
 method

src/KurrentDB.Surge/Producers/SystemProducer.cs


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Contributor

qodo-code-review bot commented Mar 26, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. TCP auth silently dropped 🐞 Bug ⛨ Security
Description
ClientWriteTcpDispatcher only forwards credentials when the write principal contains a
DelegatedClaimsIdentity; other identity types are ignored and the request is sent with
TcpFlags.None. With this PR allowing arbitrary ClaimsPrincipal to be passed into
ClientMessage.WriteEvents, authenticated writes can become anonymous/AccessDenied when routed
through the TCP write dispatcher.
Code

src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs[R67-71]

				expectedVersions: expectedRevisions,
				events: events,
				eventStreamIndexes: eventStreamIndexes,
-				user: SystemAccounts.System,
+				user: principal,
				cancellationToken: cancellationToken
Evidence
PublisherWriteExtensions now sets the write message user to the provided principal. When the write
is serialized for TCP, only DelegatedClaimsIdentity is inspected for jwt/uid/pwd; otherwise the
dispatcher falls back to unauthenticated TcpFlags.None, dropping auth for principals that don’t
include DelegatedClaimsIdentity. DelegatedClaimsIdentity is only injected by the delegated auth
wrapper, so other authenticated principals (e.g., typical ClaimsIdentity-based principals) won’t
carry credentials through this path.

src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs[61-72]
src/KurrentDB.Core/Services/Transport/Tcp/ClientWriteTcpDispatcher.cs[144-171]
src/KurrentDB.Core/Authentication/DelegatedAuthentication/DelegatedAuthenticationProvider.cs[45-53]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ClientWriteTcpDispatcher.CreateWriteRequestPackage` only extracts credentials from `DelegatedClaimsIdentity`. After this PR, `PublisherWriteExtensions.WriteEvents` can pass any `ClaimsPrincipal`, so authenticated callers whose principal does not include `DelegatedClaimsIdentity` will be forwarded over TCP without credentials (`TcpFlags.None`), leading to unexpected `AccessDenied`/anonymous behavior.

### Issue Context
The PR’s intent is to pass the caller principal on writes. That intent will not hold for TCP-forwarded writes unless the dispatcher can extract credentials from non-delegated identities or the code ensures a delegated identity is always present.

### Fix Focus Areas
- src/KurrentDB.Core/Services/Transport/Tcp/ClientWriteTcpDispatcher.cs[144-171]
- src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs[28-72]
- src/KurrentDB.Core/Authentication/DelegatedAuthentication/DelegatedAuthenticationProvider.cs[45-53]

### Implementation notes
Choose one:
1) Update `CreateWriteRequestPackage` to also look for `jwt` / `uid` / `pwd` claims on *any* identity (or on `msg.User.Claims`) rather than only `DelegatedClaimsIdentity`.
2) Alternatively, enforce/ensure that principals passed into these write APIs always include a `DelegatedClaimsIdentity` containing the required tokens (e.g., wrap/add identity when missing), and fail fast if not possible.
3) Or plumb tokens via `ClientMessage.WriteEvents(... tokens: ...)` and use those in TCP packaging.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Null principal can crash 🐞 Bug ⛯ Reliability
Description
PublisherWriteExtensions forwards the provided principal into ClientMessage.WriteEvents without a
runtime null check. If a caller passes null, ClientWriteTcpDispatcher will dereference
msg.User.Identities and throw a NullReferenceException during TCP packaging.
Code

src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs[R67-71]

				expectedVersions: expectedRevisions,
				events: events,
				eventStreamIndexes: eventStreamIndexes,
-				user: SystemAccounts.System,
+				user: principal,
				cancellationToken: cancellationToken
Evidence
WriteRequestMessage stores User = user without null validation, and the TCP write dispatcher
iterates msg.User.Identities without guarding against msg.User being null. Since the new API
surface takes a ClaimsPrincipal principal parameter but does not enforce non-null at runtime,
passing null will cause an immediate NRE on the TCP path.

src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs[61-72]
src/KurrentDB.Core/Messages/ClientMessage.cs[49-70]
src/KurrentDB.Core/Services/Transport/Tcp/ClientWriteTcpDispatcher.cs[144-171]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new `ClaimsPrincipal principal` parameter is forwarded into `ClientMessage.WriteEvents` without runtime validation. If a caller passes `null`, the TCP dispatcher will throw when accessing `msg.User.Identities`.

### Issue Context
Although the parameter is non-nullable, C# non-nullability isn’t enforced at runtime across assemblies and call sites.

### Fix Focus Areas
- src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs[28-80]
- src/KurrentDB.Core/Bus/Extensions/SystemClient.cs[163-215]

### Implementation notes
Add `ArgumentNullException.ThrowIfNull(principal);` at the start of the `IPublisher.WriteEvents(...)` extension(s) and/or at the `ISystemClient.Writing.WriteEvents(...)` entrypoints. If maintaining legacy behavior is desired, replace null with `SystemAccounts.System` explicitly (but still consider throwing to avoid silently trusted/anonymous writes).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines 67 to 71
expectedVersions: expectedRevisions,
events: events,
eventStreamIndexes: eventStreamIndexes,
user: SystemAccounts.System,
user: principal,
cancellationToken: cancellationToken
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Tcp auth silently dropped 🐞 Bug ⛨ Security

ClientWriteTcpDispatcher only forwards credentials when the write principal contains a
DelegatedClaimsIdentity; other identity types are ignored and the request is sent with
TcpFlags.None. With this PR allowing arbitrary ClaimsPrincipal to be passed into
ClientMessage.WriteEvents, authenticated writes can become anonymous/AccessDenied when routed
through the TCP write dispatcher.
Agent Prompt
### Issue description
`ClientWriteTcpDispatcher.CreateWriteRequestPackage` only extracts credentials from `DelegatedClaimsIdentity`. After this PR, `PublisherWriteExtensions.WriteEvents` can pass any `ClaimsPrincipal`, so authenticated callers whose principal does not include `DelegatedClaimsIdentity` will be forwarded over TCP without credentials (`TcpFlags.None`), leading to unexpected `AccessDenied`/anonymous behavior.

### Issue Context
The PR’s intent is to pass the caller principal on writes. That intent will not hold for TCP-forwarded writes unless the dispatcher can extract credentials from non-delegated identities or the code ensures a delegated identity is always present.

### Fix Focus Areas
- src/KurrentDB.Core/Services/Transport/Tcp/ClientWriteTcpDispatcher.cs[144-171]
- src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs[28-72]
- src/KurrentDB.Core/Authentication/DelegatedAuthentication/DelegatedAuthenticationProvider.cs[45-53]

### Implementation notes
Choose one:
1) Update `CreateWriteRequestPackage` to also look for `jwt` / `uid` / `pwd` claims on *any* identity (or on `msg.User.Claims`) rather than only `DelegatedClaimsIdentity`.
2) Alternatively, enforce/ensure that principals passed into these write APIs always include a `DelegatedClaimsIdentity` containing the required tokens (e.g., wrap/add identity when missing), and fail fast if not possible.
3) Or plumb tokens via `ClientMessage.WriteEvents(... tokens: ...)` and use those in TCP packaging.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the internal “system client” write path so event writes can carry an explicit ClaimsPrincipal, and updates current callers to pass SystemAccounts.System explicitly.

Changes:

  • Extend ISystemClient/IWriteOperations and publisher write extensions to require a ClaimsPrincipal for writes.
  • Thread the principal through SystemClientPublisherWriteExtensionsClientMessage.WriteEvents.
  • Update producers, secondary indexing, load testing, and tests to pass SystemAccounts.System where they write events.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/KurrentDB.Surge/Producers/SystemProducer.cs Passes SystemAccounts.System when writing via ISystemClient.
src/KurrentDB.SecondaryIndexing/Indexes/User/Management/UserIndexEventStore.cs Supplies system principal for transactional multi-stream writes.
src/KurrentDB.SecondaryIndexing.Tests/Fixtures/SecondaryIndexingFixture.cs Updates fixture helper writes to include system principal.
src/KurrentDB.SecondaryIndexing.LoadTesting/Environments/InMemory/PublisherBasedMessageBatchAppender.cs Updates load-test publisher writes to include system principal.
src/KurrentDB.Core/Bus/Extensions/SystemClient.cs Changes write operations API to require a ClaimsPrincipal and forwards it to publisher write extensions.
src/KurrentDB.Core/Bus/Extensions/PublisherWriteExtensions.cs Replaces implicit system user with a required ClaimsPrincipal argument and forwards it to ClientMessage.WriteEvents.
src/KurrentDB.Core.XUnit.Tests/TransactionLog/MultiStreamWrites/MultiStreamWritesTests.cs Updates test to provide system principal for a write.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 77 to 82
public interface IWriteOperations {
Task<WriteEventsResult> WriteEvents(string stream, Event[] events, long expectedRevision = -2, CancellationToken cancellationToken = default);
Task<WriteEventsResult> WriteEvents(string stream, Event[] events,
ClaimsPrincipal principal,
long expectedRevision = -2,
CancellationToken cancellationToken = default);
Task<WriteEventsMultiResult> WriteEvents(
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes the public IWriteOperations API by making ClaimsPrincipal a required parameter for all WriteEvents overloads (the prior overload without a principal is removed). If this interface is consumed outside this repo, it’s a breaking change; consider reintroducing the previous overload(s) (e.g., defaulting to SystemAccounts.System) or providing a backward-compatible extension method, while keeping the new principal-aware overloads for callers that need them.

Copilot uses AI. Check for mistakes.
Comment on lines 28 to 35
public static async Task<WriteEventsResult> WriteEvents(
this IPublisher publisher,
string stream,
Event[] events,
ClaimsPrincipal principal,
long expectedRevision = ExpectedVersion.Any,
CancellationToken cancellationToken = default
) {
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PublisherWriteExtensions.WriteEvents overload now requires a ClaimsPrincipal argument and removes the previous call pattern that relied on the implicit SystemAccounts.System user. Since this class is marked [PublicAPI], this is a breaking change for any downstream callers; consider adding an overload matching the previous signature (defaulting to SystemAccounts.System) so existing code can compile unchanged while still allowing principal-aware writes.

Copilot uses AI. Check for mistakes.
@timothycoleman timothycoleman changed the title [DB-1971] Pass claims principal when writing using ISystemClient [DB-1971] Pass claims principal and require leader flag when writing using ISystemClient Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants