...
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
...
Study PI *
...
userId ∈ {studypi:studyId edit}
...
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
Implementation
We will introduce a flat table of Permission records that can be easily retrieved by user or by target model object:
...
language | java |
---|
...
The basic permission types are:
Permission | Object | Association |
---|---|---|
view | Can view the object | Can list members of the association and view them |
edit | Can view and edit the object | Can edit any member of the association |
admin | Can view, edit, and change permissions of object | Can list, view, edit, and add/remove members of the association |
Some of these permissions can be powerful. For example, {participants:studyId admin} has the power to create accounts and enroll them in a study, or withdraw them later.
Model/Association | Description |
---|---|
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 | ||
---|---|---|
| ||
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 permissionType; // "study", "organization", "app", "system" String objectId; // "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.
...
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 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
...
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 developeredit} | |||||
STUDY_COORDINATORRESEARCHER, STUDY SCOPE | userId ∈ {participants:studyId view}, | |||||
ORG_ADMINADMIN, ORGANIZATION SCOPE | userId ∈ {organization:orgId adminedit}MEMBER, ORGANIZATION SCOPE | ADMIN, SYSTEM SCOPE | userId ∈ {system admin}, in other words, appId is null | WORKER | WORKER, SYSTEM SCOPE | userId ∈ {system worker}, in other words, appId is nullmembers:orgId admin} |
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.
...