Skip to content

Instantly share code, notes, and snippets.

@mattdrees
Last active December 17, 2015 01:59
Show Gist options
  • Save mattdrees/5532475 to your computer and use it in GitHub Desktop.
Save mattdrees/5532475 to your computer and use it in GitHub Desktop.

Authorization Mini-Framework Proposal

The problems I want to solve here:

  • It should be really really easy for business code check whether the current user is able to something.
  • That "something" should be expressed in a type-safe way, where possible. It should also be expressed in a way that allows different levels of granularity, such that broadly-scoped permission rule can allow a user access to a very narrowly-scoped 'thing'.
  • The rules that explain who has permission to what should be able to be expressed in a way that is pretty readable.
  • Permission checks should be easily disabled for tests and such.
  • It should be pretty easy to implement.

Permissions are expressed as a combination of a target and an action. An action is any object, and represents the type of thing the user is trying to do. A target is an ordered list of objects that describes upon what the user is trying to act. The first element in the list represents a 'domain', and each successive element represents a 'subdomain' of the previous element. The semantics of a domain aren't precisely defined by the framework.

Business code will check for permissions according to the following hopefully-self-explanatory examples:

Check 1

// the framework would provide an AuthorizationService available for injection, to 
// be used as a starting point for auth checks:
@Inject AuthorizationService authorizationService;


// in some business method:
DesignationNumber desig = ...
authorizationService
  .target(DesignationEntity.class, desig)   // [1]
  .action(new UpdateSecureStatusAction())  // [2]
  .checkAuthorization();  // [3]
  1. target() takes a variable number of arguments, which together form the target list for this check
  2. UpdateSecureStatusAction is a custom class representing this specific action
  3. throws an exception if the user isn't authorized to do this

Check 2

Set<DesignationNumber> designations = ...
authorizationService
  .target(
    DesignationEntity.class, 
    new DesignationSet(designations),  // [1]
    new ColumnSet("secureStartDate",  "secureEndDate"))
  .action(StandardAction.UPDATE)  // [2]
  .checkAuthorization();
  1. DesignationSet is a simple wrapper class for Set; it's needed since I'm not (yet?) handling generics
  2. the framework would have a built-in StandardAction enum

Check 3

Set<DesignationNumber> designations = ...
authorizationService
  .target(
    DesignationEntity.class, 
    new DesignationSet(designations), 
    new ColumnSet("description")) 
  .action(StandardAction.READ).and(StandardAction.UPDATE) // [1]
  .isAuthorized();  // [2]
  1. you can check multiple actions at the same time for the same target
  2. return false if the user isn't authorized to do this

The rules that determine who has access to what will be expressed as a set of methods in a single class. When the framework is performing a permission check, it will see which rules apply by using java type assignability rules for both the action and the elements of the target list. However, if the rule target list (of size N) is shorter than the check target list, it will only consider the first N elements in the check list. I'll explain this more below.

Some example permission rules follow. (These would all exist in a single class that is known to the framework; I haven't figured out yet how the framework will find this class.)

Rule 1

@PermissionRule
boolean summerProjectToolCanUpdateSecureStatusForSummerProjectDesignations(
  @Action UpdateSecureStatusAction action,  
   // the non-@Action method parameters form the target:
  Class<?> entityClass,
  DesignationSet designations)
{
  if (entityClass != DesignationEntity.class)
    return false;
  Optional<String> systemName = authenticationService.getSystemName();
  return 
    systemName.isPresent() &&
    systemName.get().equals("summer-project-site") &&
    allDesignationsAreSpDesignations(designations));
}

Rule 1 is not queried by any of Check 1, 2, or 3, because none of those checks have an action that matches UpdateSecureStatusAction and a target that matches [Class<?>, DesignationSet]. However, this rule will come into play in another example below.

Rule 2

@PermissionRule
@ForStandardActions({StandardAction.UPDATE, StandardAction.READ}) // [1]
boolean summerProjectToolCanReadAndUpdateSecureDatesForAllDesignations(
  Class<?> entityClass, 
  DesignationSet designationSet, 
  ColumnSet columns)

{
  if (entityClass != DesignationEntity.class)
    return false;
  Optional<String> systemName = authenticationService.getSystemName();
  return 
    systemName.isPresent() &&
    systemName.get().equals("summer-project-site") &&
    ImmutableSet.of("secureStartDate",  "secureEndDate").contains(columns.asSet()));
}
  1. shortcut to specify the rule only applies to some of the built-in StandardActions

Rule 2 is queried by Check 2 and Check 3, whose targets match [Class<?>, DesignationSet, ColumnSet] and whose actions match StandardAction. In addition, because of the @ForStandardActions annotation, the framework will only query this rule if the specific enum values in the checked action (UPDATE in Check 2 and both READ and UPDATE in Check 3) are one of the values declared in @ForStandardActions.

Rule 3

@PermissionRule

@ForStandardActions(StandardAction.READ)
boolean staffCanReadTheirOwnDesignationInformation(
  Class<?> entityClass, 
  DesignationSet designationSet 
)

{
  if (entityClass != DesignationEntity.class)
    return false;
  if (designationSet.asSet().size() != 1 )
    return false;
  DesignationNumber queriedDesignation = Iterables.getOnlyElement(designationSet.asSet());
  Optional<DesignationNumber> usersDesignation = authenticationService.getDesignationNumber();
  return 
    usersDesignation.isPresent() &&
    queriedDesignation.equals(userDesignation.get());
}

Rule 3 is queried only by the first part of Check 3, whose target matches [Class<?>, DesignationSet], whose action matches StandardAction, and whose StandardAction enum value (READ) is one of the actions declared in this @ForStandardActions annotation (READ). Check 3 also checks for UPDATE permissions in addition to READ permissions, but this is treated as a separate check. Rule 3 will not be queried for the UPDATE permission check.

Note that when evaluating Check 3, the size of the check target list is 3, and the size of the rule target list is 2. So only the first two elements of the check target list (the Class<?> element and the DesignationSet element) are compared with the rule target list for type assignability. This enables Rule 3 to apply to any ColumnSet.

Rule 4

// permission rules can recursively check permissions
@PermissionRule
boolean ifYouCanActOnADesignationSetThenYouCanActOnAnIndividualDesignation(
  @Action Object action,
  Class<?> entityClass, 
  DesignationNumber designationNumber)
{
  // Check 4
  return authorizationService
    .target(entityClass, new DesignationSet(designationNumber))
    .action(action)
    .isAuthorized();
}

Rule 4 is queried only by Check 1, whose target matches [Class<?>, DesignationSet] and whose action matches Object
When this rule is queried, its internal check (Check 4) will cause the framework to query Rule 1, because the target matches [Class<?>, DesignationSet] and the action matches UpdateSecureStatusAction.

This enables rules to employ other rules pretty easily. For example, this enables you to easily express that if a user can UPDATE something, then he can also READ it.

Rule 5

@PermissionRule
boolean mattCanDoAnythingHeWantsTo(@Action Object action)
{
  Optional<SsoGuid> mattsGuid = authenticationService.getSsoGuid();
  return 
    mattsGuid.isPresent() &&
    mattsGuid.get().equals(new SsoGuid("BB20A5DB-D31E-65B5-3629-E24504A00942"));
}

Rule 5 is queried by all of Check 1, 2, and 3, since their targets match the target [ ] and the action Object. In this case the rule's target list is empty, which matches all possible targets. This is just a special case of the different-size-target-list rule.

@mattdrees
Copy link
Author

Robby and I talked a little bit in-person. He'd rather use some kind of generic TypeToken-ish generic wrapper that looks a little clunky than have to write a bunch of wrapper classes.

I'm not sure I'd make the same tradeoff. It'd be more work to implement in the framework, especially if we start allowing rules like:

@PermissionRule
public <T> void ifYouCanActOnASetOfOneElementThenYouCanActOnAnElement(
  @Action Object action,
  TypedElement<T> element) // typesafe wrapper class that encodes what T really is
{
    return authorizationService
      .action(action)
      .target(
        new TypedElementOf(Set.class)
          .parameterizedBy(element.getType())
          .containing(ImmutableSet.of(element.getValue()))) // makes a new typesafe wrapper, probably
      .isAuthorized();
}

So, I think I won't try to tackle handling generics for a while.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment