Separate participant and administrative account management

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

AccountDao & AccountSecretDao

ParticipantVersionService

ConsentService, EnrollmentService

AccountService & ParticipantService

AccountService

ParticipantService

Auth: AuthenticationService, AccountWorkflowService, OAuthProviderService

UserAdminService

AccountController

UserManagementController

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.

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 a role (which might be transitively assigned through an organization, effectively saying that organization membership grants visibility to studies as well).

“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

Model

Role

Organization

Administrator

  • Can list, view, add and remove people from an organization.

  • Can they edit an account in the organization

  • Can list studies sponsored by the organization

Organization

Member

  • Can list people in the organization

  • Can list studies sponsored by the organization

  • Note that it imparts no roles vis-a-vis other models except for the auditor role on any sponsored study the member otherwise doesn’t have access to

Study

Auditor

Can read information about the study and its schedule. Cannot edit anything and can’t see people.

Study

Developer

Can read and edit the configuration of the study and its schedule.

Study

Researcher

Can list, view, edit, delete, enroll and unenroll accounts in the study.

Study

PI_Agent

Can move the study into a production stage, and can view the data being exported to a project in Synapse. User must be a validated Synapse user. Can delete a study.

Study

Admin

Can do anything related to a study or its participants, and they can change users associated to the study or their roles in that association.

App

Developer

Can access many APIs for the configuration of an app and related resources like app configs.

App

Researcher

Can see all accounts in the system regardless of organization or study boundaries.

App

Admin

Can call any API in the scope of the account’s app.

Global

Worker

Can access APIs that specifically allow the worker to call across app boundaries without switching applications first.

Global

Admin

Can do anything in any app, study, or organization (superadmin)

Note that membership in an organization is also directly modeled in the database right now via the Account.orgMembership field, and will be moved to an associative table. We may not need a “member” role though it may be more convenient.

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; // this always has to be part of the query
  String userId;
  String role; // "admin", "developer"
  String objectType; // "study", "organization", "app", "system"
  String objectId; // "studyId", "orgId", "appId"
  boolean transitive; // e.g. true if permission comes from org membership
  
  // Suggested toString() descriptor (implicitly scoped to an app):
  // "2rkp3nU7p8fjUTDVIgjT6T is an organization:sage-bionetworks admin"
}

The service (which we’ll probably access through Spring Security, see below):

interface PermissionsService {
  Set<Permission> getPermissionsForUser(String userId, boolean includeTransitive);
  Permission addPermission(Permission permission);
  void updatePermission(Permission permission);
  void removePermission(Permission permissions);
  Set<Permission> getPermissionsForObject(ObjectType 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 isAuthorized(AccountId accountId, ObjectType type, String objectId, Role... roles);
}

There will be top-level APIs to change permissions:

Method

URL

Description

GET

/v1/permissions/{userId}

Get all permissions for a user.

GET

/v1/permissions/{objectType}/{objectId}

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 a request level 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, and Spring will delegate to our own code to answer authorization questions. But 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:

  1. In a filter, create a caller's Authentication object and put it in Spring Security's SecurityContext (exactly like what we've been doing with our own RequestContext; we’d store the user’s ID and app ID);

  2. 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. Because we can implement the “permissions” method, we can carry over our specific business logic. Later we can hook in other authorization systems very cleanly this way.

  3. Remove our own static method call checks in AuthUtils. Eventually consider if we can remove RequestContext since it is 90% of the time being used to do authorization checks.

Migration

Existing roles can be expressed in the new permissions table in order to make the same kind of authorization checks. This can be done independently of allowing users to be in multiple organizations. For every administrative account in the system, we’d want to create entries based on their current roles:

Old role

New role

Description

DEVELOPER

DEVELOPER, APP SCOPE

RESEARCHER

RESEARCHER, APP SCOPE

ADMIN

ADMIN, APP SCOPE

STUDY_DESIGNER

DEVELOPER, STUDY SCOPE

Create this role for every study currently sponsored by the account’s organization

STUDY_COORDINATOR

RESEARCHER, STUDY SCOPE

Create this role for every study currently sponsored by the account’s organization

PI_AGENT, STUDY SCOPE

ORG_ADMIN

ADMIN, ORGANIZATION SCOPE

MEMBER, ORGANIZATION SCOPE

Create this permission for the account’s member organization? Not sure if we want to duplicate like this. Maybe we do and drop the org membership column, allowing for an easy migration to membership in multiple organizations.

SUPERADMIN

ADMIN, SYSTEM SCOPE

WORKER

WORKER, SYSTEM SCOPE

The steps would be:

  1. Add the permissions table, service, APIs, completely separate from existing security so they are completely functional;

  2. Create bridge code so that organization memberships are mired in the permissions table (only needs to be one way);

  3. Create bridge code so that role changes create the counterpart permissions (only needs to be one way);

  4. Migrate all existing account roles to the new permissions tables;

  5. Annotate our controllers with the new permissions;

  6. Switch over external tools to use permissions apis rather than account APIs to manage permissions;

  7. Remove roles and bridge code from accounts.

Multiple organization membership

Once we have a permissions table, we can implement accounts being in multiple organizations. The utility of this construct will be lessened (because people can be permitted to act directly against a study) but it may still be important for the future.