Skip to content

Commit

Permalink
- Refactor policies to use new base_allow effectively.
Browse files Browse the repository at this point in the history
- Add in additional policy rule helpers/layers to simplify rules required per model, etc.
- Add performance debug log for AuthorizeFilter (to track how long polar/filter generation takes).
- Update policies to use new `PermissionHelper` extension class to look up permission IDs (globally) rather than directly querying through ContentType and Permission models on each query, which adds several extra joins. These can be cached/optimized further as a separate concern.
- Add `LOGGING` setting to allow seeing debug logs.
- Update README with more info.
  • Loading branch information
devmonkey22 committed Jan 27, 2021
1 parent 67c85a5 commit 05e7e1d
Show file tree
Hide file tree
Showing 13 changed files with 381 additions and 102 deletions.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ Or we could use a hybrid approach with permissions driving access to primary mod

1. Global User/Group permissions
- For example, if a user is assigned the `casemgmt.view_client` permission globally, they can see any client.
- As future work (homework), you could create models to associate users or groups to a role globally, then create a custom Django auth backend that loads those with the user. That way, roles can play a bigger part of the global permission sources.
2. `CaseloadRoles`
- If a user is assigned to a caseload role, the user will inherit the permissions of that role, for any linked client/casetype.
- If a user (or one of their groups) is assigned to a caseload role, the user will inherit the permissions of that role, for any linked client/casetype.


There are several models that drive the authorization policies in this example.
Expand All @@ -126,8 +127,10 @@ There are several models that drive the authorization policies in this example.
## Secondary Models

1. `Document`
- Document does not have permissions required of its own. It derives it's permissions from its related `Client` and `CaseType`. For example, the user must have the `casemgmt.view_document` permission through their caseload role of a caseload in which the client and document's `CaseType` are linked (Caseload Scope) or globally (not common in a case management system).
- Users are not assigned to documents directly. It derives it's role sources from its related `Client` and `CaseType`, and more accurately, from their mutual caseloads that a user is a member of. For example, the user must have the `casemgmt.view_document` permission through their caseload role of a caseload in which the client and document template's `CaseType` are linked (Caseload Scope) or globally (not common in a case management system).

2. `WkcmpEligibilityData`
- This is an example of an extension type model, where we may want policies specific to this model, but in general, want to use the related document (and thus client/casetype/caseloads) to find scoped roles for the user.


## Demo Users
Expand Down Expand Up @@ -173,10 +176,17 @@ library is installed and configured to introspect/debug the API requests, includ
with authorization checks. This helps show `django-oso`'s partial evaluation support with Django QuerySets.


## Known Issues
## Known Issues/TODO

Where to begin... this is still a work-in-progress and very non-functional while developing the policies.
Where to begin... this is still a work-in-progress...

1. As non-admin user, there are several errors in the Partial evaluation of `Client` and `Document` models. Other APIs may return no data but should have data. See notes in `casemgmt/policy/models.polar` regarding `DocumentTemplate` as a start.
1. Most APIs do not accept POSTing data successfully. The serializers are incomplete except for reading so far due to nested relations, etc.

2. Most APIs do not accept POSTing data successfully. The serializers are incomplete except for reading so far due to nested relations, etc.
2. Performance of policy evaluation has room for improvement. For example, requests to `/api/documents` with `alan-wkcmp` user took about 0.461 seconds to evaluate and prepare the QuerySet filter. The
underlying SQL query took ~1.25ms.

1. The SQL generated from the filtered QuerySet is also non-optimal (lots of nested/repetitive EXISTS clauses, but functionally correct, which was the goal of the Oso team to start.

For example, the `user_in_role(user: casemgmt::User, role, resource: casemgmt::Caseload)` rule could ideally generate one set of joins to the CaseloadRoles table, then conditionally check user vs group, or something like that.
Progress is being made all the time on these fronts.
68 changes: 35 additions & 33 deletions casemgmt/policy/allow.polar
Original file line number Diff line number Diff line change
@@ -1,47 +1,49 @@
# ALLOW RULES --- entrypoint for all authorization decisions
### ########################################################
### ALLOW RULES --- entrypoint for all authorization decisions
### ########################################################

## Defer to models
### Is user allowed to perform "action" on resource?
###
### The ``action`` may be a standard "view", "add", "change", "delete" model permission or a custom
### action that matches "{app_label}.{action}_{model_name}".
###

### check for RBAC rule
allow(actor, action: String, resource) if
rbac_allow(actor, action, resource);
### Lookup action -> permission, then check allow
action_to_permission(action, resource, perm) and
allow(actor, perm, resource);

### check for global rule
allow(actor, action: String, resource) if
global_allow(actor, action, resource);
# Each model should define their own allow, and can use `base_allow` and/or any other custom logic.
# We could use this generic rule to automatically call `base_allow`, then I feel like it's more difficult to totally
# override all policies on a per-model basis.
#allow(actor, perm: PermissionInfo, resource) if
# base_allow(actor, perm, resource);







### ########################################################
### BASE ALLOW POLICIES
### ########################################################

## Delegate by resource

### Delegate
### User has access if allowed to access resource with given permission (using RBAC with potentially resource-scoped (non-global) roles)
base_allow(actor, perm: PermissionInfo, resource) if
rbac_allow(actor, perm, resource);

allow(user: casemgmt::User, action, client: casemgmt::Client) if
# NB: All relations should be expressed over the resource on the RHS
# to avoid using querysets
caseload in client.caseloads and
caseload matches casemgmt::Caseload and
allow(user, action, caseload);

### User has access if has permission through direct (global) permission assignment
base_allow(actor, perm: PermissionInfo, resource) if
global_allow(actor, perm, resource);

allow(user: casemgmt::User, action, case_type: casemgmt::CaseType) if
# NB: All relations should be expressed over the resource on the RHS
# to avoid using querysets
caseload in case_type.caseloads and
caseload matches casemgmt::Caseload and
allow(user, action, caseload);

# Superusers can do anything, regardless of permission/action
base_allow(actor, _action, _resource) if
actor.is_superuser;

allow(user: casemgmt::User, action, template: casemgmt::DocumentTemplate) if
caseload in template.case_type.caseloads and
caseload matches casemgmt::Caseload and
allow(user, action, caseload);

allow(user: casemgmt::User, action, document: casemgmt::Document) if
caseload in document.template.case_type.caseloads and
caseload in document.client.caseloads and
caseload matches casemgmt::Caseload and
allow(user, action, caseload);


# Allow access if user has same action rights on related document
allow(user: casemgmt::User, action, elig_data: casemgmt::WkcmpEligibilityData) if
allow(user, action, elig_data.document);
25 changes: 4 additions & 21 deletions casemgmt/policy/caseloads.polar
Original file line number Diff line number Diff line change
@@ -1,28 +1,11 @@
## Model-specific rules for caseload roles.
## (Would be autogenerated by oso)

# User in caseload role (directly) or User in caseload role (as a group member)
user_in_role(user: casemgmt::User, role, resource: casemgmt::Caseload) if
## Note: we are using the related name to go from resource to the m2m model
## This should be preferred so all attributes at written over the resource partial
caseload_role in resource.caseload_roles and
caseload_role.user = user and
role = caseload_role.role;

### Maps action to `casemgmt.{action}_caseload`
### e.g. "view" -> "casemgmt.view_caseload"
action_to_permission(action, _: casemgmt::Caseload, perm) if
perm = "_".join([action, "caseload"]);

# Direct role permission assignments
role_allow(role, action, resource) if
action_to_permission(action, resource, permission) and
role_perm in role.permissions and
role_perm.codename = permission and
role_perm.content_type.app_label = "casemgmt" and
role_perm.content_type.model = "caseload";

# TODO: Revisit
# inherits_role(role: CaseloadRole, inherited_role) if
# caseload_role_order(role_order) and
# inherits_role_helper(role.name, inherited_role_name, role_order) and
# inherited_role = new CaseloadRole(name: inherited_role_name, caseload: role.caseload);
role = caseload_role.role and
(caseload_role.user = user or
user in caseload_role.group.user);
20 changes: 8 additions & 12 deletions casemgmt/policy/global.polar
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
## Global permissions assigned to users

### Allow superusers/staff to view everything
allow(user: casemgmt::User, _action, _resource) if
user.is_staff;
#global_allow(user: casemgmt::User, _action, _resource) if
# user.is_superuser;

### User has access if has permission through direct permission assignment
global_allow(user: casemgmt::User, action: String, _resource: casemgmt::Caseload) if
user.has_perm("".join(["casemgmt.", action, "_caseload"]));
### User has access if has permission through direct (global) permission assignment
global_allow(user, perm: String, _resource) if
user_has_perm(user, perm);

global_allow(user: casemgmt::User, action: String, _resource: casemgmt::Document) if
user.has_perm("".join(["casemgmt.", action, "_document"]));

global_allow(user: casemgmt::User, action: String, _resource: casemgmt::Client) if
user.has_perm("".join(["casemgmt.", action, "_client"]));

global_allow(user: casemgmt::User, action: String, _resource: casemgmt::CaseType) if
user.has_perm("".join(["casemgmt.", action, "_casetype"]));
### User has access if has permission through direct (global) permission assignment
global_allow(user, perm: PermissionInfo, _resource) if
user_has_perm(user, perm.full_name);
20 changes: 20 additions & 0 deletions casemgmt/policy/helpers.polar
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
### ########################################################
### HELPER RULES
### ########################################################

### Maps action to permission info { "id": int, "full_name": "{app_label}.{action}_{model_name}", ... }
### e.g. "view" -> "casemgmt.view_client"
action_to_permission(action: String, resource, perm) if
resource_to_class(resource, resource_cls) and
perm = PermissionHelper.get_permission_info_for_model_action(action, resource_cls);


# If action is already PermissionInfo, use it
action_to_permission(action: PermissionInfo, _resource, perm) if
perm = action;



### Does the user have the given global role (permission)?
user_has_perm(user, perm: String) if
user.has_perm(perm);
94 changes: 94 additions & 0 deletions casemgmt/policy/models.polar
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
### ########################################################
### MODEL ALLOW POLICIES
###
### If a model wants to perform any additional ABAC type checks, they should be able to add those into the `allow` rules
### below, or defer into a lower-level rule like `role_allow` or something.
### ########################################################



allow(user: casemgmt::User, perm: PermissionInfo, resource: casemgmt::Client) if
base_allow(user, perm, resource);


allow(user: casemgmt::User, perm: PermissionInfo, resource: casemgmt::CaseType) if
base_allow(user, perm, resource);


allow(user: casemgmt::User, perm: PermissionInfo, resource: casemgmt::DocumentTemplate) if
base_allow(user, perm, resource);


allow(user: casemgmt::User, perm: PermissionInfo, resource: casemgmt::Document) if
base_allow(user, perm, resource);


allow(user: casemgmt::User, perm: PermissionInfo, resource: casemgmt::Caseload) if
base_allow(user, perm, resource);


# Allow access if user has permission using related document as a potential role source
# The permissions should still be `casemgmt.{action}_wkcmpeligibilitydata`, etc so role needs those permissions still.
allow(user: casemgmt::User, perm: PermissionInfo, resource: casemgmt::WkcmpEligibilityData) if
allow(user, perm, resource.document);


# Alternately, for WkcmpEligibilityData, we could have just called `base_allow`, and defined the
# `resource_role_applies_to(elig_data: casemgmt::WkcmpEligibilityData, resource)` rule below. The above `allow`
# approach allows us to reuse/call document-specific policies more directly.
#
# allow(user: casemgmt::User, perm: PermissionInfo, resource: casemgmt::WkcmpEligibilityData) if
# base_allow(user, perm, resource);








### ####################################################################################################
### RELATIONSHIP RULES/HELPERS

# TODO: If we had a way to use a function like `type(resource)` to pull the partial's type automatically,
# these rules wouldn't be needed, or could become generic like:
# resource_to_class(resource, resource_cls) if
# resource_cls = type(resource);
#
# Define the link between an instance of a resource (even a Partial), and the real type
resource_to_class(_resource: casemgmt::Caseload, casemgmt::Caseload);
resource_to_class(_resource: casemgmt::Client, casemgmt::Client);
resource_to_class(_resource: casemgmt::Document, casemgmt::Document);
resource_to_class(_resource: casemgmt::DocumentTemplate, casemgmt::DocumentTemplate);
resource_to_class(_resource: casemgmt::CaseType, casemgmt::CaseType);

# Caseloads are their own role sources/relation
resource_role_applies_to(caseload: casemgmt::Caseload, caseload);


# Clients are associated to caseloads (and their roles)
resource_role_applies_to(client: casemgmt::Client, caseload) if
caseload in client.caseloads;


# CaseTypes are associated to caseloads (and their member roles)
resource_role_applies_to(case_type: casemgmt::CaseType, caseload) if
caseload in case_type.caseloads;


# DocumentTemplates are associated to their case type's caseloads (or other things if CaseType defines it)
resource_role_applies_to(template: casemgmt::DocumentTemplate, resource) if
#resource in template.case_type.caseloads;
resource_role_applies_to(template.case_type, resource);



# Documents are associated to caseloads (and their roles) when it's client and case_type are in the same caseload
resource_role_applies_to(resource: casemgmt::Document, caseload) if
caseload in resource.client.caseloads and
caseload in resource.template.case_type.caseloads;


# Eligibility Data is associated to it's document's role relations (ie: should be a caseload)
#resource_role_applies_to(elig_data: casemgmt::WkcmpEligibilityData, resource) if
# resource_role_applies_to(elig_data.document, resource);
59 changes: 34 additions & 25 deletions casemgmt/policy/roles.polar
Original file line number Diff line number Diff line change
@@ -1,38 +1,47 @@
# RBAC BASE POLICY
### ########################################################
### RBAC BASE POLICY
### ########################################################

## Top-level RBAC allow rule

### The association between the resource roles and the requested resource is outsourced from the rbac_allow
rbac_allow(user, action, resource) if
# First, check whether user has a direct role
# or a role from an associated resource
rbac_allow(user, perm: PermissionInfo, resource) if
# First, check whether user has a direct role or a role from an associated resource
resource_role_applies_to(resource, role_resource) and
user_in_role(user, role, role_resource) and
role_allow(role, action, resource);
role_allow(role, perm, resource);

# RESOURCE-ROLE RELATIONSHIPS

## These rules allow roles to apply to resources other than those that they are scoped to.
## The most common example of this is nested resources, e.g. Repository roles should apply to the Issues
## nested in that repository.

### A resource's roles applies to itself
resource_role_applies_to(role_resource, role_resource);

# ROLE-ROLE RELATIONSHIPS
### #############################################################################################
### RESOURCE TO TYPE RELATIONSHIPS
### #############################################################################################

## Role Hierarchies
### Each model (resource) that needs to be able to use `action_to_permission()` to convert action to permission
### must define a `resource_to_class()` rule. This is needed until there is support for a function like `type(var)`
### to do this automatically.

### Grant a role permissions that it inherits from a more junior role
role_allow(role, action, resource) if
inherits_role(role, inherited_role) and
role_allow(inherited_role, action, resource);
#resource_to_class(_resource: label::Name, label::Name)

# TODO: Revisit if this works
# ### Helper to determine relative order or roles in a list
# inherits_role_helper(role, inherited_role, role_order) if
# ([first, *rest] = role_order and
# role = first and
# inherited_role in rest) or
# ([first, *rest] = role_order and
# inherits_role_helper(role, inherited_role, rest));


### ########################################################
### ROLE ALLOW CHECKS
### ########################################################


### Direct/indirect `Role` permission check
### If role has given permission by ID
role_allow(role, perm: PermissionInfo, _resource) if
role_perm in role.permissions and
role_perm.id = perm.id;


### Direct/indirect `Role` permission assignments
### If role has given permission name
role_allow(role, perm: String, _resource) if
# Lookup permission info first
perm_info = PermissionHelper.get_permission_info(perm) and
role_perm in role.permissions and
role_perm.id = perm_info.id;
3 changes: 3 additions & 0 deletions casemgmt_example/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Register Oso extensions, etc
from casemgmt_example import auth
auth.register_extensions()
9 changes: 9 additions & 0 deletions casemgmt_example/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django_oso import Oso

from . import oso_extensions


def register_extensions():
# Register extensions/types into Oso
Oso.register_constant(oso_extensions.PermissionHelpers, name="PermissionHelper")
Oso.register_class(oso_extensions.PermissionInfo, name="PermissionInfo")
Loading

0 comments on commit 05e7e1d

Please sign in to comment.