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 study participants), 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:
Controllers usually check for roles and consent;
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. Here are three options I can think of:
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.
Pros
It’s a standardized thing so other developers should have an easier time working with it;
Ultimately, security would be expressed in annotations, which seems nice;
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
Our existing authentication and authorization have all been written from scratch, which means we’d need to overwrite a number of Spring Security classes before any of this would work;
The result would take time and involve a lot of code (and a solid understanding of Spring Security);
Security exceptions would appear in places where we are now able to return InvalidEntityExceptions and the like. Possibly confusing to existing users.
Implement a DSL
Nothing Spring does is that hard to duplicate with a DSL that uses our existing RequestContext. We should standardize on calling these checks in the controller or the services (both is leading to confusion for me). It could look something like this:
private static final AuthEvaluator IS_ORG_ADMIN = AuthUtils.orgMember().inRole(ORG_ADMIN); // If we pass arguments in when we check, these are threadsafe and composable private static final AuthEvaluator IS_ORG_ADMIN_OR_ADMIN = IS_ORG_ADMIN.or().inAnyRole(ADMIN, SUPERADMIN); public void mustBeAnOrgAdmin(String appId, String orgId) { IS_ORG_ADMIN.checkAndThrow("orgId", orgId); // do something } public void mustBeSomeKindOfAdmin(String appId, String orgId) { IS_ORG_ADMIN_OR_ADMIN.checkAndThrow("orgId", orgId); // do something }
Pros:
Easier to implement and understand at this point, when compared with overriding Spring Security’s implementation classes
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:
If we do this early in the method, like Spring, empty/null/imaginary values are going to throw security exceptions, which might be confusing (but also requires less work on our part). For example if the user submits a made up orgId and the call requires the user be in that organization, they’ll get a security error not a validation error. (I’m fine with this).
More custom code for new developers to learn (assuming they know Spring security down to how it is implemented)
Still only in code and does not move permissions to a database.
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 needs. In exactly the same way our worker APIs all start by declaring a study (/v3/studies/{studyId}/*), we could create endpoints for organization administrators (logically under the /v1/organizations/* namespace), etc. Then role-based and entity-based checks could be done in the controller methods.
Take for example the AuthUtils.checkSelfStudyResearcherOrAdmin check for enrollments:
// self POST /v5/studies/enrollments/self // study researcher or admin POST /v3/studies/enrollments
Pros:
Consistent with what we’ve done and thus the least code churn;
Possibly easier for external users to understand;
Possibly easier to secure down the road with Spring Security (using URL-based security rather than method-based security);
Cons
I was going to say “more code” but after looking at the other options, it might still be the best way to proceed;
Some security checks might get tedious. The delete user is a good example:
boolean orgAdminDelete = context.isInRole(ORG_ADMIN) && !account.getRoles().isEmpty() && context.getCallerOrgMembership().equals(account.getOrgMembership()); boolean testDelete = AuthUtils.isSelfResearcherOrAdmin(context.getCallerUserId()) && account.getDataGroups().contains(TEST_USER_GROUP); boolean adminDelete = context.isInRole(ADMIN); if (!orgAdminDelete && !testDelete && !adminDelete) { throw new UnauthorizedException(); }