Abstraction Levels and Authorization
Working at too low of an abstraction level is a common source of duplication and technical debt. A very common culprit in this area is authorization.
Sponsor - DevIQ
Thanks to DevIQ for sponsoring this episode! Check out their list of available courses and how-to videos.
Show Notes / Transcript
Let's take a quick break from the more commonplace design patterns and talk a little bit about abstraction levels and how they impact duplication and technical debt in our software designs. You can think of high levels of abstraction as being at the level of the real world concept your software is modeling. For an ecommerce application, it might be buying something, or adding an item to a cart. The whole notion of a cart or basket is a metaphor explicitly pulled into ecommerce applications from the real world. There's certainly no literal cart or basket involved in most online shopping experiences. Low levels of abstraction refer to implementation details used by the actual software (and sometimes hardware) used by the system. When developing software, it's a good design decision to encapsulate low levels of abstraction separately from higher levels, and thus to avoid mixing abstraction layers more than necessary within any given module. The more you mix abstraction levels, the more you add tight coupling to your design, making it harder to change in response to future requirements.
A common requirement in many applications is authorization. Authorization is often conflated with the other auth word, authentication. Authentication is the process of determining who the user is. Authorization is the process of determining whether a particular user should be allowed to perform a certain operation. It can include default rules for anonymous users, but aside from that authorization only makes sense once authentication has taken place and you know who the user is.
Authorization rules can take many forms, and can be as granular as specifying that a specific user has access to a specific resource. However, most applications that need authorization will leverage roles or claims to specify how groups of users should or should not have access to certain sets of resources. This makes it much easier to manage collections of users and collections of resources, since otherwise a huge number of specific user-to-resource rights would need to be maintained. However, even this is often prone to duplication that results from too low of an abstraction level.
It's common in platforms like .NET to use roles as at least one part of determining authorization, and to use conditional logic like
if (user.IsInRole("Admins")) any time authorization logic needs to be performed. In any non-trivial system that uses this pattern, you'll probably find quite a few lines of code that match this expression, meaning there is a great deal of duplication. Duplication isn't always bad, but in this case the implementation detail of performing a role check as one part of checking whether a user is authorized to access a particular resource is adding to the system's technical debt.
Frequently, authorization rules will change over time. What happens when a new role or set of claims is created that should have access to some resources? Every one of the
if statements related to access to that resource will need to be modified. What happens if you switch from using roles to claims? Every
if statement will need to be modified. Of course, when these modifications take place, there's also the chance that bugs will be introduced, and these will manifest in many cases as security breaches.
There are many patterns you can use to improve this design. You can use a more declarative approach, such that adding certain attributes will protect certain endpoints in your application. This can remove conditional logic and can eliminate some duplication since these attributes can be applied at class or even base class levels. However, if your authorization logic is more complex than simple role membership, it may not be sufficient or at the least you'll need to write your own attributes or filters.
Another approach I've found useful is to create a first-class abstraction that describes whether a given user should have access to a given resource. I typically call such types privileges but you can refer to them as AuthorizationPolicies or whatever makes sense to you and your team if you prefer. A privilege takes in who the user is and what resource they're attempting to work with, and specifies what operations that user can perform on that resource. Since it's a design pattern, not a specific solution, how you implement the details is up to you. A common approach is to implement methods for things
CanModify. You can also further modify it to work for collections or types of resources, so that for example if the user should be able to manage product definitions, you could check whether the user has rights to type
Product and if so, include a link to manage products in the application's menu.
I have an article describing how to use privileges instead of repetitive role checks that I'll link to in the show notes that may help you get started if you're interested in learning more.
Would your team or application benefit from an application assessment, highlighting potential problem areas and identifying a path toward better maintainability? Contact me at ardalis.com and let's see how I can help.