In my previous post on modeling identity I showed how to decouple identities from accounts. Here is a more in depth look at how and when to separate all the pieces of a user's data from one another.
What is a user?
Applications typically have an object that models the user, but strictly speaking this is not really the user. The user is the human operating the user agent that interacts with the application.
The object approximating the human is a number of different things:
- An identifier the human uses to authenticate
- A collection of the user's data
- An entity that acts on behalf of the user when interacting with the system
What's important to realize is that these things are actually separate parts of what the concept of a user means the system. Even if you roll all of them into a single class you should be able to make the distinction, and be able to break them out into separate objects should the need arise.
Whenever the representation of users in your database is too limiting, it's usually a direct consequence of the different aspects of the users' representation being lumped into a single user entity.
Having a language with which to talk about a problem space is a vital tool for understanding and solving a problem. Here are some terms for this domain:
- The human using the application in meatspace
- User agent
- The software through which the user interacts with the application
- A verifiable identifier for the user (e.g. a username)
- Concrete data owned by the user (e.g. a user profile or user authored data)
- An entity with a goal in the system; something that performs operations on the data
- A set of behaviors assumed by an actor
There is significant overlap in these terms:
For the duration of a session, user agent is synonymous with the user. Humans aren't made of software, so that's as close as the application will get. But this relationship is transient, the same user could use another user agent at a later point.
For the application to treat the user agent as if it were the user it must prove an identity. Typically the identity is a username and a shared secret is used for verification, demonstrating that the meat operating the user agent is who it claims to be.
The system stores any information about the user in the user's account data.
All actions in the system are carried out by an actor. In order to perform an action this actor assumes a role.
The tricky part is that the word "user" can be used to vaguely describe every one of these terms, usually without problems. However, when there is a need for a distinction this causes confusion.
So When does "user" break down? Whenever you need to have multiple instances of these entities, associated with a single human.
Roles solve the problem of needing separate access levels for different users.
A naive solution for multiple access levels involves an access level number (or even just a super user flag) that assigns additional privileges in the system, but it's very hard to change the access controls or to add finer grained permissions. If you assign each permission to each user separately this data quickly becomes unmaintainable as well.
Role based access control is a well understood solution to this problem. Permissions are assigned to roles, and roles, in turn, are assigned to users. I won't explain RBAC, it's far too broad a topic (Google is your friend). The point I'm trying to make is that this useful abstraction relies on decoupling the notion of who the user is from what the user is allowed to do.
Taking a page from capability based systems, instead of using roles as simple data you could even use them as the actual entry point to privileged operations. If the user doesn't have access to the role then sensitive operations are not blocked, they are simply inaccessible.
This is a compelling case for making roles a first class entity in the system. Most applications won't need such flexible authorization, but for the applications that do this is a powerful abstraction.
Suppose you have 3 roles: Guest, Unprivileged, and Administrator, these roles can be associated with very fine grained permissions. User agents representing unidentified users only have access to the Guest role, but logged in users can perform operations enabled by any other roles they've been granted.
If Administrator is needs to be split up into Moderator and Superuser, most of the code remains unchanged, and the operation that updates the existing user data is simple too, so making this change go live is relatively low risk.
Supporting multiple identities is another fairly well understood problem, at least recently. Many applications are starting to support OpenID authentication, and usually existing users expect to be able to associate their existing account with a new ID.
Another example is when Yahoo bought Flickr. Flickr suddenly had two keyspaces for identities, Yahoo IDs, and Flickr usernames, both of which mapped to a single account keyspace.
If you model identities as standalone data that references a user (as opposed to being a primary or surrogate key of the user) then you can easily integrate orthogonal authentication mechanisms like RPX, SSL certificates, or just simple usernames/passwords, without polluting any code except the authentication layer. The rest of the code can stay concerned with only the user account the IDs map to.
I don't have a positive example for multiple actor support, but there are plenty of negative ones.
First and foremost is Google Apps. I've often wished I could access my work email/calendar from my personal account. Since my personal account will likely outlive my work account I don't want to use it for work or vice versa, but I still need to access the data from both. Fortunately for me email forwarding takes care of pretty much everything, but it's not a real solution.
Instead of being able to quickly switch contexts, I need to copy all the data into one place. Google knows I have access to my work email through forwarding so it lets me pretend I am sending email from there when I'm using my personal account, but there's still no segmentation.
Linode is another example. In order to manage work vs. non work machines I need to log out and log back in using a different account.
Hypothetically, the two actors that would act on my behalf in a these examples would be "Yuval the employee of Infinity Interactive" and "Yuval the unique snowflake". These are both extensions of my person, but they serve different purposes.
While an identity identifies the user from the outside, an actor identifies a user inside the system.
In a simple system there usually isn't a strong case for multiple accounts, except when consolidating two accounts (a rare occurrence). Multiple accounts are usually an artifact of not supporting multiple actors.
However, if privacy or segmentation is a needed (suppose I would like to keep my personal data hidden from my colleagues) it becomes much more relevant. Unlike the other abstractions making this distinction is the responsibility of the user, not the application, so this is a niche feature.
Websites like MyOpenID or Plaxo support contextual profiles ("personas" in MyOpenID), allowing you to use separate data in separate contexts. This masquerading ability is an important feature in the services they provide due to the sensitivity of the data, but would be useful in many other sites, especially social networks.
Multiple accounts also facilitate shared accounts. A good example is twitter accounts for projects. These accounts don't represent a single human (usually the humans operating them have separate accounts), and the limitations are pretty painful:
- The password is shared by all the collaborators
- Another user agent must be used (or the user's personal session must be terminated) to operate the shared account
Lightweight context switching would solve this. A single identity could be used to authenticate the user to the application, and access all the accounts the user should have access to.
Accounts are closely related to actors. While accounts encapsulate the data belonging to user, actors define how a user interacts with other data.
Tying it all together
So here is my "mega ultimate" model for user data. This is contained in a single transient (session bound) user agent object, with multiple delegates.
These delegates are all broken out of the single user object. What remains of this object is a thin shell that unifies all of the delegates for a single user. This object represents the actual human, and all of the different objects they have access to.
The user object is static, it serves as the hub through which all other objects are associated. The user agent object is dynamic, it defines what is known about a user for the duration of a session.
- The user agent object lives inside the session data.
- Every "login" action creates an authentication token.
- This authentication token marks an identity as verified in the user agent.
- An identity is associated with the central user object.
- A user object has access to multiple accounts.
- Each account has any number of actors through which the user interacts with data outside of the account.
- Each actor has of any number roles through which the it performs operations on that data.
The object representing the user agent needs to pick the appropriate delegate for each operation, and in most cases there is one obvious delegate to use.
Here is a diagram for the objects modeling an authenticated user:
The user agent object is responsible for picking the "active" instance of each of the multiple possible values.
In this case the user has a single user account. This account has access to two different data contexts through two actors, Work and Personal. There are only two roles in this system, Unprivileged which doesn't require authentication, and Known User which is just a default role for authenticated users. The user has registered both a username an OpenID.
The data in the dashed box has a lifetime of a single session, and represents the current set of behaviors the user is assuming, while the data on the right is persistent and represents anything the user can be.
The user signed in using the username, and is currently operating in the Work context. The Work actor's assigned roles are currently active.
Conversely, an unauthenticated user agent only has the guest role active, and makes no user data available:
A sign in action upgrades the guest user agent to an authenticated one by adding the auth token, which enables access to the full user data.
All activity that is made possible by the Unprivileged role is available to both unauthenticated and authenticated users, but actions that require a signed in user must be performed through the Known User role.
Here are some of the of the possibilities of this overengineering marvel:
- Logging into the same account with different credentials.
- Allowing an administrator to act as another user.
- Accessing different data contexts (e.g. work vs. personal).
- Shared accounts with proper segmentation of private data.
- Fine grained privacy controls.
- Fine grained, extensible permissions model.
- Account consolidation process is obvious.
- Stable internal identifiers.
These features aren't easy to implement with a single canonical user object whose ID is its primary key, and where all the data is stored as simple attributes of this objects.
Forget the "ultimate" model. It's overkill.
What's important is to merely understand the distinction between the different concepts. Most of the time you don't actually need to implement this distinction, a single user object will suffice.
Just make sure you know when and where to break things down into smaller bits, and don't do things that will prevent you from refactoring this in the future. When you need to implement one of those tricky features, avoid ad-hoc workarounds. In the long run, without having clear separation of concepts you're likely to end up with denormalized user data and inconsistent behavior.
Using duck typing (or roles or interfaces) you can simply treat your user object as an account/role/actor/identity object in the relevant parts of your code. As long as you keep the separation of the concepts clear in the usage, making the switch towards a separate first class object in the actual representation is a simple matter of refactoring.
The tradeoff of creating more abstraction layers provides, as always, flexibility at the cost of complexity. Often times we resort to inferior workarounds because they seem simpler, when in truth they are just dumbing down the problem. KISS is not a synonym for "half assed".
Using multiple distinct identities might require a bit of magic, as most user stores assume a user is an identity, instead of a user having multiple identity delegates.
By default you can still have multiple identities per user, but the user to identity relationship is-a, instead of has-a. The object that ends up in $c->user must also be the object negotiating the authentication sequence.
If you just treat each Catalyst level user object as an identity, and have the "real" user object become a delegate of that. The problem here is that of naming: $c->user->user can get pretty confusing when it really means $c->authenticated_identity->user.
Alternatively, the realm object can traverse the identity objects, and work with the "central" user object directly. In this approach $c->user is the user agent object.
A fully fledged user agent object would require custom work, as it must be tailored to your application's model of user data.
Secondly, the RBAC plugin provides a controller level predicate check for roles. The roles I've been describing in this post are more involved. Each user has role instances that are actual objects in the model. The model is manipulated by calling methods on these objects. The low level operations are still implemented by the corresponding model objects, but the controller actions invoked by the user don't access them directly, they are proxied by the roles.
Obviously the RBAC plugin can still check for the existence of these roles in order to generate simpler access violation errors earlier in the control flow, but it's no longer required to enforce the roles, enforcing is done using a safer capabilities inspired approach.
Finally, actors and accounts are purely application specific abstractions. If required, they are your responsibility to implement in the model, not the responsibility of a plugin or framework.
I'd like to thank Chris Prather for his input and for inspiring this article in the first place by patientlty listening to my ranting as he was trying to rewrite the authentication subsystem of his IRC bots.