Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Table of Contents

...

minLevel1
maxLevel7

We have been looking at three related pieces of work:

...

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

* = this is an association, the object identifier is going to be the first part of the association, so for example, an organization study object is identified by its organization. Study participants permissions are identified by the

...

Model/Association/Verb

...

Permission

...

Meaning

...

Organization

...

userId ∈ {organization:orgId view}

...

Can view information about the organization.

...

userId ∈ {organization:orgId edit}

...

Can edit the organization

...

userId ∈ {organization:orgId admin}

...

Can edit permission for the organization

...

SponsoredStudies *

...

userId ∈ {sponsoredstudies:orgId list}

...

Can see the studies sponsored by the organization

...

Members *

...

userId ∈ {members:orgId list}

...

Can see the members of an organization

...

userId ∈ {members:orgId admin}

...

Can create/add/remove members

...

Assessments *

...

userId ∈ {assessments:orgId list}

...

Can view the assessments of an organization

...

Study

...

userId ∈ {study:studyId view}

...

Can view a Study (not its participants).

...

userId ∈ {study:studyId edit}

...

Can view or edit the Study

...

userId ∈ {study:studyId admin}

...

Can edit permissions for the Study

...

userId ∈ {study:studyId pi}

...

Can move the study from design to recruitment.

...

StudyParticipants *

...

userId ∈ {participants:studyId list}

...

Can view a list of AccountSummaries in this study

...

userId ∈ {participants:studyId view}

...

Can view StudyParticipant

...

userId ∈ {participants:studyId edit}

...

Can view or edit StudyParticipant

...

userId ∈ {participants:studyId admin}

...

Can create accounts and enroll/withdraw them from the study.

...

Assessment

...

userId ∈ {assessment:guid view}

...

Can view an assessment

...

userId ∈ {assessment:guid edit}

...

Can view or edit an assessment

...

userId ∈ {assessment:guid admin}

...

Can edit permissions for the assessment

Here’s the old table

...

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, including studies

...

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.

...

Assessment

...

Assessments are owned by organizations, not studies. Study members can read them, who can work with assessments?

...

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. If we continue to model this in the database, it'll become an associative table and that association could specify the roles you gain as a member of the organization—however no one is asking for this so I don't intend that we will do it.

Implementation

We will introduce a flat table of Permission records that can be easily retrieved by user or by target model object:

Code Block
languagejava
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 role; // "admin", "developer"
  String objectType; // "study", "organization", "app", "system"
  String objectId; // "studyId", "orgId", "appId"
  
  // Suggested toString() descriptor (implicitly scoped to an app):
  // "2rkp3nU7p8fjUTDVIgjT6T ∈ {organization:sage-bionetworks admin}"
}

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):

Code Block
languagejava
interface PermissionsService {
  Set<Permission> getPermissionsForUser(String appId, String userId);
  Permission addPermission(Permission permission);
  void updatePermission(Permission permission);
  void removePermission(Permission permissions);
  Set<Permission> getPermissionsForObject(String appId, ObjectType type, String id);
  // this delete cannot be cascaded by the database and must be done manually.
  void deletePermissions(String appId, 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 isAuthorizedAs(AccountId accountId, ObjectType 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/{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 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:

  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. 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.

  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

...

userId ∈ {app:appId developer}

...

RESEARCHER

...

RESEARCHER, APP SCOPE

...

userId ∈ {app:appId researcher}

...

ADMIN

...

ADMIN, APP SCOPE

...

userId ∈ {app:appId admin}

...

STUDY_DESIGNER

...

DEVELOPER, STUDY SCOPE

...

userId ∈ {study:studyId developer}

...

STUDY_COORDINATOR

...

RESEARCHER, STUDY SCOPE

...

userId ∈ {study:studyId researcher}

...

PI_AGENT, STUDY SCOPE

...

userId ∈ {study:studyId pi_agent}

...

ORG_ADMIN

...

ADMIN, ORGANIZATION SCOPE

...

userId ∈ {organization:orgId admin}

...

MEMBER, ORGANIZATION SCOPE

...

userId ∈ {organization:orgId member}

...

SUPERADMIN

...

ADMIN, SYSTEM SCOPE

...

userId ∈ {system admin}, in other words, appId is null

...

WORKER

...

WORKER, SYSTEM SCOPE

...

userId ∈ {system worker}, in other words, appId is null

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;

...

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:

Code Block
languagejava
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):

Code Block
languagejava
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:

  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. 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.

  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 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},
userId ∈ {participants:studyId edit},
userId ∈ {participants:studyId admin}

ORG_ADMIN

userId ∈ {organization:orgId edit},
userId ∈ {organization:orgId admin}
userId ∈ {members:orgId admin}

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 roles and organization membership changes are mirrored in the permissions table (but not vice versa?);

  3. 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;

  4. Annotate our controllers with the new permissions;

  5. Remove old code checking permissions;

  6. 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).

  7. Remove bridge code;

  8. Remove roles from accounts;

  9. 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

...