The conception, birth, and first steps of an application named Charlie

Subscribe: Atom or RSS

Charlie’s Approach to Authorization

by Alister Jones (SomeNewKid)

After putting in place Charlie’s Web System, Plugin System, and Entity System, and then building the core business objects on top of the Entity System, I was a little unsure of what to do next. I desperately wanted to get Charlie looking good, since design and usability is what I find most interesting. But I knew that I needed to stay focused on the deeper, more fundamental aspects of Charlie. With that in mind I reviewed the functional specification that the aardvark had advised me to write. I came to the section that specified Charlie’s major features, which read as follows:

The first major feature is that Charlie will be simple.
The second major feature is that Charlie will be secure.
The third major feature is that Charlie will be globalized.
The final major feature is that Charlie will be configurable.

The first feature, that Charlie would be simple, was an ongoing challenge that was so far being met. The second feature, that Charlie would be secure, was what I decided to tackle next.

I started by reviewing the OWASP Guide to Building Secure Web Applications, and then Microsoft’s Designing Application-Managed Authorization. With a heavy sigh, I then worked through Building Secure Microsoft ASP.NET Applications. Fortunately, only fractions of the book were relevant to Charlie, so it did not take as long to get through its 560 pages as I had first feared.

Like the feature of being simple, the feature of being secure will be an ongoing challenge in the design of Charlie.

Still, there were a few easy first steps that I was able to take. First, I encrypted all of the passwords that were being saved to the database. Security 101 there. Second, I learned how to add a salt to those encrypted values. Fortunately, Microsoft provides easy to follow instructions on how to do this in its Building Secure Microsoft ASP.NET Applications document. (That’s a link to the free online version, if you want to save yourself the $115 Australian dollars I paid years ago to get this in book form, not knowing that it was also available for free.) I then created a custom IPrincipal implementation, yet again referring to the same hefty book. This is all standard stuff in a secure ASP.NET application, so I won’t talk about it.

What I will talk about is where my approach to security varied from that recommended by Microsoft. In my last weblog entry I noted how my stubborn personality is playing its part in the design of Charlie. This was certainly why I had the audacity to take a different approach to authorization than that presented in virtually all Microsoft documentation and sample projects.

The common approach to authorization that I think is foolish is the use of pre-determined roles. Here is an example from the Personal Site Starter Kit:

bool filter = 
    !(HttpContext.Current.User.IsInRole("Friends") ||
      HttpContext.Current.User.IsInRole("Administrators"));

This is a very common approach that is shown throughout Microsoft’s documentation and in its sample projects. Yet, it seems foolish to me. For one thing, it is using hard-coded role names. What if it is later decided that Friends is an unsuitable role name, and we need to change it? In almost every other area of programming, we are wisely advised to avoid hard-coding string values. Yet, here we are doing exactly that, and in the important area of security. Another problem with this technique is that not only are the names of the authorized roles set in stone, so too is the number of authorized roles. In this case, it is two roles. What happens if we later add another role that should share authorization to perform this particular task?

I decided that nowhere was I going to hard-code either the names or the number of authorized roles. Instead, I decided that every entity (be it am internet domain, or a web page, or a section of a page, or an article, or the comment of an article) would have a collection of CreateRoles, RetrieveRoles, UpdateRoles, and DeleteRoles.

Because every entity had this collection of roles, I can perform simple, meaningful security checks. Here is the code that checks whether the current user has permission to view the requested page:

if (WebContext.Current.User.IsInRoles(this.Document.RetrieveRoles) == false)
{
    throw new HttpException(403, "Unauthorized");
}

This approach performs the same kind of authorization check as that shown above in the Microsoft example, but without hard-coding either the name or the number of roles. Later in the same page class, the code checks whether the user can edit the contents of the page:

if (WebContext.Current.User.IsInRoles(this.Document.UpdateRoles))
{
    // code to show the Edit button
}

After the user’s authorization to view the entire page is checked, more fine-grained checks are made for each and every Container object (which equates roughly to a Placeholder control that presents page content):

foreach (Container container in this.Document.Containers)
{
    if (WebContext.Current.User.IsInRoles(container.RetrieveRoles))
    {
        // code to place the container on the page
    }
}

Not only does this approach make more sense to me, it also contributes to Charlie's fourth main feature, which is to be configurable. The approach of giving every entity a collection of CreateRoles, RetrieveRoles, UpdateRoles, and DeleteRoles means that each website owner can tailor the roles to his or her requirements.

More interesting, perhaps, is that the above is a back-up security check. The main security check is down in the base EntityManager class. Here is the current code for the LoadEntity method of the EntityManager class:

protected Entity LoadEntity(Entity entity, Criteria criteria)
{
    // Try to get the entity from cache. If not,
    // try to get the entity from the database.
    String cacheKey = criteria.ToString();
    Entity cached = this.GetEntityFromCache(cacheKey);
    if (cached != null)
    {
        entity = cached;
    }
    else
    {
        entity = this.Mapper.Retrieve(entity, criteria);
        criteria.Id = entity.Id;
        entity.MarkAfterLoad();
        this.AddEntityToCache(cacheKey, entity);
    }
    
    // Check that the entity's roles have been loaded.
    // If not, try to load them.
    if (entity != null)
    {
        if (entity.RolesLoaded == false)
        {
            entity = this.Mapper.AddRolesToEntity(entity, criteria);
        }
        if (entity.RolesLoaded == false)
        {
            throw new SecurityException(
                "Roles were not loaded for this entity.");
        }
    }
    
    // Check user's permission to retrieve this entity.
    if (entity != null && 
        WebContext.Current != null)
    {
        Principal principal = WebContext.Current.User;
        if (principal == null ||
            principal.IsInRoles(entity.RetrieveRoles) == false)
        {
            if ((entity is Role) == false)
            {
                entity = null;
            }
        }
    }
    
    return entity;
}

If you follow the code through, you will see that it first grabs the requested entity. Then, it will check whether the CreateRoles, RetrieveRoles, UpdateRoles, and DeleteRoles have been loaded. If not, it will try to load them. If this fails, a security exception will be thrown. Now that we have an entity with its roles loaded, the EntityManager checks whether the current user’s roles are within the entity’s retrieve roles. If not, the entity is set to null, so that it will not be returned to the calling class.

Because this is deep down in the base EntityManager class, this security check is performed for every single entity involved in processing a user’s page request. If the consuming class performs its own IsInRoles test, as in the 403 page check above, this is merely a double-check.

A good question is, why not simply perform a roles check before retrieving the entity from the cache or from the database? The reason that the entity must first be retrieved is that the security system is very fine-grained. Each and every entity can define its own CRUD roles, or it can fall back to default roles. It is necessary then to retrieve the entity to check whether the entity has its own defined roles. I’ll talk more about this in the next weblog entry.

So that was the first main instance where I stubbornly refused to follow conventional wisdom, and created instead an authorization system that I felt to be more logical, more secure, and more flexible. (Stubborn and opinionated, am I not?)

by Alister Jones | Next up: A Beautiful Cascade

0 comments

----