Skip to content

[identity] add IsMachine extension and identity:users.read claim type for read-only operation is UserApi - #1003

Open
dkarkanas wants to merge 2 commits into
developfrom
feature/identity-user-read-claims
Open

[identity] add IsMachine extension and identity:users.read claim type for read-only operation is UserApi#1003
dkarkanas wants to merge 2 commits into
developfrom
feature/identity-user-read-claims

Conversation

@dkarkanas

Copy link
Copy Markdown
Contributor

This pull request introduces support for a new users.read scope, enabling machine users to access user and log reading endpoints with more granular permissions. It also updates authorization policies to recognize machine users and allows them to use the new scope. The most important changes are summarized below:

Authorization Policy Updates:

  • Updated the BeUsersReader policy to allow access if a principal has the new users.read scope and is a machine user, in addition to the existing requirements.
  • Updated the BeLogsReader policy to allow access if the principal has the logs scope and is a machine user, in addition to the existing requirements.

Scope and API Changes:

  • Added the new UsersRead scope (identity:users.read) to the SubScopes class for granular user read access.
  • Updated the allowed scopes for the users API to include the new users.read scope, enabling clients with this scope to access user management endpoints.

ClaimsPrincipal Extension:

  • Added an IsMachine extension method to ClaimsPrincipal to identify machine users (authenticated principals without a subject id claim).

Copilot AI review requested due to automatic review settings April 20, 2026 15:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 introduces a new granular scope (identity:users.read) intended to allow machine-to-machine principals to read users (and logs) with more fine-grained permissions in the Identity Server management APIs.

Changes:

  • Added a new sub-scope constant IdentityEndpoints.SubScopes.UsersRead = "identity:users.read".
  • Expanded Users API allowed scopes to include identity:users.read.
  • Updated BeUsersReader/BeLogsReader policies to recognize “machine users” via a new ClaimsPrincipal.IsMachine() extension.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/Indice.Features.Identity.Server/Manager/UsersApi.cs Adds UsersRead to the Users API route group allowed scopes.
src/Indice.Features.Identity.Server/Manager/IdentityEndpoints.cs Introduces IdentityEndpoints.SubScopes.UsersRead constant.
src/Indice.Features.Identity.Server/Extensions/ServiceCollectionExtensions.cs Updates authorization policies to allow machine principals under certain scopes.
src/Indice.Common/Security/ClaimsPrincipalExtensions.cs Adds IsMachine() extension method used by authorization policies.

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

Comment on lines 294 to 298
authOptions.AddPolicy(IdentityEndpoints.Policies.BeUsersReader, policy => {
policy.AddAuthenticationSchemes(IdentityEndpoints.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireAssertion(x => x.User.HasScope(IdentityEndpoints.SubScopes.Users) && x.User.CanReadUsers());
.RequireAssertion(x => (x.User.HasScope(IdentityEndpoints.SubScopes.Users) && x.User.CanReadUsers()) || (x.User.HasScope(IdentityEndpoints.SubScopes.UsersRead) && x.User.IsMachine()));
});

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

BeUsersReader policy is registered twice in this method. The later AddPolicy(IdentityEndpoints.Policies.BeUsersReader, ...) (lines 304-308) will overwrite the earlier registration (lines 294-298), effectively removing the new UsersRead/IsMachine() logic at runtime. Remove the duplicate policy registration or update it so the final registered policy includes the new assertion.

Copilot uses AI. Check for mistakes.
group.WithGroupName("identity");
// Add security requirements, all incoming requests to this API *must* be authenticated with a valid user.
var allowedScopes = new[] { options.ApiScope, IdentityEndpoints.SubScopes.Users }.FilterOutNulls().ToArray();
var allowedScopes = new[] { options.ApiScope, IdentityEndpoints.SubScopes.Users , IdentityEndpoints.SubScopes.UsersRead }.FilterOutNulls().ToArray();

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

By adding IdentityEndpoints.SubScopes.UsersRead to the route group's allowedScopes, any endpoint that relies only on the group-level RequireClaim(Scope, allowedScopes) (i.e., does not add BeUsersReader/BeUsersWriter explicitly) becomes callable with the read-only scope. In this group there is at least one non-GET endpoint (POST {userId}/email/confirmation) without an explicit writer policy, so identity:users.read can trigger side effects. Add explicit authorization (likely BeUsersWriter) to that endpoint, or split read vs write endpoints into separate groups with different allowed scopes.

Copilot uses AI. Check for mistakes.
policy.AddAuthenticationSchemes(IdentityEndpoints.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireAssertion(x => x.User.HasScope(IdentityEndpoints.SubScopes.Logs) && x.User.CanReadUsers());
.RequireAssertion(x => (x.User.HasScope(IdentityEndpoints.SubScopes.Logs) && x.User.CanReadUsers()) || (x.User.HasScope(IdentityEndpoints.SubScopes.Logs) && x.User.IsMachine()));

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

The BeLogsReader assertion repeats HasScope(IdentityEndpoints.SubScopes.Logs) in both branches, which makes the intent harder to read. Consider factoring the scope check once (e.g., require the logs scope and then check (CanReadUsers() || IsMachine())) to keep the policy easier to maintain.

Suggested change
.RequireAssertion(x => (x.User.HasScope(IdentityEndpoints.SubScopes.Logs) && x.User.CanReadUsers()) || (x.User.HasScope(IdentityEndpoints.SubScopes.Logs) && x.User.IsMachine()));
.RequireAssertion(x => x.User.HasScope(IdentityEndpoints.SubScopes.Logs) && (x.User.CanReadUsers() || x.User.IsMachine()));

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +84
public static bool IsMachine(this ClaimsPrincipal principal) =>
principal.Identity?.IsAuthenticated is true && principal.FindSubjectId() is null;

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

IsMachine uses principal.Identity?.IsAuthenticated, which only reflects the first identity on the principal. If a principal contains multiple identities, this can incorrectly return false even when another identity is authenticated. Consider using principal.Identities.Any(i => i.IsAuthenticated) (or, since callers already use RequireAuthenticatedUser(), dropping the authentication check and only checking the absence of subject id).

Copilot uses AI. Check for mistakes.
Comment on lines 294 to 298
authOptions.AddPolicy(IdentityEndpoints.Policies.BeUsersReader, policy => {
policy.AddAuthenticationSchemes(IdentityEndpoints.AuthenticationScheme)
.RequireAuthenticatedUser()
.RequireAssertion(x => x.User.HasScope(IdentityEndpoints.SubScopes.Users) && x.User.CanReadUsers());
.RequireAssertion(x => (x.User.HasScope(IdentityEndpoints.SubScopes.Users) && x.User.CanReadUsers()) || (x.User.HasScope(IdentityEndpoints.SubScopes.UsersRead) && x.User.IsMachine()));
});

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

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

New authorization behavior is introduced here (identity:users.read + IsMachine() for BeUsersReader, and machine access for logs). There are existing identity endpoint tests (e.g., UserApiTests/UserAvatarApiTest) but none appear to cover these new scope/policy combinations. Add tests that assert: (1) a machine principal with identity:users.read can call GET user endpoints, (2) it cannot call write endpoints, and (3) a machine principal with identity:logs can call log-reading endpoints.

Copilot uses AI. Check for mistakes.
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.

3 participants