Lightning-Fast Nested User Groups in C#
Daniel Miller - 05/Dec/2015
Daniel Miller - 05/Dec/2015
[SHOWTOGROUPS=4,20]
This article describes the C# implementation for a high-performance user/role security principal hierarchy.
This is the first in a series of follow-ups to a previous article titled "Lightning-Fast Access Control Lists in C#". It described a solution for building and querying an access control list as part of an application security model. The solution was fast and lightweight, and it had no dependencies upon a specific database schema.
Many readers requested a follow-up article to propose a database schema suitable for supporting that solution. Before I propose a schema, there is some additional groundwork that needs be done. This will help to guarantee the schema does not impede a high-performance solution.
As most of you probably know, application security is a difficult problem, and solutions (both good and bad) often bear a striking resemblance to black magic. Software security models have the potential for enormously complex requirements, with priorities that often conflict with one another. For example, flexibility, robustness, fault-tolerance, performance, ease of development, ease of administration, ease of use... any (and all) of these can be important aspects.
A good security model has many different moving parts, and a solid access control list is just one of the primary parts. The purpose of this article is to describe a solution for another primary part that is closely related: managing a hierarchy of security principals.
Paired with an Access Control List (ACL), a user/role hierarchy (or Principal Model) needs to satisfy four basic functional requirements:
In keeping with the theme of the previous article, here, we'll aim for an elegant solution that is also blisteringly fast.
Background
The code makes some foundational assumptions, which should be described before we get into the details.
Most important, it is assumed that every permission is an object with three properties (Principal, Operation, Resource) so it can be read as a declarative statement having this form:
You can find definitions and examples for these three properties in the previous article.
The code in this article is focused exclusively on a model to manage and query users assigned to nested groups. The object we're concerned with here is a security Principal Model.
A Principal is understood to represent the identity of a specific user or group of users. Here, we will use the term User to identify a specific individual person, and we will use the term Role to identify a specific group of users. Both Users and Roles are Principals, so a skeletal class model might look like this:
Fair warning: I won't be doing that here.
Instead, the focus here is on the overall data structure and algorithm design. At this stage, I am not concerned with Principal, User, or Role class definitions. Those can come later. Instead, I want to keep the discussion (and the initial code) as elementary as possible.
The problem we are trying to solve is difficult enough on its own, and at this early stage, we don't want to get lost in secondary details. Therefore, the code here assumes the following deliberate simplifications:
Path notation is a useful shorthand to identify nested roles, in the same way that path notation is a useful way to reference files in a file system or pages on a web site.
For Example...
The path "Springfield Residents/Simpson Family Members/Parents" implies the existence of three roles: Springfield Residents, Simpson Family Members, and Parents. It also defines relationships: Simpson Family Members are a subset of Springfield Residents, and Parents are a subset of Simpson Family Residents.
In this example, it is important to note the assumptions I have described disallow the existence of a role "Springfield Residents/Flanders Family Members/Parents", because the role named "Parents" cannot appear in multiple locations within the hierarchy. Roles names must be unique, and roles can have only one parent.
This constraint exists solely for the sake of simplicity, In a production system, you will most likely use something other than the name of a role to uniquely identify it, and in that case, you might choose to permit multiple roles with the same name (in different locations) differentiated by some other identifier. To illustrate:
What are Nested Roles?
Nested roles define subset (or containment) relationships in this model. If A contains B and B contains C, then A indirectly contains C. This can be visualized using a Venn diagram or a directed graph:
One More Constraint
There is one final constraint, again for the sake of simplicity: Role names should not include commas, slashes, colons, hyphens or bars. It's up to you if you want to enforce this. I like to troubleshoot role hierarchies by dumping them to plain text. I also like to test and debug the code with plain text input. These tasks are easier with some basic role-naming conventions to facilitate plain text.
For example, the string value "[email protected]:Animals/Monkeys,Adventurers" can be used to declare the existence of a user named Curious George, and three roles named Animals, Monkeys, and Adventurers, where Monkeys are a subset of Animals, and the role of Adventurer is not related to Animals or Monkeys.
As another example, troubleshooting both code and data is easier when you can display a group hierarchy in plain text like this:
That should be enough preamble... let's jump into the code...
Into the Code
What's the Goal?
Performance is still the highest priority with this solution. A close second is the requirement to support minimalist calls from client code. The API should be clean and compact. For example, when a principal model and an access control list are paired with one another, a permission check should look something like this:
Here is another example, in which we want to obtain a collection of all the roles to which Alice is assigned:
This should include Alice herself (obviously), and the roles to which she is assigned directly, and the roles to which she is assigned indirectly - which could then be used to query an ACL:
Creating and loading a principal model has to be simple as well, so less-experienced developers and administrators can re-use it. Ideally, the calls should look something like this:
To maintain a loose coupling with the rest of the application (front-end and back-end), the code needs to include methods for loading a principal model from a DataTable or a CSV:
In the end, I settled on two interfaces: one that is responsible for storing the data model, and one that is responsible for navigating and interrogating model. Towards the end of the article, I will explain the reason for this separation.
Building the Model
GroupTree
First, the principal model needs to store the group hierarchy. This is the full set of roles, outside the context of any particular user's membership. This is our "master list" of groups and the parent/child relationships connecting them. For the sake of example, suppose we have the following roles defined:
[/SHOWTOGROUPS]
This article describes the C# implementation for a high-performance user/role security principal hierarchy.
- .../KB/database/1060991/Source.zip
This is the first in a series of follow-ups to a previous article titled "Lightning-Fast Access Control Lists in C#". It described a solution for building and querying an access control list as part of an application security model. The solution was fast and lightweight, and it had no dependencies upon a specific database schema.
Many readers requested a follow-up article to propose a database schema suitable for supporting that solution. Before I propose a schema, there is some additional groundwork that needs be done. This will help to guarantee the schema does not impede a high-performance solution.
As most of you probably know, application security is a difficult problem, and solutions (both good and bad) often bear a striking resemblance to black magic. Software security models have the potential for enormously complex requirements, with priorities that often conflict with one another. For example, flexibility, robustness, fault-tolerance, performance, ease of development, ease of administration, ease of use... any (and all) of these can be important aspects.
A good security model has many different moving parts, and a solid access control list is just one of the primary parts. The purpose of this article is to describe a solution for another primary part that is closely related: managing a hierarchy of security principals.
Paired with an Access Control List (ACL), a user/role hierarchy (or Principal Model) needs to satisfy four basic functional requirements:
- A permission is assigned to a principal.
- A principal is an individual user or a group of users.
- A user may be assigned to multiple groups.
- Groups may be nested into a hierarchy with any number of levels.
In keeping with the theme of the previous article, here, we'll aim for an elegant solution that is also blisteringly fast.
Background
The code makes some foundational assumptions, which should be described before we get into the details.
Most important, it is assumed that every permission is an object with three properties (Principal, Operation, Resource) so it can be read as a declarative statement having this form:
Код:
[Principal] is granted permission to [Operation] on [Resource]
You can find definitions and examples for these three properties in the previous article.
The code in this article is focused exclusively on a model to manage and query users assigned to nested groups. The object we're concerned with here is a security Principal Model.
A Principal is understood to represent the identity of a specific user or group of users. Here, we will use the term User to identify a specific individual person, and we will use the term Role to identify a specific group of users. Both Users and Roles are Principals, so a skeletal class model might look like this:
Код:
class Principal { };
class User : Principal { };
class Role : Principal { };
Fair warning: I won't be doing that here.
Instead, the focus here is on the overall data structure and algorithm design. At this stage, I am not concerned with Principal, User, or Role class definitions. Those can come later. Instead, I want to keep the discussion (and the initial code) as elementary as possible.
The problem we are trying to solve is difficult enough on its own, and at this early stage, we don't want to get lost in secondary details. Therefore, the code here assumes the following deliberate simplifications:
- A user is uniquely identified by a string value (i.e., email address).
- A role is uniquely identified by a string value (i.e. name). No two roles share the same name, regardless of their positions in the hierarchy.
- A role may have no parent role, or it may have one parent role. It cannot have multiple parents.
- A role may have no users, or it may have multiple users.
- A user may be assigned to any number of roles. (Users are not leaf-nodes in a tree. This is an important distinction.)
Path notation is a useful shorthand to identify nested roles, in the same way that path notation is a useful way to reference files in a file system or pages on a web site.
For Example...
The path "Springfield Residents/Simpson Family Members/Parents" implies the existence of three roles: Springfield Residents, Simpson Family Members, and Parents. It also defines relationships: Simpson Family Members are a subset of Springfield Residents, and Parents are a subset of Simpson Family Residents.
In this example, it is important to note the assumptions I have described disallow the existence of a role "Springfield Residents/Flanders Family Members/Parents", because the role named "Parents" cannot appear in multiple locations within the hierarchy. Roles names must be unique, and roles can have only one parent.
This constraint exists solely for the sake of simplicity, In a production system, you will most likely use something other than the name of a role to uniquely identify it, and in that case, you might choose to permit multiple roles with the same name (in different locations) differentiated by some other identifier. To illustrate:
What are Nested Roles?
Nested roles define subset (or containment) relationships in this model. If A contains B and B contains C, then A indirectly contains C. This can be visualized using a Venn diagram or a directed graph:
One More Constraint
There is one final constraint, again for the sake of simplicity: Role names should not include commas, slashes, colons, hyphens or bars. It's up to you if you want to enforce this. I like to troubleshoot role hierarchies by dumping them to plain text. I also like to test and debug the code with plain text input. These tasks are easier with some basic role-naming conventions to facilitate plain text.
For example, the string value "[email protected]:Animals/Monkeys,Adventurers" can be used to declare the existence of a user named Curious George, and three roles named Animals, Monkeys, and Adventurers, where Monkeys are a subset of Animals, and the role of Adventurer is not related to Animals or Monkeys.
As another example, troubleshooting both code and data is easier when you can display a group hierarchy in plain text like this:
Код:
|-Root
| |-Alpha
| |-Beta
| \-Gamma
| |-Delta
| | \-Epsilon
| |-Zeta
| \-Eta
\-Theta
\-Iota
|-Kappa
\-Lambda
That should be enough preamble... let's jump into the code...
Into the Code
What's the Goal?
Performance is still the highest priority with this solution. A close second is the requirement to support minimalist calls from client code. The API should be clean and compact. For example, when a principal model and an access control list are paired with one another, a permission check should look something like this:
Код:
if (principals.IsUserInRole("Alice","Explorers"))
{
// Proceed if Alice is assigned to the Explorers role - or to any role that is a superset of
// (i.e. ancestor to) the Explorers role.
}
Here is another example, in which we want to obtain a collection of all the roles to which Alice is assigned:
Код:
var roles = principals.GetUserRoles("Alice");
This should include Alice herself (obviously), and the roles to which she is assigned directly, and the roles to which she is assigned indirectly - which could then be used to query an ACL:
Код:
if ( acl.IsGranted( principals.GetUserRoles("Alice"), "Drink", "Mysterious Potion" ) )
{
// Proceed if Alice is granted permission to drink the mysterious potion. This permission
// might be granted directly to Alice, or it might be granted to a role that she is directly
// assigned to, or it might be granted to a role that contains one of the role to which she
// is assigned.
}
Creating and loading a principal model has to be simple as well, so less-experienced developers and administrators can re-use it. Ideally, the calls should look something like this:
Код:
// Invoke principals::AddRole( child, parent )
// All Harmless Lunatics are Mad Tea Party Attendees. Not not all attendees are lunatics.
principals.AddRole( "Harmless Lunatics", "Mad Tea Party Attendees" );
To maintain a loose coupling with the rest of the application (front-end and back-end), the code needs to include methods for loading a principal model from a DataTable or a CSV:
Код:
model.LoadRoles( "C:\Files\Roles.csv" )
model.LoadUsers( "C:\Files\Users.csv" )
In the end, I settled on two interfaces: one that is responsible for storing the data model, and one that is responsible for navigating and interrogating model. Towards the end of the article, I will explain the reason for this separation.
Код:
public interface IPrincipalModel
{
GroupTree Groups { get; }
MembershipList Roles { get; }
MembershipList Users { get; }
void AddGroup(string name);
void AddGroup(string child, string parent);
void AssignUserToRole(string user, string role);
}
public interface IPrincipalFinder
{
MembershipResultSet FindAllRoles(string user, MembershipList users, IGroupTree groups);
}
Building the Model
GroupTree
First, the principal model needs to store the group hierarchy. This is the full set of roles, outside the context of any particular user's membership. This is our "master list" of groups and the parent/child relationships connecting them. For the sake of example, suppose we have the following roles defined:
- Animals
- Humans/Explorers
- Harmless Lunatics/Mad Tea Party Attendees
Код:
# Animals
# Humans
## Explorers
# Harmless Lunatics
## Mad Tea Party Attendees
[/SHOWTOGROUPS]
Последнее редактирование: