Adding roles and authorisation to the adventure product


Nov 1, 2022

ACCEPTED

Lily Hall

#authorisation #authentication #firebase #<!-- #optional #-->

Technical Story: PT1-301 / Prototype a firebase authentication app

Context and Problem Statement

We can use firebase authentication to provide a means for users to become authenticated using email accounts. Firebase authentication also provides a means to persist state for logged in users and offers some customizable fields for personalisation such as display names and photo URLs. Once we have introduced firebase authentication we can confidently identify a logged in user, but have no means of identifying what access that provides.

The problem this ADR aims to address is specifically, how do we know which resources a given firebase user has access to?

Decision Drivers

  • Some users should only have access to a resource on an itemized basis (DMCs on itinerary)
  • Some users will have defacto access to a resource (FP on itineraries)
  • Some users will see limited information on a given resources (DMCs on an itinerary/notes)
  • Anonymous users will not see any itineraries (Anon on itineraries)

Considered Options

  • Role-based access control (RBAC)
  • Role-based access control (RBAC) with Granular Permissions Model
  • Policy-based Access Control (PBAC) with Access Control Lists (ACL)s

Decision Outcome

Chosen option: Role-based Access Control (RBAC) with ACLs

Because it meets the requirements without a huge amount of work and still conforms to our mental model of there being 'types' of user. Whilst it's less scalable than having a granular permissions model, it is a lot less work and offers the most 'bang for buck'. I believe we should look to eventually replace it with a better model such as granular permissions model as the app grows but don't see huge benefit in doing that right now. Similarly, forgoing the concept of roles is less work then even this, but has too many negative implications for code complexity and the 'role' abstraction offers a reasonable middle ground.

Positive Consequences

  • Introduces concept of a 'type' of user. (Guest, Flash Pack User, DMC)
  • Avoids components caring too much about the authorisation details.
  • Addresses the requirements without going too much further.

Negative Consequences

  • Granular permissions requires ACLs for individual access constraints.
  • Components become directly aware of roles which may be too abstract.
  • Roles may change in the future and necessitate code changes beyond what seems reasonable.

Pros and Cons of the Options

Role-based Access Control (RBAC) with ACLs

rbac-wacl

Role-based Access Control with ACLs relies on both pre-defined roles to infer access to high-level concepts within the app and seperate ACLs to implement security at the individual level for low-level data. A user can be assigned a role such as 'DMC' which might allow them to enumerate itineraries, but an ACL at the itineraries level might scope which itineraries a specific user can see beyond what's inferred from their role. Components and routes still work in assumptions about authorisation based on roles & must also be aware of ACLs. Every instance of individually securable objects must have schema changes to accommodate the ACL, which could grow to be a problem if ACLs are not implemented consistently over time.

Technical implementation of this using firebase auth could be using custom claims, which are attributes defined on user accounts. This approach would allow us to set a customer user claim against a user (eg. has role X) and then every time a new token is issued, those custom attributes can be accessed. There are limitations in that the entire payload can't be more than 1000 bytes, so this is only really suitable for role based authentication schemes, an ACL on the specific securable would still be necessary for granular permissions.

  • Good, because it cleanly matches our mental model of user 'types'.
  • Good, because it would initially be relatively trivial to implement.
  • Good, because it avoids being too declarative about permissions and only addresses our immediate requirements.

  • Bad, because it requires us to implement ACL logic and a schema for every object that requires itemised permissions model.
  • Bad, because it means implementing two different paradigms of authorisation which can be at odds with eace other. (eg. which takes precedence?)
  • Bad, because it would eventually be more work to maintain.
  • Bad, because it leads to code becoming hyper-aware of authorisation concerns.
  • Bad, because ACLs could be implemented inconsistently over time.
  • Bad, because it offers no flexibility for edge cases in permissions structures.

Role-based Access Control (RBAC) with Granular Permissions Model

RBAC-wperms

RBAC with permissions relies on pre-defined roles to infer access to permissions. Rendering logic in UI components or the API can test specifically against a given permission, which a role may or may not provide access to. A user can be assigned a role such as 'Flash Pack User' which may grant them permissions such 'app.itineraries.list.all'. This approach relies upon permissions being well defined and the code being 'permission-aware'. Roles and permissions become concepts we define in the schema, which are adjustible. A global ACL can be defined with extraneous permissions outside of roles, for example to grant a permission against a user directly, in addition to permissions inferred from roles.

As described above, roles could be assigned using the custom claims process on firebase auth. We could then define a pattern for defining permission strings that allow us to scope both to a broad feature set, to a specific operation or a specific resource that can be parsed as tree-like labels. Once we've defined the pattern, we create an appropriate permission for operations within the client which require access control. Our defined roles can have a suite of permissions specified in our DB, which any user will have given a particular role. In addition to permissions inferred from roles, users can have individual permissions assigned directly, which may grant read access to a specific resource (eg. read itinerary 5). The permission definitions, role mappings & user permission mappings would be stored in the database. We can query permissions as suggested by @igoratron using ltree functionality, assuming our convention is sufficiently sensible.

  • Good, because it still matches our mental model of user 'types'.
  • Good, because it allows us to scope access concerns specifically to their individual components/routes.
  • Good, because it offers flexibility if a role changes as roles can be adjusted in DB without redeploy.
  • Good, because it kinda answers individual level perms by having specific permissions applied against users directly.
  • Good, because it is a consistent model for both blanket permissions against a type of object and very specific permissions against a field on a specific instance of that type of object. (eg. 'app.itinerary.7.notes' vs ' app.itineraries.list.all')
  • Good, because it scales pretty well once implemented

  • Bad, because parsing & validating permissions can be difficult. (eg. 'app.itineraries.list.7')
  • Bad, because it requires quite a lot of work to define conventions for defining permissions.
  • Bad, because it might offer us more granularity and flexibility than is actually required. (Goes above and beyond)
  • Bad, because it likely results in the most work upfront.

Policy-based Access Control (PBAC) with ACLs

pbac-wacl

PBAC with ACLs omits the concept of a role within the application and relies solely upon attributes with specific ACLs for individual access. A user can be assigned an attribute such as 'has a flash pack email' which a route or component might check for and behave in a certain way, the process of checking therefore being a 'policy'. Objects which require individual access have ACLs defined as required. In pure terms, the process of authorisation becomes the burden of whichever logic would check for it, where the facts are presented and the component makes a decision, rather than the component being unaware of authorisation.

Technical imlpementation of this could be to simply infer facts from the logged in users via some sort of authorisation provider, components which require access control would access the context provided and decide what to do based on those facts. An example would be logic to check whether the provided user's email has "@flashpack.com" and a bool property (fact) like isFlashPackUser. Each component in this instance ends up checking against these facts, the specific checks a component decides to do therefore comprise a 'policy' of sorts.

  • Good, because components check directly against the attributes they care about to infer access.
  • Good, because it is the quickest to implement and can be changed later.

  • Bad, because it conflicts with our concept of there being a 'types' of a user.
  • Bad, because we end up duplicating a lot of complex authorisation logic on the API, routing and/or UI componentry.
  • Bad, because the how and why a user can access a given resource becomes difficult to track and understand.
  • Bad, because a fact/attribute might change shape and cause assumptions throughout components to be invalid.
  • Bad, because components and routes may define authorisation inconsistently.