Identity Auth Permissions

I am a bit confused on how to do permissions. I want granular permissions so I have marked my auto CRUD models up with permission attributes. In /admin-ui it only shows roles though.

Is there any way to manage users permissions or an example project with granular permissions with identity auth?

To my knowledge, Permissions aren’t baked into AspNetIdentity as a first class concept, only:

For more granular control, you could combine these, but since this isn’t standard, it’s not something ServiceStack has baked into the admin-ui for ASPNET Identity. I find once more control beyond role based permission is needed, it usually starts to touch on resource level authorization which is after the service code has allowed the request, and where code-first attributes are less practical. Hope that helps.

Thanks for the reply. It seems like the most compatible way would be to use multiple roles and then set policies to define the logic for how combinations of roles work at granular level. That way I can still manage access inside /admin-ui as it would just be adding combinations of roles.

Is there any drawback to adding the granular permissions to claims like FinancialReportAccess=true etc…

I guess claims are not manageable in the /admin-ui so that’s one drawback, but on the surface it seems like it might be a better route that lots of policies.

I’d recommend following the Identity Auth way of doing things, which look like they recommend using a custom policy:

// Configure your policies
builder.Services.AddAuthorization(options =>
  options.AddPolicy("Something",
  policy => policy.RequireClaim("Permission","CanViewPage","CanViewAnything")));

Whilst there’s no standard Claim Type for permissions, if you use JwtClaimTypes.Permissions (aka “perms”) it should automatically populate it in the ServiceStack Authenticated UserSession’s Permissions when populated from an Identity Auth ClaimsPrincipal.

OK that makes sense. I an easy way to make that manageable.

If I use UserManager like:

await userManager.AddClaimAsync(adminUser, new Claim(JwtClaimTypes.Permission, "CanViewReport"));

It persists it to the AspNetUserClaims table but I don’t see any way to manage that with SS.

I came up with extending the Application User like:

public class ApplicationUser : IdentityUser
{
    // ..
    public List<string> Permissions { get; set; } = new List<string>();
}

Then inside AdditionalUserClaimsPrincipalFactory I populate the claims like:

public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
    var principal = await base.CreateAsync(user);
    var identity = (ClaimsIdentity)principal.Identity!;

    var claims = new List<Claim>();

    if (user.Permissions?.Count > 0)
    {
        foreach(var permission in user.Permissions)
        {
            claims.Add(new Claim(JwtClaimTypes.Permissions, permission));
        }
    }

    identity.AddClaims(claims);
    return principal;
}

And then I can add this to the /admin-ui edit form like so:

options.AdminUsersFeature(feature =>
{
    feature.FormLayout =
    [
        // ..
        Input.For<ApplicationUser>(x => x.Permissions, c => c.Type = Input.Types.Tag),
    ];
});

Does this seem like an OK solution? I am always a bit trepidations when it comes to auth so appreciate any input.

Also, I noticed in the components gallery there is a multi-select:

image

Is it possible to use that in locode forms? It would be a much nicer solution as the free-form tag inputs leave some room for error.

I tried with Combobox but can’t see a multiselect option:

Input.For<ApplicationUser>(x => x.Permissions, c => {
    c.Type = Input.Types.Combobox;
    c.AllowableEntries = new KeyValuePair<string, string>[]
                        {
                            new KeyValuePair<string, string>("Can View Reports", "CanViewReport"),
                            new KeyValuePair<string, string>("Can Delete Reports", "CanDeleteReport"),
                        };

}),

Many thanks for all the support and advice!

That’s the right place to add the claims so that they’re included in the Auth Cookie.

There is a Multiple Property on InputInfo that you can set:

options.AdminUsersFeature(feature =>
{
    feature.FormLayout.Add(Input.For<ApplicationUser>(x => x.Permissions, c => {
        c.Type = Input.Types.Combobox;
        c.AllowableEntries = [
            new("Can View Reports", "CanViewReport"),
            new("Can Delete Reports", "CanDeleteReport")
        ];
        c.Multiple = true;
    }));
});

It’s on OK solution if you can get it to work, but you’re basically limited to the declarative functionality available.

Although I’m more inclined to use a TagInput component instead.

So the drawback of doing this is I can’t actually use policies because there isn’t a declarative attribute for policies.

How would I use policies with ServiceStack Identity auth & Auto CRUD?

I had a crack at making one based on the claims attribute. It seems to work OK

/// <summary>
/// Protect access to this API to only Authenticated Users who pass policy requirements
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class RequiredPolicyAttribute : AuthenticateAttribute
{
    public string PolicyName { get; set; }

    public RequiredPolicyAttribute(ApplyTo applyTo, string policyName)
    {
        this.PolicyName = policyName;
        this.ApplyTo = applyTo;
        this.Priority = (int)RequestFilterPriority.RequiredRole;
    }

    public RequiredPolicyAttribute(string policyName)
        : this(ApplyTo.All, policyName) { }

    public override async Task ExecuteAsync(IRequest req, IResponse res, object requestDto)
    {
        if (HostContext.AppHost.HasValidAuthSecret(req))
            return;

        await base.ExecuteAsync(req, res, requestDto); //first check if session is authenticated
        if (res.IsClosed)
            return; //AuthenticateAttribute already closed the request (ie auth failed)

        if (await PassPolicy(req, PolicyName))
            return;

        await HostContext.AppHost.HandleShortCircuitedErrors(req, res, requestDto,
            HttpStatusCode.Forbidden,"Policy {0} failed".LocalizeFmt(req, PolicyName));
    }

    public static async Task<bool> PassPolicy(IRequest req, string policyName)
    {
        var principal = req.GetClaimsPrincipal();
        //if (principal.IsInRole(RoleNames.Admin))
        //    return true;

        var authorizationService = req.TryResolve<IAuthorizationService>();
        var result = await authorizationService.AuthorizeAsync(principal, null, policyName);

        return result.Succeeded;
    }


    protected bool Equals(RequiredPolicyAttribute other)
    {
        return base.Equals(other) && string.Equals(PolicyName, other.PolicyName);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((RequiredPolicyAttribute)obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            int hashCode = base.GetHashCode();
            hashCode = (hashCode * 397) ^ (PolicyName != null ? PolicyName.GetHashCode() : 0);
            return hashCode;
        }
    }
}

I meant your UI options are limited when extending the built-in UI vs implementing a Custom UI that you have full control over.

Note the [Authenticate(Policy)] attribute has a Policy which attaches the Required Policy to the endpoint. Alternatively when using Endpoints you can instead use ASP .NET Core’s [Authorize] attribute to secure the endpoint.

Ah I didn’t see that property, thanks.

You are right, it would probably be better to make custom UI and use the UserManager directly. Are there any plans to update /admin-ui to support editing user claims?

We can look into it, if you drop a quick feature request at https://servicestack.net/ideas you’ll get notified when it’s available.