Since I started doing MVC instead of webforms I have mostly been a happier guy but verifying your Authorization was easier in Webforms in my opinion. With a little bit of T4 love it can bee easy in MVC too.
What I did in Webforms was that I put all my restricted pages in folders and then locked down the folders. I even had a graphical interface to show which folder a certain role had access to.
In MVC you can do something similar by splitting your controllers so that you lock down the whole controller that administrate the item and leave the reading controller open. It didn't really fit into my plan and I could still forget to lock down a controller.
When I could not figure out a way to guarantee that all my restricted actions really where restricted in code I started to think about if I could build a tool that atleast makes it easy to verfiy my Authorization settings.
What I really wanted was a graphical presentation of all my actions looking something like this
Action | Role1 | Role2 |
Create | False | True |
Index | True | True |
I thought of either creating a WPF application that would parse my c# controllers using regular expressions or do it using T4 templates. I like to have some hair on my head so I decided string parsing was out of the question and T4 templates it was.
Here is how the finished matrix looks like:
Download the source to follow along as some of the code in the post might be hard to read. You should probably install the T4 editor extension for VS2010. It can be found among the Extensions.
My authorization
I do not like magic strings so I have defined my roles in an enum
namespace System.Web.Mvc
{
[Serializable]
[Flags]
public enum SystemRoles
{
Admin = 1 << 0,
Member = 1 << 1,
ShopManager = 1 << 2,
ContactManager = 1 << 3,
PartialMember = 1 << 4
}
}
To work with this I need a custom Authorization attribute. At http://schotime.net/blog/index.php/2009/02/17/custom-authorization-with-aspnet-mvc/ you will find the idea and explanation of the attribute I used. The attribute is included in the sample project.
This allows me to write my authorization like this:
[CustomAuthorize(Roles = SystemRoles.Admin | SystemRoles.ContactManager)]
public ActionResult LogOff()
{
FormsAuth.SignOut();
return RedirectToAction("Index", "Home");
}
That means that both Admin and ContactManager has access to that method but nobody else.
In my projects where I use the default membership I often have a setup where all admins are members and so on. This means that if Member has access it basically means that Admin has access too because all admins are members. When I create the matrix I would like to respect those rules and show that Admin has access whenever Member has access to make it more obvious who can do what.
Getting started with T4 templates
I had never written anything using this before so it was a slow start. I had the T4Mvc template by David Ebbo installed and I knew that his template would parse all the controllers and actions which is almost what I needed. The only thing I needed to do if I could get all the actions was to read the attributes of the the actions.
It turned out to be a fair chunk of code to get started with though and I almost gave up for a while. I had to find some other resources to get going. Scott Hanselman lists a few in this post
Lets get started with the code
If you are completly new to T4 you should know that there are three parts in the template.
- import, assemply and output directives to begin with.
- logic to generate the output, much like the MVC view
- the templates internal class/code
In this case I also have a settingsfile.
Imports and things like that
<#@ template language="C#v3.5" debug="true" hostspecific="true" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE80" #>
<#@ assembly name="VSLangProj" #>
<#@ assembly name="System.Web.Mvc" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="EnvDTE80" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating.Interfaces" #>
<#@ output extension=".html" #>
<# PrepareDataToRender(this); #>
I should admit that I'm not sure that all these imports are needed. This is stolen straight from David Ebbos template. The only thing that has changed is the output directive which tells the template that it should create a .html file.
The last line is the only interesting thing here. There is a method in internal code that this call goes to. This call make sure that all data about the controllers and actions are read when the output in is generated.
The settingsfile
In this file some customization can be done. The comments should explain the most
// The folder under the project that contains the controllers
const string ControllersFolder = "Controllers";
// The fully qualified name of the AuthorizationAttribute
const string AuthAttributeName = "System.Web.Mvc.CustomAuthorizeAttribute";
//What to call anonymous users
const string AnonymousRole = "Anonymous";
//The other roles in use. It is the name that is used in the authorization attribute
static readonly string[] Roles = new string[] {"SystemRoles.Admin","SystemRoles.Member","SystemRoles.PartialMember","SystemRoles.ShopManager","SystemRoles.ContactManager"};
//In the default ASP.NET Membership one user can have several roles.
//If the project has a structure where each Admin is also part of the Member group you can specify those implixations here.
//The dictionary should be read as: for each member in a role in the value the member is also in role in the key
static readonly Dictionary<string, string[]> ImplicationDictionary = new Dictionary<string, string[]>(){
{AnonymousRole, new string[] {"SystemRoles.Admin","SystemRoles.Member","SystemRoles.PartialMember","SystemRoles.ShopManager","SystemRoles.ContactManager"}},
{"SystemRoles.Admin", new string[0]},
{"SystemRoles.Member", new string[] {"SystemRoles.Admin","SystemRoles.ShopManager","SystemRoles.ContactManager"}},
{"SystemRoles.PartialMember", new string[0]},
{"SystemRoles.ShopManager", new string[0]},
{"SystemRoles.ContactManager", new string[0]}
};
The internal class/code
The first part here is also stolen from Ebbos template. I have removed some things that I do not need but otherwise it's the same code.
<#@ Include File="AMatrix.settings.t4" #>
<#+
This part tells the template to include my settings file. This makes all the constants set in the settingsfile accessible as if they where defined in the .tt file. The <#+ means that the code that follows untill next #> is internal code. After this lines comes the methods I copied.
const string ControllerSuffix = "Controller";
static DTE Dte;
static Project Project;
static string AppRoot;
static IList<Controller> Controllers;
static TextTransformation TT;
static string T4FileName;
static string HtmlStringType;
static string VisitedItems;
static Microsoft.CSharp.CSharpCodeProvider codeProvider = new Microsoft.CSharp.CSharpCodeProvider();
//From David Ebbos T4Mvc(Modified)
void PrepareDataToRender(TextTransformation tt) {
TT = tt;
T4FileName = Path.GetFileName(Host.TemplateFile);
Controllers = new List<Controller>();
VisitedItems = "";
// Get the DTE service from the host
var serviceProvider = Host as IServiceProvider;
if (serviceProvider != null) {
Dte = serviceProvider.GetService(typeof(SDTE)) as DTE;
}
// Fail if we couldn't get the DTE. This can happen when trying to run in TextTransform.exe
if (Dte == null) {
throw new Exception("T4MVC can only execute through the Visual Studio host");
}
Project = GetProjectContainingT4File(Dte);
if (Project == null) {
Error("Could not find the VS Project containing the T4 file.");
return;
}
// Get the path of the root folder of the app
AppRoot = Path.GetDirectoryName(Project.FullName) + '\\';
ProjectItem folder = GetProjectItem(Project, ControllersFolder);
FindControllersRecursive(folder);
}
//From David Ebbos T4Mvc(Modified)
Project GetProjectContainingT4File(DTE dte) {
// Find the .tt file's ProjectItem
ProjectItem projectItem = dte.Solution.FindProjectItem(Host.TemplateFile);
return projectItem.ContainingProject;
}
//From David Ebbos T4Mvc
ProjectItem GetProjectItem(Project project, string name) {
return GetProjectItem(project.ProjectItems, name);
}
//From David Ebbos T4Mvc
ProjectItem GetProjectItem(ProjectItems items, string subPath) {
ProjectItem current = null;
foreach (string name in subPath.Split('\\')) {
try {
// ProjectItems.Item() throws when it doesn't exist, so catch the exception
// to return null instead.
current = items.Item(name);
}
catch {
// If any chunk couldn't be found, fail
return null;
}
items = current.ProjectItems;
}
return current;
}
I haven't looked at exactly what the 3 last methods does. The first starts by initiating the DTE and getting a reference to the Project. Only the last two lines here is my code. The first line gets a reference to the Controllers folder and the second line calls the method that recursivly looks for controllers here is where the fun begins.
void FindControllersRecursive(ProjectItem projectItem){
// Recurse into all the sub-items (both files and folder can have some - e.g. .tt files)
foreach (ProjectItem item in projectItem.ProjectItems) {
FindControllersRecursive(item);
}
if (projectItem.FileCodeModel != null) {
ProcessControllerActionMethods(projectItem.FileCodeModel);
}
}
Here we recurse for all items and if the item we are currently looking at is a code file we pass it on for processing.
MSDN about ProjectItem.FileCodeModel: A FileCodeModel object is returned only for project items that are code files and in projects that implement the Visual Studio code model.
Before we continue looking at the methods there are two classes I defined that will be used in next method.
class Controller{
public Controller(){
Actions = new List<Action>();
}
public bool HasAuthorization { get; set; }
public Dictionary<string, bool> AuthorizationDictionary { get; set; }
public string Name{get; set; }
public IList<Action> Actions { get; set; }
}
class Action {
public string Name{get; set; }
public bool HasAuthorization { get; set; }
public Dictionary<string, bool> AuthorizationDictionary { get; set; }
public string HttpVerb { get; set; }
}
It's nothing special here. The only thing to note is that both the controller and the Action has AuthorizationDictionary. You can place AuthorizationAttributes on the controller which will be valid for all actions in that controller so we need both to get the proper results later. Let's get on with the Processing the code file.
void ProcessControllerActionMethods(FileCodeModel codeModel) {
//Loop over the using statements and namespaces
foreach (CodeElement elem in codeModel.CodeElements)
{
//If we have the namespace loop over the classes in the namespace untill we find a class with the Controller suffix
if (elem.Kind == vsCMElement.vsCMElementNamespace){
foreach (CodeElement classLevelElem in elem.Children)
{
if (classLevelElem.Kind == vsCMElement.vsCMElementClass){
if(classLevelElem.Name.EndsWith(ControllerSuffix)){
Controller controller = new Controller(){Name = classLevelElem.Name};
Controllers.Add(controller);
CodeClass classItem = classLevelElem as CodeClass;
controller.AuthorizationDictionary = SetUpAuthorizationDictionary();
ParseAuthorizationAttribute(classItem.Attributes, controller.AuthorizationDictionary);
//Loop over all the members of the class. If we have a public method count that as an action
foreach (CodeElement methodLevelElem in classLevelElem.Children)
{
if (methodLevelElem.Kind == vsCMElement.vsCMElementFunction){
CodeFunction method = methodLevelElem as CodeFunction;
if(method.Access == vsCMAccess.vsCMAccessPublic && method.Type.AsFullName == "System.Web.Mvc.ActionResult"){
Action action = new Action(){ Name = method.Name };
controller.Actions.Add(action);
action.AuthorizationDictionary = SetUpAuthorizationDictionary();
ParseAuthorizationAttribute(method.Attributes, action.AuthorizationDictionary);
}
}
}
}
}
}
}
}
}
This is probably a pain to read on this page. But download the sourcecode and take a look there instead.
FileCodeModel.CodeElements returns the code elements at top level in the file. In most cases that will mean using statements and namespaces. So in the outer loop we look over this items untill we have a namespace.
In the second loop we loop over all CodeElements in the namespace. This could be enums, interfaces and things like that but we are only interested in finding a class and that class should also have a name that ends with "Controller".
If we have a match we create a new Controller(not an MVC controller but the class above).
CodeClass classItem = classLevelElem as CodeClass;
controller.AuthorizationDictionary = SetUpAuthorizationDictionary();
ParseAuthorizationAttribute(classItem.Attributes, controller.AuthorizationDictionary);
In these lines we cast our CodeElement to a CodeClass so that we can get access to the Attributes property. There is also a call to a utitlity method to initiate the AuthorizationDictionary for this controller. And then we pass on the list of attributes and the dictionary to a method that handles parsing the attributes and updating the dictionary according to that. I will show this soon.
Next loop is doing the same things as we just did but for methods. We are not interested in other methods then the Actions on the controller so each method that we should proceed with has to be public and should return an ActionResult. This check verifies this.
CodeFunction method = methodLevelElem as CodeFunction;
if(method.Access == vsCMAccess.vsCMAccessPublic && method.Type.AsFullName == "System.Web.Mvc.ActionResult"){
Action action = new Action(){ Name = method.Name };
controller.Actions.Add(action);
action.AuthorizationDictionary = SetUpAuthorizationDictionary();
ParseAuthorizationAttribute(method.Attributes, action.AuthorizationDictionary);
}
Again the cast was to get access to to different properties of the method. And if we have a match we do the same thing as for the controller.
Now the method that parses the attributes. First we should go through the logic. If there are no AuthorizationAttribute all roles should have acces. Similary if we have [CustomAuthorizationAttribute(false)] everyone should have access. If the attribute exists and includes certain roles the roles in the attribute should have access and the other roles should not have access
//Iterate through the attributes and if we find an AutorizationAttribute we parse that
void ParseAuthorizationAttribute(CodeElements attributes, Dictionary<string, bool> authDictionary){
foreach(CodeElement attrib in attributes){
if(attrib.FullName == AuthAttributeName){
CodeAttribute attribute = attrib as CodeAttribute;
//I have the [Authorize(false)]. If we have that we have our authorization attribute and should break
if(attribute.Value.ToLower() == "false"){
break;
}
//Se which roles that are included in this attribute
foreach(string role in Roles){
if(attribute.Value.Contains(role)){
authDictionary[role] = true;
foreach(string impliedRole in ImplicationDictionary[role]){
authDictionary[impliedRole] = true;
}
}
}
return;
}
}
//If we get here we had no auth attribute and that means all roles have access
authDictionary[AnonymousRole] = true;
foreach(string role in Roles){
authDictionary[role] = true;
}
}
attrib.FullName is the name of the type including the namespaces. AuthAttributeName is defined is the settings file. If we have an AuthorizationAttribute and the value is false we have already found the attribute but it included nothing so we only break the loop and it will be counted as if there where no attribute at all.
If the value was not false we loop over all out Roles and see which roles that exists in the attributes value. We set each of these roles to false in the authorization dictionary and then we look if there are any roles that the member must also belong to if he is in the found role. If so we set those to true too. And the we return from the method.
If there was no AuthorizationAttribute we simply set the access for all roles to true. That nearly it. It's only the utility method to set up the dictionary left. I'll show it here but I'll leave it to you to figure that one out
//Utitlity method to init the authorization dictionary
Dictionary<string, bool> SetUpAuthorizationDictionary(){
Dictionary<string, bool> dict = Roles.ToDictionary(r => r, r => false);
dict.Add(AnonymousRole, false);
return dict;
}
That was the internal class. I must admit it seemed much more complex when I first wrote it. I should probably refactor it a bit too. I don't like methods where there are so many loops and if statements as some of these.
Creating the output
As I mentioned above the output will be a html file.
<html>
<head>
<style type="text/css">
.true{
background-color: #669966;
}
.false{
background-color: #cc6666;
}
.actionName, th{
background-color: #999999;
}
</style>
</head>
<body>
<#= VisitedItems #>
<# foreach(Controller controller in Controllers){ #>
<h1><#= controller.Name #></h1>
<table>
<tr>
<th>Action</th>
<th>
<#= AnonymousRole #>
</th>
<# foreach (string item in Roles){ #>
<th>
<#= item #>
</th>
<#}#>
</tr>
<# foreach(Action action in controller.Actions){ #>
<tr>
<td class="actionName"><#= action.Name #></td>
<td class="<#= (controller.AuthorizationDictionary[AnonymousRole] && action.AuthorizationDictionary[AnonymousRole]).ToString() #>">
<#= (controller.AuthorizationDictionary[AnonymousRole] && action.AuthorizationDictionary[AnonymousRole]).ToString() #>
</td>
<# foreach (string item in Roles){ #>
<td class="<#= (controller.AuthorizationDictionary[item] && action.AuthorizationDictionary[item]).ToString() #>">
<#= (controller.AuthorizationDictionary[item] && action.AuthorizationDictionary[item]).ToString() #>
</td>
<#}#>
</tr>
<# } #>
</table>
<# } #>
</body>
</html>
That is the whole thing. If you are used to MVC this should make perfect sense to you. If you are not the <# #> tags hold program flow statements like if, for etc etc and the <#= #> means the statment inside will be written to the template.
When the table is being created we loop over the roles and for each role we output true if both the controller and the action allows this role. If any of the stops the role false is printed.
The result
The reason this is printed as a html file is because we can open the html file in the designer in VS and look at the graphical report. Here is how it looks for one controller I have.
In this project Anonymous is not allowed to SaveTag and only Admin may use Recreate so I have already found two issues(it was not planned I promise).
Comments and suggestions are welcome. I hope you where able to follow the code and maybe got some own ideas on how to use T4. Download the source and play around.
I should mention that I have only tested this against MVC in VS 2010 beta 2. It might not work under different settings. You are free to do whatever you want with this code but it is provided as is and I do not guarantee that the results are correct.
Improvement and issues
- Inheritance does not work
- If you have roles with names like Customer and CustomerVIP it will match Customer if CustomerVIP is present even if Customer is not.
- I would like to allow the parsing function to be defined in the settingsfile. That way it could work with more projects without chaning the source.