3
\$\begingroup\$

Role based security does not work for me well in a multitenant app, as I need to query DB to decide on permission allowance. Here is the solution I came to.

All implementations of this interface will be auto registered in IoC container (note covariance/contravariance support):

public interface IResourceAuthorizer<out TResource, in TAccess> 
    : IResourceAuthorizer where TAccess : Access
{
    bool Grants(int resourceId, UserId userId);
}

Where marker interface IResourceAuthorizer is:

public interface IResourceAuthorizer {}

and Access is:

public abstract class Access {}
public abstract class Read : Access {}
public abstract class Write : Access {}
public abstract class Create : Write {}
public abstract class Update : Write {}
public abstract class Delete : Write {}

It can be easily extended with Execute, etc.

Possible implementations might look like this (demo code, some database querying logic is supposed to be here):

public class Company {} // some resource to authorize access to
public class Corporation : Company {}

public class CorporationAuthorizer :
    IResourceAuthorizer<Corporation, Access> 
{
    public bool Grants(int resourceId, UserId userId) => 
        resourceId % 2 != 0;
}

or

public class CompanyReadAuthorizer :
    IResourceAuthorizer<Company, Read> 
{
    public bool Grants(int resourceId, UserId userId) => 
        true;
}

public class CompanyCreateUpdateAuthorizer :
    IResourceAuthorizer<Company, Create> 
    IResourceAuthorizer<Company, Update> 
{
    public bool Grants(int resourceId, UserId userId) => 
        userId.Authenticated;
}

The following interface and its implementation will be used for permission testing after registering them in IoC container:

public interface IAuthorizer
{
    bool Grants<TResource, TAccess>(int resourceId, UserId userId)
        where TAccess : Access;
}

public class Authorizer : IAuthorizer
{
    public Authorizer(IEnumerable<IResourceAuthorizer> authorizers)
    {
        Authorizers = authorizers;
    }

    IEnumerable<IResourceAuthorizer> Authorizers { get; }

    public bool Grants<TResource, TAccess>(int resourceId, UserId userId) 
        where TAccess : Access
    {
        var authorizer = Authorizers
            .OfType<IResourceAuthorizer<TResource, TAccess>>()
            .SingleOrDefault();

        if (authorizer == null)
            throw new NotImplementedException();

        return authorizer.Grants(resourceId, userId);
    }
}

There are some extensions to help with consuming of this component:

public static class AuthorizerExtensions
{
    public static bool Grants<TResource, TAccess>(
        this IAuthorizer authorizer)
        where TAccess : Access =>
        authorizer.Grants<TResource, TAccess>(0, UserId.Current);

    public static bool Grants<TResource, TAccess>(
        this IAuthorizer authorizer, UserId userId)
        where TAccess : Access =>
        authorizer.Grants<TResource, TAccess>(0, userId);

    public static bool Grants<TResource, TAccess>(
        this IAuthorizer authorizer, int resourceId)
        where TAccess : Access =>
        authorizer.Grants<TResource, TAccess>(resourceId, UserId.Current);
}

public static class AuthorizerGuards
{
    public static void Authorize<TResource, TAccess>(
        this IAuthorizer authorizer)
        where TAccess : Access
    {
        if (!authorizer.Grants<TResource, TAccess>())
            throw new UnauthorizedAccessException();
    }

    public static void Authorize<TResource, TAccess>(
        this IAuthorizer authorizer, UserId userId)
        where TAccess : Access
    {
        if (!authorizer.Grants<TResource, TAccess>(userId))
            throw new UnauthorizedAccessException();
    }

    public static void Authorize<TResource, TAccess>(
        this IAuthorizer authorizer, int resourceId)
        where TAccess : Access
    {
        if (!authorizer.Grants<TResource, TAccess>(resourceId))
            throw new UnauthorizedAccessException();
    }

    public static void Authorize<TResource, TAccess>(
        this IAuthorizer authorizer, int resourceId, UserId userId)
        where TAccess : Access
    {
        if (!authorizer.Grants<TResource, TAccess>(resourceId, userId))
            throw new UnauthorizedAccessException();
    }
}

OK, now we can use it:

class CompanyController : Controller
{
    public CompanyController(IAuthorizer authorizer)
    {
        Authorizer = authorizer;
    }

    IAuthorizer Authorizer { get; }

    public void Delete(int id)
    {
        Authorizer.Authorize<Company, Delete>(id);
        // ...
    }
}

UserId type described here. GitHub repository (just a sketch).

Does it make sense? Would you personally go with this approach to structure your code?

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.