Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best design pattern to control permissions on a "per object per user" basis with ServiceStack?

I know that ServiceStack provides a RequiredRole attribute to control permissions, however, this does not fully work for my use case. I have a website with a lot of user-generated content. Users can only edit documents they have explicit permissions to. Permissions are controlled per object or group of the object. So, if a user is an admin of a group then they can edit all documents managed by that group.

What is the best design pattern to control access to a request on a per object per user basis? I want to approach this with as DRY a methodology as possible as it will affect 95% of all my API endpoints.

Also, can this be integrated with FluentValidation and return appropriate HTTP responses?

Many thanks,

Richard.

like image 209
richardwhatever Avatar asked Mar 13 '14 10:03

richardwhatever


1 Answers

I use per object permissions in my ServiceStack applications. Effectively this is an Access-Control-List (ACL).

I have created a Working Self Hosted Console Example which you can fork on GitHub.

ACL pattern:

I use the database structure shown in the diagram below, whereby resources in my database such as documents, files, contacts etc (any resource I want to protect) are all given an ObjectType id.

Database

The permissions table contains rules that apply to specific users, specific groups, specific objects and specific object types, and is flexible to accept them in combinations, where a null value will be treated like a wildcard.

Securing the service and routes:

I find the easiest way to handle them is to use a request filter attribute. With my solution I simply add a couple of attributes to my request route declaration:

[RequirePermission(ObjectType.Document)]
[Route("/Documents/{Id}", "GET")]
public class DocumentRequest : IReturn<string>
{
    [ObjectId]
    public int Id { get; set; }
}

[Authenticate]
public class DocumentService : Service
{
    public string Get(DocumentRequest request)
    {
        // We have permission to access this document
    }
}

I have a filter attribute call RequirePermission, this will perform the check to see that the current user requesting the DTO DocumentRequest has access to the Document object whose ObjectId is given by the property Id. That's all there is to wiring up the checking on my routes, so it's very DRY.

The RequirePermission request filter attribute:

The job of testing for permission is done in the filter attribute, before reaching the service's action method. It has the lowest priority which means it will run before validation filters.

This method will get the active session, a custom session type (details below), which provides the active user's Id and the group Ids they are permitted to access. It will also determine the objectId if any from the request.

It determines the object id by examining the request DTO's properties to find the value having the [ObjectId] attribute.

With that information it will query the permission source to find the most appropriate permission.

public class RequirePermissionAttribute : Attribute, IHasRequestFilter
{
    readonly int objectType;

    public RequirePermissionAttribute(int objectType)
    {
        // Set the object type
        this.objectType = objectType;
    }

    IHasRequestFilter IHasRequestFilter.Copy()
    {
        return this;
    }

    public void RequestFilter(IRequest req, IResponse res, object requestDto)
    {
        // Get the active user's session
        var session = req.GetSession() as MyServiceUserSession;
        if(session == null || session.UserAuthId == 0)
            throw HttpError.Unauthorized("You do not have a valid session");

        // Determine the Id of the requested object, if applicable
        int? objectId = null;
        var property = requestDto.GetType().GetPublicProperties().FirstOrDefault(p=>Attribute.IsDefined(p, typeof(ObjectIdAttribute)));
        if(property != null)
            objectId = property.GetValue(requestDto,null) as int?;

        // You will want to use your database here instead to the Mock database I'm using
        // So resolve it from the container
        // var db = HostContext.TryResolve<IDbConnectionFactory>().OpenDbConnection());
        // You will need to write the equivalent 'hasPermission' query with your provider

        // Get the most appropriate permission
        // The orderby clause ensures that priority is given to object specific permissions first, belonging to the user, then to groups having the permission
        // descending selects int value over null
        var hasPermission = session.IsAdministrator || 
                            (from p in Db.Permissions
                             where p.ObjectType == objectType && ((p.ObjectId == objectId || p.ObjectId == null) && (p.UserId == session.UserAuthId || p.UserId == null) && (session.Groups.Contains(p.GroupId) || p.GroupId == null))
                             orderby p.ObjectId descending, p.UserId descending, p.Permitted, p.GroupId descending
                             select p.Permitted).FirstOrDefault();

        if(!hasPermission)
            throw new HttpError(System.Net.HttpStatusCode.Forbidden, "Forbidden", "You do not have permission to access the requested object");
    }

    public int Priority { get { return int.MinValue; } }
}

Permission Priority:

When the permissions are read from the permission table, the highest priority permission is used to determine if they have access. The more specific the permission entry is, the higher the priority it has when the results are ordered.

  • Permissions matching the current user have greater priority than general permissions for all users i.e where UserId == null. Similarly a permission for the specifically requested object has higher priority than the general permission for that object type.

  • User specific permissions take precedence over group permissions. This means, that a user can be granted access by a group permission but be denied access at user level, or vice versa.

  • Where the user belongs to a group that allows them access to a resource and to another group that denies them access, then the user will have access.

  • The default rule is to deny access.

Implementation:

In my example code above I have used this linq query to determine if the user has permission. The example uses a mocked database, and you will need to substitute it with your own provider.

session.IsAdministrator || 
(from p in Db.Permissions
 where p.ObjectType == objectType && 
     ((p.ObjectId == objectId || p.ObjectId == null) && 
     (p.UserId == session.UserAuthId || p.UserId == null) &&
     (session.Groups.Contains(p.GroupId) || p.GroupId == null))
 orderby p.ObjectId descending, p.UserId descending, p.Permitted, p.GroupId descending
 select p.Permitted).FirstOrDefault();

Custom Session:

I have used a custom session object to store the group memberships, these are looked up and added to the session when the user is authenticated.

// Custom session handles adding group membership information to our session
public class MyServiceUserSession : AuthUserSession
{
    public int?[] Groups { get; set; }
    public bool IsAdministrator { get; set; }

    // The int value of our UserId is converted to a string!?! :( by ServiceStack, we want an int
    public new int UserAuthId { 
        get { return base.UserAuthId == null ? 0 : int.Parse(base.UserAuthId); }
        set { base.UserAuthId = value.ToString(); }
    }


    // Helper method to convert the int[] to int?[]
    // Groups needs to allow for null in Contains method check in permissions
    // Never set a member of Groups to null
    static T?[] ConvertArray<T>(T[] array) where T : struct
    {
        T?[] nullableArray = new T?[array.Length];
        for(int i = 0; i < array.Length; i++)
            nullableArray[i] = array[i];
        return nullableArray;
    }

    public override void OnAuthenticated(IServiceBase authService, ServiceStack.Auth.IAuthSession session, ServiceStack.Auth.IAuthTokens tokens, System.Collections.Generic.Dictionary<string, string> authInfo)
    {
        // Determine UserId from the Username that is in the session
        var userId = Db.Users.Where(u => u.Username == session.UserName).Select(u => u.Id).First();

        // Determine the Group Memberships of the User using the UserId
        var groups = Db.GroupMembers.Where(g => g.UserId == userId).Select(g => g.GroupId).ToArray();

        IsAdministrator = groups.Contains(1); // Set IsAdministrator (where 1 is the Id of the Administrator Group)

        Groups = ConvertArray<int>(groups);
        base.OnAuthenticated(authService, this, tokens, authInfo);
    }
}

I hope you find this example useful. Let me know if anything is unclear.

Fluent Validation:

Also, can this be integrated with FluentValidation and return appropriate HTTP responses?

You shouldn't try and do this in the validation handler, because it is not validation. Checking if you have permission is a verification process. If you require to check something against a specific value in a datasource you are no longer performing validation. See this other answer of mine which also covers this.

like image 189
Scott Avatar answered Nov 09 '22 08:11

Scott