EntityAuthorizeClaims - not working?

In my Sphinx server, I do the following:

procedure TfrmMain.SphinxConfigConfigureToken(Sender: TObject; Args: TConfigureTokenArgs);
begin
  Args.Token.Claims.AddOrSet('user_role', 'user');
  if SameText(Args.User.UserName.Value, 'mark') then
    Args.Token.Claims.AddOrSet('user_role', 'admin');
end;

I have three entity types with the following (partial) implementation for my XData server:

  [Entity]
  [EntityAuthorizeClaims('user_role', 'user', EntitySetPermissionsAll)]
  [EntityAuthorizeClaims('user_role', 'admin', EntitySetPermissionsAll)]
  [Table('Invoice')]
  [Id('FID', TIdGenerator.Guid)]
  TInvoice = class
  private
    [Column('ID', [TColumnProp.Required])]
    FID: TGuid;
    .........
  [Entity]
  [EntityAuthorizeClaims('user_role', 'user', EntitySetPermissionsRead)]
  [EntityAuthorizeClaims('user_role', 'admin', EntitySetPermissionsAll)]
  [Table('InvoiceItem')]
  [Id('FID', TIdGenerator.Guid)]
  TInvoiceItem = class
  private
    [Column('ID', [TColumnProp.Required])]
    FID: TGuid;
    ......
  [Entity]
  [Table('Secret')]
  [EntityAuthorizeClaims('user_role', 'user', EntitySetPermissionsNone)]
  [EntityAuthorizeClaims('user_role', 'admin', EntitySetPermissionsAll)]
  [Id('FID', TIdGenerator.Guid)]
  TSecret = class
  private
    [Column('ID', [TColumnProp.Required])]
    FID: TGuid;
    ...............

Logging in as user 'mark' (user_role = admin) I do not have access (in the XData server) to the TInvoice and TInvoiceItem entities (403) but I do have access to the TSecret entities. Logging in as any other user (user_role = user) I don't have access to any entity.

What am I misunderstanding/doing wrong?

Adding/setting a claim 'scope' with a value 'user' or 'user admin' in combination with EntityAuthorizeScopes worked fine. Is my understanding correct that this scope is not related to the scope of the ClientApp in Sphinx?

1 Like

I guess that you misunderstood how XData behaves when there are multiple EntityAuthorizeClaims attributes.

When you have two or more of EntityAuthorizeClaims attributes with a specific entity set permission added to a class, such entity set permission will only be available if all claims are present.

Thus, a definition like this:

  [EntityAuthorizeClaims('user_role', 'user', EntitySetPermissionsNone)]
  [EntityAuthorizeClaims('user_role', 'admin', EntitySetPermissionsAll)]
  TSecret = class

Means TEntitySetPermission.GET will be accepted if claim user_role has value admin.

While this definition:

  [EntityAuthorizeClaims('user_role', 'user', EntitySetPermissionsRead)]
  [EntityAuthorizeClaims('user_role', 'admin', EntitySetPermissionsAll)]
  TInvoiceItem = class

Means TEntitySetPermission.GET will be accepted if claim user_role has value admin and claim user_role has value user.

Hi Wagner,

Thanks for clarifying, but this is quite a tricky implementation (of the attributes).

My first reaction was to take this as the attributes behave in a sort of AND fashion (i.e. both must be fulfilled). However, that doesn't explain why the TSecret entity works if user_role = 'admin' and TInvoiceItem doesn't for either user_role.

However, looking at this topic EntityAuthorizeScopes problem, it appears there must not be any overlap in permissions granted by the attributes. So for the TInvoiceItem entity user_role 'User' is also required for GET and LIST and therefore it doesn't work. In case of TSecret this is not required (GET/LIST not used for 'user') and therefore it works. Can you confirm this?

My intention (for a real-life application) was to control access rights by different levels of user_roles (e.g. user, poweruser, admin). With a single-value claim (as in this case 'user_role' containing a single value) that will become quite complex and error prone if my understanding above is now correct.

Based on above, below approach (using EntityAuthorizeScope with 'stacked' roles in scope) appears to work as desired and make things much easier to implement and therefore less error prone for my scenario.

  Args.Token.Claims.AddOrSet('scope', 'email user');
  if SameText(Args.User.UserName.Value, 'mark') then
    Args.Token.Claims.AddOrSet('scope', 'user poweruser admin');
  [Entity]
  [EntityAuthorizeScopes('user,poweruser,admin', EntitySetPermissionsAll)]
  [Table('Invoice')]
  [Id('FID', TIdGenerator.Guid)]
  TInvoice = class
   ....

  [Entity]
  [EntityAuthorizeScopes('user,poweruser,admin', EntitySetPermissionsRead)]
  [EntityAuthorizeScopes('poweruser,admin', EntitySetPermissionsWrite)]
  [Table('InvoiceItem')]
  [Id('FID', TIdGenerator.Guid)]
  TInvoiceItem = class
   ....

  [Entity]
  [EntityAuthorizeScopes('poweruser,admin', EntitySetPermissionsRead)]
  [EntityAuthorizeScopes('admin', EntitySetPermissionsWrite)]
  [Table('Secret')]
  [Id('FID', TIdGenerator.Guid)]
  TSecret = class
   ....

Or would you use a different approach for my scenario?

I must admit I still find this does my head in :rofl:

1 Like

Thanks Russell, your comment make me feel slightly better, as I have been struggling with various aspects of getting an XData/Sphinx setup working (and understand it) :laughing:

The solution with EntityAuthorizeScopes works fine so far and is somewhat easier to grasp. Makes me wonder in what scenario one would use EntityAuthorizeClaims (i.e. is it of added value?).

Now on to somehow persisting user roles in my Sphinx server... :thinking:

I’d like to clarify how the EntityAuthorizeClaims mechanism works in relation to multiple attributes and entity endpoints, as this can understandably cause confusion.

How EntityAuthorizeClaims Works — The “AND” Behavior

When you apply EntityAuthorizeClaims with multiple permissions listed, it works with an AND logic in the sense that each specified endpoint permission requires the claims specified for authorization.

This is because each entity in TMS XData exposes multiple endpoints, each corresponding to specific CRUD operations:

  • List
  • Get
  • Insert
  • Modify
  • Delete

These are represented internally by the enum TEntitySetPermission.

Grouping Permissions & Endpoint Mapping

To simplify managing permissions, there are helper sets defined:

EntitySetPermissionsRead = [TEntitySetPermission.List, TEntitySetPermission.Get];
EntitySetPermissionsWrite = [TEntitySetPermission.Insert, TEntitySetPermission.Modify, TEntitySetPermission.Delete];

When you decorate your entity with an attribute like:

[EntityAuthorizeClaims(..., ..., [TEntitySetPermission.Insert, TEntitySetPermission.Modify])]

you are essentially specifying which endpoints this authorization applies to.

What Does This Mean?

  • The last parameter in EntityAuthorizeClaims is a set of the entity’s endpoints.
  • The claims you specify inside EntityAuthorizeClaims are required for each of those endpoints.
  • So, for example, the attribute above means:
    • To call the Insert endpoint, claims must be valid.
    • To call the Modify endpoint, claims must also be valid.

That’s why it behaves as if you applied separate attributes:

[EntityAuthorizeClaimsForInsert(...)] 
[EntityAuthorizeClaimsForModify(...)]

but instead of having to write two attributes, you just specify both permissions in the last parameter’s set.

Why the “AND” Logic?

  • Because the attribute applies claims validation for each endpoint it covers.
  • When multiple permissions are in the set, the user needs claims valid for all those endpoints.
  • If the user misses required claims for any, access is denied.

Summary

  • Every entity endpoint (List, Get, Insert, Modify, Delete) has its own permissions.
  • EntityAuthorizeClaims applies claims to a specific subset of these endpoints.
  • Using multiple endpoints in the attribute means claims are required for each one separately.
  • This is a convenient shortcut instead of using multiple separate attributes.

I hope this clarifies how EntityAuthorizeClaims works under the hood and how to properly specify it to meet your authorization needs.

1 Like

That's exactly the purpose of the scope claim (to work as a kind of roles added to the user) and that's the purpose of th behavior of AuthorizeScopes end EntityAuthorizeScopes.

Whatever scenario you want a claim to be present in the token. It's another level of flexibility. But for full flexibility you should simply use the server-side events and put your logic in Delphi code: Authentication and Authorization | TMS XData documentation

The attributes are helpers for the most common cases.

Thanks, Wagner.

I suppose this is now a good ticket as reference for other (future) users struggling with the same issue. :+1:t2:

2 Likes