Versions Compared

Key

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

In adding Mobile Toolbox to Bridge, we’re encountering more complex authorization. We have roles like an organization administrator, who can create users who are members of their organization (but who cannot see any other kind of accounts, including study participants in their organization’s studies), and study designers, who can edit some of the things that a developer can edit…but only in the context of the studies they have access to.

Right now these checks are spread out:

  1. Controllers usually check for roles and consent;

  2. AuthUtils methods are called in the services to check the relationship of the caller to entities like studies and organizations.

This is getting messy. I think we could use a more robust alternative to implement this. But first, here are the authorization checks we have implemented or want to implement in the MTB timeframe (described in terms of access to objects in the REST API, rather than through the several endpoints that are needed to expose each object in the API itself, and skipping participant-facing APIs). Then as well look at alternatives for implementation, I’ll show what modeling enrollment would look like.

Authorization Rules

...

Object

...

Assoc(1)

...

Role

...

Permissions(2)

...

AccountSummary

...

global (but filtered)

...

researcher

...

read

...

Study (“participant”)

...

researcher

...

read

...

Organization (“member”)

...

org admin

...

read

...

App

...

all

...

read

...

dev, admin

...

update

...

superadmin

...

create, delete

...

AppConfig

...

public

...

read (filtered)

...

dev

...

create, read, write, delete

...

AppConfigElement

...

dev

...

create, read, write, delete

...

Assessment

...

Organization (“owner”)

...

dev

...

create, read, write

...

admin

...

delete

...

AssessmentConfig

...

public

...

read

...

Organization (“owner”)

...

dev

...

write

...

Enrollment(Detail)

...

Account (“self”)

...

any

...

create, read, delete

...

Study

...

researcher

...

create, read, delete

...

Study

...

admin

...

create, read, delete

...

AssessmentResource

...

Organization (“owner”)

...

developer

...

create, read, delete

...

admin

...

delete

...

FileMetadata/Revision

...

developer

...

create, read, write

...

admin

...

delete

...

HealthDataRecord(Ex3)

...

participant

...

create

...

worker

...

write

...

MasterScheduleConfig

...

superadmin

...

create, read, write, delete

...

NotificationMessage

...

self, admin, researcher

...

create

...

NotificationRegistration

...

self, researcher, admin

...

read

...

NotificationTopic

...

developer

...

create, read, write

...

admin

...

delete

...

Organization

...

any

...

read

...

Account (“membership”)

...

org_admin

...

write

...

admin

...

create, delete

...

RecordExportStatusRequest

...

worker

...

write

...

ReportData

...

Study

...

any/public

...

read

...

Study

...

dev, worker

...

create, delete

...

ReportIndex

...

Study

...

any

...

read

...

Study

...

dev

...

create, write

...

RequestInfo

...

Account (“self”)

...

RequestInfo

...

Study

...

researcher

...

read

...

admin, worker

...

read

...

SchedulePlan

...

developer, researcher, worker

...

read

...

developer

...

create, write

...

admin

...

delete

...

SmsTemplate

...

Account

...

worker

...

create

...

Study

...

org_admin

...

create

...

Organization (“sponsors”)

...

org_admin

...

create, read, delete

...

StudyConsent

...

dev

...

create, read

...

StudyParticipant

...

Study (“enrolled”)

...

researcher

...

create, read, write

...

worker

...

read

...

Participant (“self”)

...

read, write

...

admin

...

create

...

Subpopulation

...

dev

...

create, read, write

...

researcher

...

read

...

admin

...

delete

...

Survey

...

dev, researcher, worker

...

read

...

dev

...

create, write

...

admin

...

delete

...

Tag

...

public

...

read

...

superadmin

...

create, delete

...

Template/TemplateRevision

...

dev

...

create, read, write

...

admin

...

delete

...

Upload

...

dev, admin, worker

...

read

...

UploadSchema

...

The Solution

We implemented an AuthEvaluator class that can be used to describe static objects that will check our different authorization rules. (Sometimes we still need to do a verification on models returned from the system, which are not covered…e.g. “is this object you loaded associated to the study we just verified you have access to?”)

The AuthEvaluator rules can be called in the controllers, then it’s much easier to find the authorization rule for a given API endpoint when writing SDK documentation. The evaluator currently has an overly-broad rule that needs to be removed for more specific checks:

Jira Legacy
serverSystem JIRA
columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
serverIdba6fb084-9827-3160-8067-8ac7470f78b2
keyBRIDGE-2897

Bridge Roles

ADMIN - all authorization checks should succeed for ADMINs. They should be excepted in the AuthEvaluator, then they can be removed from getSession(…) checks.

A SUPERADMIN is an ADMIN in all respects, but who has APIs to switch between apps without an account in each app (UserManagementController.signInForSuperAdmin and AuthenticationController.changeApp). Admin and Superadmin SDK clients can just define the APIs, like the cache or app creation APIs, that are only available to those roles, since all the other clients will work and succeed for these users.

This is not fully or consistently implemented, see:

Jira Legacy
serverSystem JIRA
columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
serverIdba6fb084-9827-3160-8067-8ac7470f78b2
keyBRIDGE-2898

DEVELOPER, RESEARCHER - These divide the APIs between calls to configure an app and calls to return information about study participants (include PII). They are app-scoped, and too broad for apps that have multiple studies run by different researchers.

We might choose to allow these roles access to their study-scoped alternates (described below). They would then be supersets of those roles. See:

Jira Legacy
serverSystem JIRA
columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
serverIdba6fb084-9827-3160-8067-8ac7470f78b2
keyBRIDGE-2899

STUDY COORDINATOR - This is a study-scoped version of a researcher (they can only access studies that they have access to through their organization). This person may be able to do other things, like set up schedules for a study.

STUDY DEVELOPER - This is a study-scoped version of a developer. It’s not clear yet if it will be needed (coordinators might be able to configure everything about a study and we aren’t going to let outside developers muck about with app configuration).

Jira Legacy
serverSystem JIRA
columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
serverIdba6fb084-9827-3160-8067-8ac7470f78b2
keyBRIDGE-2896

ORGANIZATION ADMINISTRATOR - This is an administrator of an organization, that can edit an organization, including creating accounts in the organization. They or their users can in turn create studies that are sponsored by, and accessible to, all organization members.

The Problem (original issues that led to this design)

In adding Mobile Toolbox to Bridge, we’re encountering more complex authorization. We have roles like an organization administrator, who can create users who are members of their organization (but who cannot see any other kind of accounts, including study participants in their organization’s studies), and study designers, who can edit some of the things that a developer can edit…but only in the context of the studies they have access to.

Right now these checks are spread out:

  1. Controllers usually check for roles and consent;

  2. AuthUtils methods are called in the services to check the relationship of the caller to entities like studies and organizations.

This is getting messy. The issues I feel I’m encountering

  1. Security is defined in different places, there’s not one place to see what is and is not permissible;

  2. Utility methods are difficult to compose into new requirements and despite my best attempts, the names of these things rename confusing at times;

  3. Consequently it’s hard to say we don’t have lapses in the authorization checks that are occurring.

I think we could use a more robust alternative to implement this. But first, here are the authorization checks we have implemented or want to implement in the MTB timeframe (described in terms of access to objects in the REST API, rather than through the several endpoints that are needed to expose each object in the API itself, and skipping participant-facing APIs). Then as well look at alternatives for implementation, I’ll show what modeling enrollment would look like.

Authorization Rules

Object

Assoc(1)

Role

Permissions(2)

AccountSummary

global (but filtered)

researcher

read

Study (“participant”)

researcher

read

Organization (“member”)

org admin

read

App

all

read

dev, admin

update

superadmin

create, delete

AppConfig

public

read (filtered)

dev

create, read, write, delete

AppConfigElement

dev

create, read, write, delete

Assessment

Organization (“owner”)

dev

create, read, write

admin

delete

AssessmentConfig

public

read

Organization (“owner”)

dev

write

Enrollment(Detail)

Account (“self”)

any

create, read, delete

Study

researcher

create, read, delete

Study

admin

create, read, delete

AssessmentResource

Organization (“owner”)

developer

create, read, delete

admin

delete

FileMetadata/Revision

developer

create, read, write

admin

delete

HealthDataRecord(

...

(2) C = create/write, R = read (list or detail object), U = update/write/delete logically, D = delete physically.

Here are objects from the v2 domain model that have been designed far enough to think about permissions (designer = STUDY_DESIGNER):

...

Object

...

Assoc

...

Role

...

Permissions

...

Protocol/Study Arms

...

Organization (“owner”)

...

designer, developer

...

create, read, write

...

Study

...

designer, developer

...

read

...

admin

...

delete

...

Schedule

...

Study (“owner”)

...

designer, developer

...

create, read, write

...

admin

...

delete

...

Study Arm Schedule

...

Organization (“owner”)

...

designer, developer

...

create, read, delete

Spring Security

This is the most complex option available, but we could use the method-based security via annotations. Our service methods could declare the security using complex expression rules.

Modeling EnrollmentService, it would look like this (ignoring the work necessary to make it work):

...

languagejava

...

Ex3)

participant

create

worker

write

MasterScheduleConfig

superadmin

create, read, write, delete

NotificationMessage

self, admin, researcher

create

NotificationRegistration

self, researcher, admin

read

NotificationTopic

developer

create, read, write

admin

delete

Organization

any

read

Account (“membership”)

org_admin

write

admin

create, delete

RecordExportStatusRequest

worker

write

ReportData

Study

any/public

read

Study

dev, worker

create, delete

ReportIndex

Study

any

read

Study

dev

create, write

RequestInfo

Account (“self”)

RequestInfo

Study

researcher

read

admin, worker

read

SchedulePlan

developer, researcher, worker

read

developer

create, write

admin

delete

SmsTemplate

Account

worker

create

Study

org_admin

create

Organization (“sponsors”)

org_admin

create, read, delete

StudyConsent

dev

create, read

StudyParticipant

Study (“enrolled”)

researcher

create, read, write

worker

read

Participant (“self”)

read, write

admin

create

Subpopulation

dev

create, read, write

researcher

read

admin

delete

Survey

dev, researcher, worker

read

dev

create, write

admin

delete

Tag

public

read

superadmin

create, delete

Template/TemplateRevision

dev

create, read, write

admin

delete

Upload

dev, admin, worker

read

UploadSchema

dev

create, read, write

admin

delete

(1) = association to another model object. This typically means an additional check to ensure is a member of an organization, or has access to a study through their organization, or is the target of the call as well as the caller, and so forth. If blank, then the association is to an app, because everything is scoped/tenanted to an app.

(2) C = create/write, R = read (list or detail object), U = update/write/delete logically, D = delete physically.

Here are objects from the v2 domain model that have been designed far enough to think about permissions (designer = STUDY_DESIGNER):

Object

Assoc

Role

Permissions

Protocol/Study Arms

Organization (“owner”)

designer, developer

create, read, write

Study

designer, developer

read

admin

delete

Schedule

Study (“owner”)

designer, developer

create, read, write

admin

delete

Study Arm Schedule

Organization (“owner”)

designer, developer

create, read, delete

Spring Security

This is the most complex option available, but we could use the method-based security via annotations. Our service methods could declare the security using complex expression rules.

Modeling EnrollmentService, it would look like this (ignoring the work necessary to make it work):

Code Block
languagejava
public class EnrollmentService {

    @Secure("principal.id == userId or hasRole('ADMIN') or " + 
      "(hasRole('RESEARCHER') and hasOrgSponsoredStudy(studyId))")
    public PagedResourceList<EnrollmentDetail> getEnrollments(...) {
    }
    
    @Secure("principal.id == userId or hasRole('ADMIN') or " + 
      "(hasRole('RESEARCHER') and hasOrgSponsoredStudy(studyId))")
    public Enrollment enroll(...) {
    }
    
    @Secure("principal.id == userId or hasRole('ADMIN') or " + 
      "(hasRole('RESEARCHER') and hasOrgSponsoredStudy(studyId))")
    public void updateEnrollment(...) {
    }
    
    @Secure("principal.id == userId or hasRole('ADMIN') or " + 
      "(hasRole('RESEARCHER') and hasOrgSponsoredStudy(studyId))")
    public Enrollment unenroll(...) {
    }
}

Pros

  1. It’s a standardized thing so other developers should have an easier time working with it;

  2. Ultimately, security would be expressed in annotations, which seems nice;

  3. One step closer to implementing a major change to something like storing access rights in a database (so far we haven’t seen anything in Bridge that requires that level of authorization, but)

Cons

  1. Our existing authentication and authorization have all been written from scratch, which means we’d need to overwrite all the core Spring Security classes before any of this would work;

  2. The result would take time and involve learning a lot more about Spring Security;

  3. Security exceptions would appear in places where we are now able to return InvalidEntityExceptions and the like. For example empty/null/imaginary values are going to throw security exceptions. (I’m fine with this).

Implement a DSL

Nothing Spring does is that hard to duplicate with a DSL that uses our existing RequestContext. I think it makes sense to call these in the services, where multiple endpoints might create multiple conditions under which a call is acceptable. It could look something like this:

Code Block
languagejava
public class EnrollmentService {
    private static final AuthEvaluator SELF_ADMIN_OR_STUDY_RESEARCHER = 
        AuthUtils.canAccessStudy().inRole(RESEARCHER).or()
            .inAnyRole(ADMIN, SUPERADMIN).or()
            isSelf();

    public PagedResourceList<EnrollmentDetail> getEnrollments(...) {
    }
    {
     @Secure("principal.id == callerUserId or hasRole('ADMIN') or " + SELF_ADMIN_OR_STUDY_RESEARCHER.checkAndThrow("studyId", studyId, "userId", userId);
    }
  "(hasRole('RESEARCHER') and hasOrgSponsoredStudy(studyId))")
    public Enrollment enroll(...) {
    }     
    @Secure("principal.id == callerUserId or hasRole('ADMIN') or " + 
      "(hasRole('RESEARCHER') and hasOrgSponsoredStudy(studyId))")SELF_ADMIN_OR_STUDY_RESEARCHER.checkAndThrow("studyId", studyId, "userId", userId);
    }
    
    public void updateEnrollment(...) {
    }     
    @Secure("principal.id == callerUserId or hasRole('ADMIN') or " +SELF_ADMIN_OR_STUDY_RESEARCHER.checkAndThrow("studyId", studyId, "userId", userId);
    }
  "(hasRole('RESEARCHER') and hasOrgSponsoredStudy(studyId))")
    public Enrollment unenroll(.....) {) {
        SELF_ADMIN_OR_STUDY_RESEARCHER.checkAndThrow("studyId", studyId, "userId", userId);
    }
}

Pros

  1. It’s a standardized thing so other developers should have an easier time working with it;

  2. Ultimately, security would be expressed in annotations, which seems nice;

  3. One step closer to implementing a major change to something like storing access rights in a database (so far we haven’t seen anything in Bridge that requires that level of authorization, but)

Cons

  1. Our existing authentication and authorization have all been written from scratch, which means we’d need to overwrite all the core Spring Security classes before any of this would work;

  2. The result would take time and involve learning a lot more about Spring Security;

  3. Security exceptions would appear in places where we are now able to return InvalidEntityExceptions and the like. For example empty/null/imaginary values are going to throw security exceptions. (I’m fine with this).

Implement a DSL

Nothing Spring does is that hard to duplicate with a DSL that uses our existing RequestContext. I think it makes sense to call these in the services, where multiple endpoints might create multiple conditions under which a call is acceptable. It could look something like this:

  1. Easier to implement and understand at this point, when compared with overriding Spring Security’s implementation classes

  2. Arguably, easier to understand because it’ll only contain what it necessary for our application (as opposed to Spring which is always more complicated because it can handle anything, including future requirements).

Cons:

  1. More custom code for new developers to learn (assuming they already know Spring security which is more portable for them anyway);

  2. We’re bearing down further on a custom implementation making that much harder later if we decide to abandon it for anything else. If we’re going to switch, better to switch sooner.

Recommit to implementing authorization in the controllers

Arguably we can continue to use authorization in the controllers if we just create more endpoints that are tailored to our specific authorization scenarios.

Take the example of AuthUtils.checkSelfStudyResearcherOrAdmin check for enrollments:

Code Block
languagejava
public class EnrollmentService {{

    @GetMapping("/v5/studies/{studyId}/enrollments")
    privatepublic static final AuthEvaluator IS_SELF_ADMIN_OR_STUDY_RESEARCHER =PagedResourceList<EnrollmentDetail> getEnrollments(...) {
        UserSession session = AuthUtils.canAccessStudy().inRole(RESEARCHER).or()getAuthenticatedSession(RESEARCHER, ADMIN);

        if   .inAnyRole(!session.isInRole(ADMIN, SUPERADMIN).or()
) && !AuthUtils.canAccessStudy(studyId)) {
          throw isSelfnew UnauthorizedException();
     public PagedResourceList<EnrollmentDetail> getEnrollments(...) { }
    }
  IS_SELF_ADMIN_OR_STUDY_RESEARCHER.checkAndThrow("studyId", studyId); 
   }
    @GetMapping("/v3/participants/self/enrollments")
     public EnrollmentPagedResourceList<EnrollmentDetail> enrollgetSelfEnrollments(...) {
        IS_SELF_ADMIN_OR_STUDY_RESEARCHER.checkAndThrow("studyId", studyIdUserSession session = getAuthenticatedSession();
    }    // set userId to the caller's public void updateEnrollment(...) {user Id
    }

    IS_SELF_ADMIN_OR_STUDY_RESEARCHER.checkAndThrow("studyId", studyId);
    }@PostMapping("/v5/studies/{studyId}/enrollments")
    
    public Enrollment unenrollenroll(...) {
        IS_SELF_ADMIN_OR_STUDY_RESEARCHER.checkAndThrow("studyId", studyIdUserSession session = getAuthenticatedSession(RESEARCHER, ADMIN);

   }
}

Pros:

  1. Easier to implement and understand at this point, when compared with overriding Spring Security’s implementation classes

  2. Arguably, easier to understand because it’ll only contain what it necessary for our application (as opposed to Spring which is always more complicated because it can handle anything, including future requirements).

Cons:

  1. More custom code for new developers to learn (assuming they already know Spring security which is more portable for them anyway);

  2. We’re bearing down further on a custom implementation making that much harder later if we decide to abandon it for anything else. If we’re going to switch, better to switch sooner.

Recommit to implementing authorization in the controllers

Arguably we can continue to use authorization in the controllers if we just create more endpoints that are tailored to our specific authorization scenarios.

Take the example of AuthUtils.checkSelfStudyResearcherOrAdmin check for enrollments:

Code Block
public class EnrollmentService {

    @GetMapping     if (!session.isInRole(ADMIN) && !AuthUtils.canAccessStudy(studyId)) {
          throw new UnauthorizedException();
        }
    }

    @PostMapping("/v3/participants/self/enrollments")
    public Enrollment enrollSelf(...) {
        UserSession session = getAuthenticatedSession();
        // set userId to the caller's user Id
    }

    @PostMapping("/v5/studies/{studyId}/enrollments/{userId}")
    public PagedResourceList<EnrollmentDetail>void getEnrollmentsupdateEnrollment(...) {
    }
    
    @GetMapping("/v3/participants/self/enrollments")     UserSession publicsession PagedResourceList<EnrollmentDetail>= getSelfEnrollments(...) {getAuthenticatedSession(RESEARCHER, ADMIN);

     }   if (!session.isInRole(ADMIN)  @PostMapping("/v5/studies/{studyId}/enrollments")&& !AuthUtils.canAccessStudy(studyId)) {
          publicthrow Enrollmentnew enrollUnauthorizedException(...) {;
        }
    }
    
    @PostMapping@DeleteMapping("/v3v5/participantsstudies/self{studyId}/enrollments/{userId}")
    public Enrollment enrollSelfunenroll(...) {
    }    UserSession session @PostMapping("/v5/studies/{studyId}/enrollments/{userId}")= getAuthenticatedSession(RESEARCHER, ADMIN);

    public    voidif updateEnrollment(!session...isInRole(ADMIN) && !AuthUtils.canAccessStudy(studyId)) {
    }      throw new   @DeleteMapping("/v5/studies/{studyId}/enrollments/{userId}")UnauthorizedException();
     public Enrollment unenroll(...) { }
    }

    @DeleteMapping("/v3/participants/self/enrollments/{userId}")
    public Enrollment unenrollSelf(...) {
        UserSession session = getAuthenticatedSession();
        // set userId to the caller's user Id
    }
}

Another example are org administrators who can list, read, create, and delete administrative accounts in their organizations:

...