Authorization

First, let’s get this out of the way - authorization is terrifying. It’s considered a high risk element of any system as it can result in unwanted data exposure if there are any issues. No one wants that to happen.

But it doesn’t have to be terrifying as long as you take time to understand what the requirements are, how the system works and what the relations all are.

There are three components of authorization:

  1. Who you are

  2. What permissions do you have

  3. What data you have access to

Identity 🧑

The concept of identity anywhere is actually complicated if you stop to think about it.

Who are you?

You are you, but you have a government passport number, a provincial drivers license, a provincial health card, a Netflix profile, a bank debit card, a credit card, etc.

There’s no one universal identity card - the most common form of identity is your drivers license which is really weird if you think about it long enough because what does driving a car have to do with anything you use that for, and no one wants to be walking around with a passport or exposing it to anyone that doesn’t need it, but there’s no actual link between most of these identity documents.

It’s worth noting that not having one universal identity card isn’t actually a bad thing, but that’s a whole Confluence space in itself.

Accounts

On the internet, a lot of your identity is usually based on your email address, but you can also have multiple email addresses and even throwaway email addresses. You may have a personal email and a work email, and if you’re volunteering for a sports organization, they might give you a generic position email address. You’re already up to three identities just with emails. Also when you create your email address, no one is typically verifying it against any sort of government identity, so is an email really your identity?

But just like you have a drivers license and a health card both issued by the province, each online service you register for on the internet creates their own identity (aka account) that’s associated with your email address typically (or some cases, a phone number, like Signal does).

This is also the case in Play, and while Play has an account with an email addresses, authorization isn’t based on it and the account is primarily for authentication.

Instead, authorization is based on Participants, and Accounts provide access to Participants through AccountIdentities.

AccountIdentity

An Account can be linked to multiple Participants and vice versa, and this many-to-many relationship is managed through AccountIdentitys. When the app accesses the API, an X-Identity header is sent with each request so the the system knows which Participant the request is identifying as.

Typically most people have a single AccountIdentity for their account, but a parent’s account could have multiple identities that include their children, or someone could have multiple identities across tenants if they play in multiple sports, as they have a Participant per tenant.

Participants

So accounts are just credentials, a username and password that grants you access to an account, which has many identities, which ultimately lead to Participants.

Why participants? This is actually a somewhat unique property of Spordle because our system is really just one big ERP full of people, so participants actually make a much better primary identity to base authorization on than accounts in our case.

Typically Spordle ID has the participants for players, team staff and officials, which covers a significant portion of Play’s users. All of these types of participants are able act in the system. In fact, we can even automate authorization (details further down) for these users since we know which team they belong to or what kind of qualification they have. So you could almost say people already have “accounts” in the system, but haven’t setup credentials yet.

Of course, that’s not everyone that uses Play. We still need to account for administrative staff like registrars that manage the teams, league staff that manage the schedules and assigners that make sure games have officials.

In a lot of cases, most of these administrative staff might already have a participant in our system because they were a coach in a previous/current season or they’re qualified officials. This leaves a smaller chunk of people that have no prior experience, so someone can just manually create a participant for them. Participants in Play aren’t required to be linked to Spordle ID.

So that’s why everything is based on participants, because it’s already a very convenient user directory that we already have. The person in question just need to be able to validate they are the participant in question upon registration to create an account linked to the participant, and another admin can preconfigure their roles ahead of time.

Applications

There’s also another type of identity for applications like websites and third parties to enabled public access or private third-party machine-to-machine access.

This type of identity is defined as an OAuthClientApplication, which is an equivalent principal to Participants and Accounts used for permissions in Play.

Implementation

To summarize, an Account is just a username and password, that has one or many AccountIdentities that link the Account to a Participant, which is also just a first name, last name and an registry number. If you’re machine, you have an OAuthClientApplication.

For authenticating with the API, an OAuthAccessToken is created which is the token that’s communicated with each request to authenticate the request with the Account, and an X-Identity header is provided to know which Participant the request is acting as. As a security measure, the identity header must be an identity that the account is linked to, otherwise it resets back to the default identity.

The app can also make a request to /accounts/current to get the current account information, which permissions are currently effective (based on the active identity), what other identities your account has, and some additional tenant info (season, notices, flag) that aren’t relevant here.

 

Privileges 🗝️

So now that we know who you are, what do you do around here anyway?

Scopes

As a user, you’re used to thinking about roles, but roles are actually made up of scopes, which define access to individual components, like do you have rights to manage a schedule or edit a team roster?

These scopes happen to be common across different roles, so the system defines scopes and then we can compose roles out of it. Defining scopes requires a bit of a balance, you don’t want too many that you’re constantly creating new scopes, but you also don’t want too few that you can’t create granular enough roles. Play isn’t necessarily always perfect at achieving this balance.

Roles

Roles are a collection of scopes, but they also have a couple of additional properties like context and target type, which is where authorization becomes more tailored to Play.

There are three contexts in Play:

  • League contexts are based on scheduling

  • Officiating contexts are based on assigning

  • Administrative contexts refer to everything including the above

Scopes define one third of a role, contexts define another third, while target types define the last third, which limit a role to be applicable to an Office, Schedule, Team, Game, or Participant.

A role just describes a job function, like an Office Scheduler (which would be a league context) or a Team Assigner (which would be an officiating context). These example roles don’t actually exist today, but they’re possible to create.

You can mix and match these three pieces to create a variety of roles. It is also feasible for users to create roles, but that’s not exposed today. One missing piece to do that is to define which office a role belongs to, as they’re currently all system defined and one region isn’t going to want to see another region’s roles.

Note: current roles are described in the reference documentation.

Permissions

You know who you are and what you do, but that’s not enough! Who you work for?

A permission is composed of a participant (principal), role, and a target (office, schedule, team, etc). These three pieces define one part of the authorization puzzle. You are a league manager for XYZ league.

Play has some additional possible restrictions on permissions that can be set, so while you’re given access to XYZ league, you can be restricted to certain categories or groups within that league. There are additional options to disable reports and scorekeeping which automatically exclude those scopes from the role when applied.

Permissions can also expire. Once that happens, they’re effectively soft-deleted to the authorization system, but they’re not actually soft-deleted so another admin can decide to extend the expiry for another season.

Implementation

Picking up from the last implementation section, a Participant has AccountPermissions, which have a principal (the Participant), an AccountRole (which has a targetType, context and scopes), and a target (an office, schedule, team, etc as defined by the role’s targetType).

At the start of each request, a request authorization context is defined that includes your identity and your effective permissions. To keep query filtering efficient, the system reduces each defined permission into minimal combined permissions. So users create permissions for each office, and if the user has the same role for 3 different offices, there’s one effective permission.

Office Hierarchy

The other thing to keep in mind is the nature of office hierarchy, so if a role is created in a region, it inherits access to the district, associations, leagues, etc under it. So one permission at the region could turn into several association permissions automatically.

League, Tournament, Cup and Zone offices are also special because they have OfficeMembers which extend the hierarchy so there’s additional permission expansion here as well. Members of these offices also get an upward inherited role in the league/etc with filtered scopes. This means role in a league automatically gains access to the associations.

Automatic Roles

Lastly, there are automatic system roles based on roster members (for player and coach roles) and qualifications (for officials and scorekeepers). There’s also an automatic participant role to grant you access to manage your own profile and an automatic game volunteer role for the scorekeeper login to grant access for one specific game.

Access 🔐

Now we get into the fun part - of all the data we have, what do you get access to with your permissions? There’s actually two parts to this, can you do something and what specific data you can do it on.

Model access

This is where we try to piggyback on as much of Loopback’s authorization system as possible.

Out of the box, it doesn’t quite work perfectly because of all the nuances already described. We’ve created custom role resolvers which we use to define how the user’s defined permissions have access, and a Scope mixin to make defining authorization on models a little nicer.

The custom role resolver iterates through all the (effective) permissions in the request context to ensure you have the scope, and then depending on the role’s context type, it picks the appropriate model method to figure out if you should have access for the given model - this is where hasOfficeAccess, hasScheduleAccess, hasTeamAccess, etc that are defined on every model kick in.

These functions vary for each model as the relation for each back to the permissions target can be very different. If you’re not requesting a specific entity, the resolver just checks if you have the appropriate scope, otherwise it checks if you have access to that specific entity based on it’s relations.

As a basic example, a Team has a direct office relation, so that’s an simple check for Office permission (hasOfficeAccess), and team permissions are obvious (hasTeamAccess), while a schedule role is via the ScheduleTeam relation to a Schedule (hasScheduleAccess).

Game is a more complex example, as the role context will determine whether access is based on the game’s schedule office (League context) or the game’s assigning settings office (Officiating context) for office roles. The League context can also be based on the home or away team as well, and the Administrative context is all four of these relations. The scope will also impact which relations are checked as will if the role was inherited via the office hierarchy.

There’s also a special case to keep in mind where as long as you have an applicable scope, you have access as the creator of that entity. This is based on the account.

Data access

Supposing you get past the first layer to determine whether or not you have access, you might be making a request for a list of entities, so the API needs to make a call to the database. Before that happens, a filter is applied on top of your request filter based on all of your permissions.

Unlike in model access, if we’re requesting a list of entities, we can’t just check scopes, and we also don’t have a specific entity to check against, so the filter is build so you can only access what you have rights to, and if you try to request something you don’t have access to, if it’s not in the middle of the Venn diagram of what you requested and what you have access to, it’s not returned in the response.

This is how you get 404s even though the data exists. If you’re wondering why not a 403, in the context of the user, it simply doesn’t exist. This approach has benefits for privacy as well as you’re not indirectly divulging secrets by saying they exist but they don’t have access to them.

The scope mixin is also what applies the filters, based on the type of your role (office, schedule, team, game, participant) and also further filtered by a tenant filter to ensure that we enforce a strong firewall between tenants.

Escape hatch

As a league manager, you’re responsible for a small set of teams and people, so that’s all your have access to. You don’t need any sort of access to other teams or people… until you do. A team from another region that you don’t normally have access to is coming to play in one of your league’s tournaments. Now what?

When querying the API, there’s a scope parameter than can be specified to allow access to a broader scope of data, which means less restrictive authorization filtering. By default, an Authorized scope is used, but this can be expanded to the Tenant or System, or restricted to Reports. There’s also a Parents scope which acts as a subset between Tenant and Authorized.

Each model defines which scopes are allowed, so this escape hatch doesn’t work everywhere (e.g., addresses only allow Authorized access). Some models only have the System scope defined, so Authorized filtering doesn’t kick in at all (e.g., roles), or others only have Tenant defined (e.g., assign systems, or categories). If a higher scope that isn’t defined is attempted to be used, the system automatically resets back to Authorized.

The app takes advantage of this escape hatch in a few different ways.

  • Browsing the main list of games/teams/etc or cards typically use the Authorized scope.

  • When resolving many relations (e.g., via a TeamField), the Tenant scope is used (which the API might bounce back to Authorized based on the model).

  • In the tournament example, the main teams page is scoped by Authorized, but the add team dialogs have their team inputs scoped by Tenant so you can search from anywhere, since a team from Quebec could visit BC.

    • Worth noting that while the Team model allows Tenant access, the RosterMember model does not. Once the team is added to the schedule, you become authorized to the team until it’s removed.

Implementation

Two parts to this: our custom role resolver will hit the model’s hasOfficeAccess, hasScheduleAccess, hasTeamAccess, etc functions based on the type of permission, and a filter will be applied on top of any user filter via the Scope mixin. If you have a system role, this is skipped.

The actual implementation details of each model vary too much to describe here. A lot of models delegate to related models (e.g., a RosterMember will delegate to Team and ScheduleTeam), so you’ll want to look for models that define properties like officeId, scheduleId, teamId and then look at their relations.

Scope filtering is all defined in the scope mixin and the filter creators use switch cases to group relational logic, so that might be a little easier to follow. These should be logically similar to the hasAccess functions.

The key to understanding the scope filters is to realise that the office scope filters are building relations from the queried model to officeIds, the schedule scope filters are building relations to scheduleIds, etc.

 

image-20240308-150207.png