Bridge v2 authorization model
We have been looking at three related pieces of work:
Refactoring our account system to separate participants and administrative users;
Implementing more flexible, role-based authorization over domain objects (like studies);
Allowing users to be assigned to multiple organizations.
In brief, these are mostly separate pieces of work, but we believe #1 should happen before further integration with Synapse or any other external accounts management system, and #2 should happen before #3, so we don’t have to add functionality to organizations that we would just then remove. Doing #1 first might also make #2 easier to implement, but it is not absolutely necessary.
Separate participant and administrative account management
The business logic behind participant accounts has always been substantial (verification, consent, anonymization), but recently our requirements for administrative accounts have grown as well. Currently logic for both kinds of accounts is intermixed, making any additional work to either kind of account a higher risk than it needs to be.
Here are the current dependencies between our accounts, authorization, and consent classes:
This makes it difficult to talk about changes to the management of accounts, even for something like authorization. Here is one aspirational model where we separate Synapse-managed accounts and participant accounts, so that all the business logic around participants is separated from our code to manage admin accounts. Basically “Account" would refer to an administrative account and "Participant" would refer to a participant account most places in our code. (An alternative is to create a new thing, like AdminAccount
, for the admin accounts):
There would be backwards-incompatible consequences to this refactor. The /v3/participants APIs could no longer be used to create and manage administrative accounts. I don’t know who uses these for that purpose, at least the Bridge Study Manage would need to revise the UI it has to list all admins/users in one giant list (currently under the legacy panel). That API is still useful, but it would only return true participant accounts.
Notes on refactoring…
General
Explicitly mark administrative accounts before we migrate roles, because right now that's the way we determine these kind of accounts. We don’t want administrative accounts to turn into participant accounts if someone (by mistake or otherwise) removes the roles from the account.
AccountDao & AccountSecretDao
In the refactor above, a number of services can directly use AccountDao rather than AccountService. AccountDao and AccountSecretDao are basically fine as is, but we could write faster custom queries to return things like ID and health code than we are currently doing in the service classes.
ParticipantVersionService
Should monitor changes to the Accounts table that are done through ParticipantService, not AccountService.
ConsentService, EnrollmentService
AccountService -> AccountDao
AccountService & ParticipantService
These two could use a common abstract parent class. For example, deleteAccount() and deleteParticipant() would share a lot of code in common. Other places they will be pretty different (e.g. only AccountService needs to enforce role change logic, only participant service needs to deal with enrollment scenarios, etc.)
AccountService
move authenticate, changePassword, reauthenticate, deleteReauthToken, verifyChannel to the AuthenticationService.
Event services should not be used in AccountService, they should be used in ParticipantService;
ParticipantVersionService: should be used in ParticipantVersionService
any attempt to manipulate participant accounts through this service should fail immediately with a bad request exception;
ParticipantService
AccountService -> AccountDao
Any code related to roles and/or organizations can be removed to AccountService;
any attempt to manipulate admin accounts through this service should fail immediately with a bad request exception;
updateIdentifiers: authenticate in the controller as part of the security check so you can take these auth methods out of AccountService and ParticipantService. Also this method is more likely useful on AccountService (arguably we can just allow administrators to change participant’s phone and email address).
Auth: AuthenticationService, AccountWorkflowService, OAuthProviderService
These are all related and could be combined, except that it would make an unwieldy service. Maybe we could have a front controller that delegates to a set of implementation classes so external classes only deal with an AuthenticationService, but we're not maintaining a monolith.
AccountService/ParticipantService → AccountDao in all three services
UserAdminService
Fine as is (a crazy service to make integration testing palatable).
AccountController
add paginated API to retrieve admin accounts in the app (across all organizations…organizations have a membership API that most of our external users should be using);
access to these methods would require app:admin or system:superadmin powers (see below);
UserManagementController
it may be simpler and easier to create enrollment records directly in the account for testing purposes. This would remove the consent service and the enrollment service references;
An AccountService/ParticipantService base class should delete from all those other services, so we don't have to reference userAdminService just to properly delete an account.
Expanded permissions model
We’re seeking a permissions model that will cover our current security capabilities while tackling new use cases, such as the ability to manage access to studies.
The authorization model will be permissions-based, assigning list, view, and/or edit permissions vis-a-vis a specific target model;
These new permissions will be a separate table, not an association to an Account or any other record in the system…this is easier to graft onto the existing system. With some Hibernate sorcery, we can ensure that foreign key relationships are maintained and permissions are deleted when users and objects are deleted;
Permissions can be on objects (LIST, VIEW, or EDIT assessments, studies, organizations) but also on associations (VIEW or EDIT study sponsors, organization members) and rarely, on verbs (EDIT allowing for study launch). For example, the owner of an organization would have EDIT permission for the Organization and EDIT permission for the organization members. A member could have VIEW permissions over an organization’s sponsored studies. The permission to launch a study is very specific to that transformation of the state of the study.
Organizations will principally be a means to communicate who can see whom in a multi-tenanted application. Accounts will be assignable to multiple organizations. Migrated accounts will be given permissions to the sponsored studies of that organization, and then going forward, further permissions will have to be added on a case-by-case basis;
We will maintain ADMIN, WORKER, DEVELOPER, and RESEARCHER roles for our older APIs that are role-based (these will NOT be modeled in the new permissions table), but all newer v2 APIs should use this new permission structure, and the Roles STUDY_DESIGNER, STUDY_COORDINATOR, and ORG_ADMIN should be removed as part of this refactor. In addition:
Workers have their own APIs which bypass most security constraints and will continue to be implemented with the worker role;
Superadmins pass all security checks in both security systems;
App-scoped developer and researcher will no longer have study-scoped permissions;
App-scoped admins can still probably do anything within their app (so somewhat similar to super admins).
Use Cases
Use Case |
|
---|---|
Permissions changes should register for users without them having to sign out and sign back in again | If cached they need to be separate from the session in Redis. Otherwise, reading them on each request would meet this requirement. |
New admin account created with a sandbox in which studies can be created/edited that are not visible to others | When an account creates a study, it will be made the admin of that study. Searching for lists of studies will only return studies for which the caller has at least the AUDITOR role. |
“Sandbox” can be converted to real study, with additional users in specific roles for that study | Admin of a study can add additional users. We have not specified how we will make a study an “evaluation” study but that would need to be removable. |
Study is extended by creating a new study | Admin of the new study would need to copy over all the permissions from the old study. Bridge’s APIs should make this straightforward to do. |
Add someone to a study’s administration team | Add a permission (a role vis-a-vis the study) to that study. |
Remove someone from a study’s administration team | Remove a permission (a role vis-a-vis the study) from that study. |
Create similar authorization model for assessments | We should be able to expand this approach to any other model object we want to secure. |
Secured objects/scopes
The basic permission types are:
Permission | Object | Association (for a study) |
---|---|---|
list | Can list the object | Can see the study exists |
read | Can view the object | Can see the details of the study |
edit | Can edit the object | Can edit/update the study |
delete | Can delete the object | Can delete the study |
admin | Can view, edit, and change permissions of object | Can see permissions for the study and add/remove them. |
manage(?) |
| Add/remove members of an association might need to be split out from editing members (e.g. the power to enroll vs. the power to administer a participant). In that case admin does not include add/remove rights, that is moved to the manage role. |
Some of these permissions can be powerful. For example, {participants:studyId edit} has the power to create accounts and enroll them in a study, or withdraw them later.
Model/Association | Description |
---|---|
Account | A specific account, probably necessary to model “self” rules in our system. Might be given to all accounts automatically without having to write a record to the permissions table. |
Organization | An organization |
Sponsored Studies * | The studies sponsored by an organization |
Members * | The members of an organization |
Assessment Library * | The assessments owned by an organization (and thus not part of the shared and public library) |
Study | A study |
Study PI * | The PI of a study (a very specific association, always EDIT if it exists) |
Participants * | The participants in a study |
Assessment | An assessment |
Implementation
We will introduce a flat table of Permission records that can be easily retrieved by user or by target model object:
public class Permission {
String guid; // synthetic key makes create/add/update APIs easier
String appId; // most permissions except system-wide, and usually implicit
String userId;
String accessLevel; // "admin", "developer"
String entityType; // "study", "organization", "app", "system"
String entityId; // "studyId", "orgId", "appId"
// Suggested toString() descriptor (implicitly scoped to an app):
// "2rkp3nU7p8fjUTDVIgjT6T ∈ {organization:sage-bionetworks admin}"
}
// Each type relates to a specific entity and its ID (indicated in the constructor)
public enum PermissionType {
ASSESSMENT (ASSESSMENT),
STUDY (STUDY),
ORGANIZATION (ORGANIZATION),
SPONSORED_STUDIES (ORGANIZATION),
MEMBERS (ORGANIZATION),
ASSESSMENTS (ORGANIZATION), // ASSESSMENT_LIBRARY?
STUDY_PI (STUDY),
PARTICIPANTS (STUDY);
}
For APIs that have to display permissions, the appId/userId can be replaced with an AccountRef
object, similar to the EnrollmentDetail
object.
The service (along with a method to integrate with Spring Security):
interface PermissionsService {
Set<Permission> getPermissionsForUser(String appId, String userId);
Permission addPermission(Permission permission);
void updatePermission(Permission permission);
void removePermission(Permission permission);
Set<Permission> getPermissionsForType(String appId, PermissionType type, String id);
/** Spring security will need a very focused method to check, for a
* given user and a given object, does the user have any of the required
* roles to perform the request. This method can fudge things like
* app-scoped permissions, too.
*/
boolean isAuthorizedAs(AccountId accountId, PermissionType type, String objectId, Role... roles);
}
There will be top-level APIs to change permissions. Creating an object that is managed with permissions will always make the creator the administrator of that object:
Method | URL | Description |
---|---|---|
GET | /v1/permissions/{userId} | Get all permissions for a user. |
GET | /v1/permissions/{entityType}/{entityId} | Get all permissions for an object like organization, study, or app. |
POST | /v1/permissions | Create a permission for a specific object and user. Caller must be an admin for the object. Returns the object with a GUID. |
POST | /v1/permissions/{guid} | Update a permission (caller must be an admin for the object). |
DELETE | /v1/permissions/{guid} | Remove a permission for an object (caller must be an admin for the object). |
Spring Security
Spring security has nice support for annotation-based authorization constraints. I would suggest we switch to it and secure the system at the controller layer by annotating our controller methods. Spring provides an expression language we can use to declare our constraints, and we can even implement new methods in that constraint language, so that Spring delegates to our own code to answer authorization questions. It would allow new developers to work with a technology that they have seen before, and that is documented.
Using Spring security for authorization (not authentication, at least initially) we would do the following:
In a filter, create a caller's
Authentication
object and put it in Spring Security'sSecurityContext
(exactly like what we've been doing with our ownRequestContext
; we’d store the user’s ID and app ID);Add authorization annotations to all of our controller methods.
We can basically do our security checks in these annotations, e.g.@PreAuthorize("permit('developer', #studyId)")
- permit a developer for the study ID (taken from the method’s parameters) to access the controller method. Or@PostAuthorize("returnedObject.ownerId == authentication.orgMembership")
to check rules against the object being returned from the method. Because we can implement custom functions in the evaluation language, we can carry over our specific business logic. Later we can hook in other authorization systems very cleanly this way.Remove our own static method call checks in
AuthUtils
. Eventually consider if we can removeRequestContext
since it is 90% of the time being used to do authorization checks.
Migration
Existing roles will need to be maintained for older APIs. New APIs including assessments, all the /v5/studies/* APIs, will solely use the new permissions system. Therefore, we will not map existing roles into the permissions table. We will migrate the study-scoped roles:
Old role | Permissions |
---|---|
STUDY_DESIGNER | userId ∈ {study:studyId edit} |
STUDY_COORDINATOR | userId ∈ {participants:studyId view}, |
ORG_ADMIN | userId ∈ {organization:orgId edit}, |
The steps would be:
Add the permissions table, service, APIs, completely separate from existing security so they are completely functional;
Create bridge code so that roles and organization membership changes are mirrored in the permissions table (but not vice versa?);
Migrate all existing account roles to the new permissions tables. Changes made at any time after the migration should also make it to the permissions tables, which still cannot be used;
Annotate our controllers with the new permissions;
Remove old code checking permissions;
Switch over to use the new permissions apis rather than account APIs to manage permissions (probably by throwing errors if roles are changed on an account).
Remove bridge code;
Remove roles from accounts;
Remove code that is granting permissions to studies as a result of organization membership, which is a large external change that will need to be documented and supported in existing tools.
Role to Permission Mapping
This mapping is necessary for steps 2 and 3 in migration. Each role needs to be mimicked by permissions.
Roles manage access to studies and assessments through organization membership. The “sponsored studies” and “assessment library” permissions similarly grant access to studies and assessments based on organization. So the individual “study” and “assessment” permissions should be unnecessary for migration.
The “participant” permission is more complicated because it is handled by individual study. Developers and Study Designers only have access to test accounts. Researchers and Study Coordinators have access to participant data. So mimicking the role means granting “participant” access to each study in their organization.
Entity Type | Access Level | DEVELOPER | RESEARCHER | STUDY_COORDINATOR | STUDY_DESIGNER | ORG_ADMIN | ADMIN | Note |
ASSESSMENT | List | No | No | No | No | No | No | Assessments should be accessible through the assessment library permission. |
Read | No | No | No | No | No | No | ||
Edit | No | No | No | No | No | No | ||
Delete | No | No | No | No | No | No | ||
Admin | No | No | No | No | No | No | ||
ASSESSMENT_LIBRARY | List | Yes | Yes | Yes | Yes | Yes | Yes | The entity ID would be the user's organization, granting permissions to all assessments under that organization. |
Read | Yes | Yes | Yes | Yes | Yes | Yes | ||
Edit | Yes | No | No | Yes | No | Yes | ||
Delete | Yes | No | No | Yes | No | Yes | ||
Admin | No | No | No | No | Yes | Yes | ||
MEMBERS | List | Yes | Yes | Yes | Yes | Yes | Yes |
|
Read | Yes | Yes | Yes | Yes | Yes | Yes | ||
Edit | No | No | No | No | Yes | Yes | ||
Delete | No | No | No | No | Yes | Yes | ||
Admin | No | No | No | No | Yes | Yes | ||
ORGANIZATION | List | Yes | Yes | Yes | Yes | Yes | Yes |
|
Read | Yes | Yes | Yes | Yes | Yes | Yes | ||
Edit | No | No | No | No | Yes | Yes | ||
Delete | No | No | No | No | Yes | Yes | ||
Admin | No | No | No | No | Yes | Yes | ||
PARTICIPANTS | List | No | Yes | Yes | No | No | Yes | Participant permissions are managed by study ID. So each study in the organization will need these permissions granted to mimic the role's access. |
Read | No | Yes | Yes | No | No | Yes | ||
Edit | No | Yes | Yes | No | No | Yes | ||
Delete | No | Yes | Yes | No | No | Yes | ||
Admin | No | No | No | No | No | Yes | ||
SPONSORED_STUDIES | List | Yes | Yes | Yes | Yes | Yes | Yes | Researchers and Study Coordinators can "phase transition" studies. |
Read | Yes | Yes | Yes | Yes | Yes | Yes | ||
Edit | Yes | Yes | Yes | Yes | No | Yes | ||
Delete | Yes | No | No | Yes | No | Yes | ||
Admin | No | No | No | No | Yes | Yes | ||
STUDY | List | No | No | No | No | No | No | All studies in organization should be covered through the sponsored studies permission. |
Read | No | No | No | No | No | No | ||
Edit | No | No | No | No | No | No | ||
Delete | No | No | No | No | No | No | ||
Admin | No | No | No | No | No | No | ||
STUDY_PI | List | No | No | No | No | No | No |
|
Read | No | No | No | No | No | No | ||
Edit | No | No | No | No | No | No | ||
Delete | No | No | No | No | No | No | ||
Admin | No | No | No | No | No | No |
Multiple organization membership
Introduce a Membership associative table between Accounts and Organizations, and migrate the existing orgMembership value to this table. The table should be a simple associative table as roles are managed separately. But when we need to retrieve the list of members, or the organizations a person belongs to, it’s the object model we will examine.
Externally the biggest change to the Organizations API would be that adding or removing a person from an organization would not automatically change their one and only membership.
Questions
We could design the permissions table so the objects are cascade deleted when object is deleted, otherwise it will need to be done manually (a method we must call).
Assessments are owned by organizations, but what does that mean going forward? I would think all organization members can read or list the organization’s assessments, but can you be a developer vis-a-vis an assessment? Who would assign that (presumably the organization administrator)? Can an assessment itself have an admin?
Can we manage permissions for “entities of concern” in Synapse, specifically projects and access teams? It would appear we need to follow an authorization model that’s close enough to Synapse’s that we can translate changes in the Bridge paradigm to meaningful changes in Synapse, and vice versa (at least for the entities we directly work with).
Can we build an invitation system that uses Synapse to send the messages, since we won’t have email addresses for Synapse accounts? It seems like we will eventually want the same invite-and-accept functionality for joining organizations, studies, etc. that we see in Synapse. Actually this goes for things like warnings about participants who are out of adherence (and it’s out of scope but came up in this design discussion).