Securing ASP.Net MVC in Intranet scenario with Active Directory groups is just a matter of applying Authorize attributes to the controllers or action methods:

[Authorize(Roles="IT")]
public ActionResult Index()  
{
    return View();
}        

This does the job but as the number of controllers and action methods grows, it becomes harder to visualize the overal security. Administrators would want to know who has access to what and which Active Directory group a given user should be added in order to be granted access.

One of the way of visualizing and designing permissions sometimes is called security matrix. This is a table where rows represent system's features and column roles (Active Directory groups). If cell in the intesection of feature and role has "X" then all members of this role have access to this feature:

IT Admins
View Home Page X X
Change Settings X


Having a security matrix is helpful in designing the security, but it would be nice to apply permissions in code and have a helper page that reflects over the code, finds features and roles and displays them as a table. In that way anybody can look into this hepler page to understand actual security model.

Here's what it takes to make it happen.

Custom Authorization Attirubute

/// <summary>
/// Used for applying permissions to the action methods. For example: 
/// [Feature("View Home Page", "IT")]                   
/// public ActionResult Index()
/// { ...
/// </summary>
public class Feature : AuthorizeAttribute  
{
    private string _name;
    private string[] _allowedGroups;                

    public Feature(string name, params string[] groups)
    {
        _name = name;
        _allowedGroups = groups;
    }

    public Feature(string name, ICollection<string> groups)
        : this(name, groups.ToArray())
    {
    }

    public string Name
    { 
        get { return _name; }
    }

    public ICollection<string> Groups
    { 
        get { return _allowedGroups; }
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        bool found = false;
        if (httpContext.Request.IsAuthenticated)
        {   
            foreach (var group in _allowedGroups)
            {
                if (httpContext.User.IsInRole(group))
                {
                    found = true;
                    break;
                }
            }                
        }

        return found;
    }
}

This attribute can be used on action methods to define feature names and roles:

public class HomeController : Controller  
{        
    [Feature("View Home Page", "IT", "Admins")]
    public ActionResult Index()
    {
        return View();
    }        
}

SecurityMatrix class

public class SecurityMatrix  
{        
    public ICollection<string> Features { get; private set; }
    public ICollection<string> Groups { get; private set; }
    public bool[,] Matrix { get; private set; }

    public SecurityMatrix()
    {
        Features = new List<string>();
        Groups = new List<string>();
        Matrix = new bool[1, 1];
    }

    public void BuildMatrix(ICollection<Feature> features)
    {
        Features = features.Select(s => s.Name).ToList();
        Groups = features.SelectMany(s => s.Groups).Distinct().ToList();
        int numberOfFeatures = Features.Count;
        int numberOfGroups = Groups.Count;
        Matrix = new bool[numberOfFeatures, numberOfGroups];

        // Note: this runs in quadratic time
        for (int i = 0; i < numberOfFeatures; i++)
        {
            for (int j = 0; j < numberOfGroups; j++)
            {
                var feature = features.ElementAt(i);
                Matrix[i, j] = feature.Groups.Contains(Groups.ElementAt(j));
            }
        }
    }
}

Permissions Service

Then we need a service with two methods. First one uses reflection to find all custom attributes and builds the security matrix:

public SecurityMatrix GetSecurityMatrix()  
{
    var features = new List<Feature>();
    Assembly asm = Assembly.Load("Lisa"); 
    Type[] types = asm.GetTypes();
    IEnumerable<Type> controllers = types.Where(s => s.BaseType == typeof(Controller));
    IEnumerable<MethodInfo> actionMethods = controllers.SelectMany(s => s.GetMethods());
    IEnumerable<CustomAttributeData> featureAttributes = actionMethods.SelectMany(s => s.CustomAttributes).Where(a => a.AttributeType == typeof(Feature));

    foreach(var attribute in featureAttributes)
    {
        object[] p = attribute.ConstructorArguments.Select(s => s.Value).ToArray();
        string featureName = p[0].ToString();
        List<string> groups = ((ReadOnlyCollection<CustomAttributeTypedArgument>)p[1]).Select(s => s.Value.ToString()).ToList();                
        var feature = (Feature)Activator.CreateInstance(attribute.AttributeType, featureName, groups);
        features.Add(feature);
    }

    var securityMatrix = new SecurityMatrix();
    securityMatrix.BuildMatrix(features);
    return securityMatrix;            
}

Second one is to pull members from AD groups:

public ICollection<string> GetGroupMembers(string groupName)  
{            
    var context = new PrincipalContext(ContextType.Domain);
    var groupPrincipal = GroupPrincipal.FindByIdentity(context, IdentityType.Name, groupName);
    if (groupPrincipal == null)            
    {
        return new List<string>();
    }
    else
    {
        var members = groupPrincipal.GetMembers(false).Select(s => s.Name).ToList();
        return members;
    }
}

Controller and View

These are fairly straightforward:

public class PermissionsController : Controller  
{
    private IPermissionsService _service;

    public PermissionsController (IPermissionsService service)
    {
        _service = service;
    }

    public ActionResult Index()
    {
        SecurityMatrix domainSecurityMatrix = _service.GetSecurityMatrix();
        var model = Mapper.Map<ViewModels.SecurityMatrix>(domainSecurityMatrix);
        return View(model);
    }

    public ActionResult Members(string groupName)
    {            
        ICollection<string> members = _service.GetGroupMembers(groupName);
        var model = new ViewModels.GroupMembers { GroupName = groupName, Members = members };
        return View(model);
    }
}
@using ViewModels;
@model SecurityMatrix

<table class="table table-striped table-bordered">  
    <thead>
        <tr>
            <th>Feature</th>
            @foreach (string groupName in Model.Groups)
            {
                var url = Url.Action("Members", new { groupName = groupName });
                <th><a title="View Group Members" href="@url">@groupName <i class="glyphicon glyphicon-user"></i></a></th>
            }
        </tr>
    </thead>
    <tbody>
        @for (int i = 0; i < Model.Features.Length; i++)
        {
            string feature = Model.Features[i];
            <tr>
                <td>@feature</td>
                @for (int j = 0; j < Model.Groups.Length; j++)
                {
                    string character = Model.Matrix[i, j] ? "x" : string.Empty;
                    <td>@character</td>
                }
            </tr>
        }                
    </tbody>
</table>  

This will be produce a helper page just like shown above.