Authorization – TDD Patterns

Authorization is the process of deciding whether to allow a user can access or use some resource. (Don’t confuse it with authentication, the process of deciding whether a user is who they claim to be.)

TDD and Authorization

From a TDD perspective, there are two core tests: that an authorized user can do something, and that an unauthorized user cannot.

func test_authorizedUserMayAccess() {
    let auth = authorization-subsystem
    let user = authorized-user
    let resource = some-resource
    assertTrue(auth.permits(user, resource))
}

func test_unauthorizedUserMayNotAccess() {
    let auth = authorization-subsystem
    let user = non-authorized-user
    let resource = some-resource
    assertFalse(auth.permits(user, resource))
}

You have a lot of room for design decisions in this: What objects do you consult about authorization? (An authorization service, or the resources?) How does the authorization work? Is there more than one type of access or permission (e.g., read vs write)?

Separate the testing of the authorization mechanism (as above) from the testing of particular resources. Otherwise, you’ll end up with a bunch of near-identical tests (and risk duplication of implementation). If you make the mechanisms transparent enough, you may be able to test the latter by inspection.

This makes the problem more of an application-level concern: “Have you configured the authorizations correctly?” rather than “Have you implemented authorization correctly?”

Tools

Before you go too wild implementing an authorization system, assess whether existing tools and libraries will support your needs.

The Simplest Systems – No Authorization

The simplest systems have no authorization – any user can do anything with no checking. There may be authorization at a different level: my word processor doesn’t make me sign in, but it runs in the context of an account where I do sign in.

Admin / Superuser

The next level of complexity is to designate someone as the administrator or superuser, who has all capabilities, vs. a regular user with limited capabilities.

The easiest way to start is to pass a user object around, and decide at each access point whether the user is authorized. But pay attention to what’s growing!:

  • the number of users
  • the number of superusers
  • new types of users (e.g., guest, unknown, or supervisor)
  • the number of resources requiring restricted access
  • the number of decision points

As these grow, the simple model gets creaky, and you need to watch out carefully for duplication.

Role-Based Authorization

You often find that patterns emerge, where different roles have different access needs, and the needs need not be in a strict hierarchy. By grouping users into roles, and defining access by role, you can bring order to it and treat all people with a role uniformly.

You may be able to create a matrix: role x resource → permission, or perhaps: role x resource x operation → permission. This may let you centralize the mechanism, which lets you more easily configure the access per role.

This also lets you split the access decision:

  1. Does the user have the intended role? -and-
  2. Does the role allow the intended access?

There are many conceptual approaches to authorization; be aware of what happens when your needs exceed the capabilities of your current approach.

Conclusion

Authorization checks whether users can use resources; you need to test both positively (authorized users can access) and negatively (unauthorized users cannot). Once the mechanism works, you want to check the actual authorization at the application level: verify configuration rather than independent tests for each role and resource.

Resources

Role-based access control“, Wikipedia. Retrieved 2021-05-18.