Overview

The purpose of this spike is to investigate how to implement static permission migration.  That is, how to handle when permissions & permission sets defined in module descriptors are renamed, removed, or updated (e.g. to include new permissions) during a module upgrade.  This does not cover the case of user-defined module permission sets.

TL;DR

The following statements apply to permissions defined in module descriptors.  NOT user-defined permissions.

JIRA

Spikes:

Implementation Stories:

Changes to OKAPI

ModuleDescriptor

During module upgrade

  1. OKAPI Makes a call to mod-permissions' _tenantPermissions endpoint, providing a lists of permissions (details below).

Upon mod-permissions Install/Upgrade

  1. Send all permissions to mod-permissions to "refresh" them.  See "Determination of which permissions are new/updated/removed" below.  

Changes to mod-permissions

Tenant Permissions API

_tenantPermissions is updated to determine which permissions are new, updated, removed.  That information is that used to update not only the permissions table but also the user-permissions table.   So if a user has a permission which is being renamed, updated with fewer/additional sub-permissions, or removed, the user's permissions are updated accordingly.  

Determination of which permissions are new/updated/removed

The first time mod-permissions that implements v2.0 of the _tenantPermissions interface is enabled, OKAPI will call that API with the permissions defined in each of the enabled module descriptors.   Special handling exists in mod-permissions that will allow it to add the necessary module context, enabling it to detect/determine which permissions are new/removed/updated in subsequent _tenantPermissions calls.  

OkapiPermissionSet.json Schema

See https://github.com/folio-org/mod-permissions/blob/master/ramls/okapiPermissionSet.json

This is the payload of the _tenantPermissions endpoint remains the same, aside from "moduleId" now being a required field.

Example:

{
  "moduleId": "mod-foo-2.0.0",
  "perms": [ <list of permission objects> ]
}

Permission.json Schema

See https://github.com/folio-org/mod-permissions/blob/master/ramls/permission.json

This is the stored permission schema.  It needs to be expanded to include some additional context about who/what defined it.

{
  ...
  "moduleName": "mod-foo",
  "moduleVersion": "1.2.3" 
}

Or, for a user-defined permission these fields would simply be omitted.  Permissions created by the _tenantPermissions API will always have moduleName populated, which Permissions created by the Perms API will never populate moduleName or moduleVersion. 

Removal of Static Module Permissions

In order to support the ability to downgrade a module, the decision has been made to implement a soft delete when permissions are removed from a module descriptor.  Here, the OKAPI part stays the same, but on the mod-permissions side, the _tenantPermissions API will not actually delete permissions, nor will it remove the permission assignments.  Instead, the permission will be marked as inactive.  A separate process for cleaning up inactive permissions will be introduced.

Inactive permissions should be filtered out of responses from the various perms APIs:

GET /perms/permissions

NOTE:  In the case where we query for a permission by name... e.g. GET /perms/permissions?query=permissionName=some.inactive.permission ?  We automatically add a clause to not return inactive permissions, so this would return an empty result set, even though the permission does exist in the system.  Or course, if includeInactive=true the results would include the permission specified.

GET /perms/users/<id>/permissions (and similar GET /perms/users/<id>)

GET /perms/permissions/<id> 

Leave that as-is. 

Notes

Several ideas have been briefly discussed for extending this API's functionality

Another idea which was discussed (and considered an implementation detail) was to store inactive permissions off to the side, e.g. in a separate table.  The idea being that it might help simplify/minimize the amount of filtering needed, specifically we wouldn't need to add clauses to provided CQL queries to omit inactive permissions.  We would however still need to filter out permissions from the subPermissions/childOf fields.

Schema Changes

Permission.json Schema

See https://github.com/folio-org/mod-permissions/blob/master/ramls/permission.json

This is the stored permission schema.  It needs to be expanded to include some additional context about who/what defined it.

API Changes

A new API will be introduced for removing inactive permissions

Interface: permissions

Endpoint: POST /perms/permissions/purge-inactive

Request: No body

Responses:

Required Permissions: perms.permissions.purge-inactive.post

Behavior:  

NOTE:  A call to GET /perms/permissions?query=inactive==true could be used to retrieve the list of permissions that will be purged prior to using this new endpoint.

Permission Name Conflict Resolution

explores options for handling conflicting permission names.  Three potential solutions were considered:

(error) Fail the install/upgrade

This it probably the safest thing to do, but is undesirable given separate conversations about reducing the number of things that would cause an upgrade to completely stop.

(error) Overwrite the existing permission

While this is the current behavior, it constitutes a security vulnerability in that it could lead to permission escalation.  For this reason, this solution is essentially off the table.

(tick) Rename the existing user-defined permission

For example by adding a numeric suffix.  Permissions defined by the system (modules) take precedence.

  1. Renaming a user-defined permission and replacing it with a system-defined permission could get confusing.
  2. This might be the least disruptive solution being considered.

(error) Prefix/scope the permissions

Here, permissions would be scoped.  Module-defined permissions would get a prefix of something like "system", "static" or "module", whereas permissions defined via the perms API would be scoped with something different, such as "custom", "dynamic" or "user-defined".  

The main concern with this solution is that it's disruptive and more complicated than the other solutions.

NOTE:  There are currently no restrictions on display name uniqueness... So this means that multiple permissions could have the same "display name", but behind the scenes one might be defined by a module and another might be user-defined.

End-to-End Examples

Example 1 - New, modified, and removed permissions

Given the following permissions:

{
  "permissionName": "foo",
  "subPermissions": [ ],
  "grantedTo": [ "c183b277-9d16-4687-b22a-2f181529d018" ],
  ...
}, {
  "permissionName": "bar",
  "subPermissions": [ "bar.get", "bar.post", "bar.delete" ],
  "grantedTo": [ "c183b277-9d16-4687-b22a-2f181529d018" ],
  ...
}, {
  "permissionName": "baz",
  "subPermissions": [ ],
  "grantedTo": [ "c183b277-9d16-4687-b22a-2f181529d018" ],
  ...
}

And following user:

{
  "id": "c183b277-9d16-4687-b22a-2f181529d018",
  "username": "bob",
  "permissions": [ "foo", "bar", "baz", "bar.get", "bar.post", "bar.delete" ]
  ...
}

If _tenantPermissions was called with:

{
  "ModuleId": "mod-foo-2.0.0", 
  "fromModuleId": "mod-foo-1.2.3",
  "toPerms": [
    {
      "permissionName": "zip",
      ...
    }, {
      "permissionName": "zap",
      "subPermissions": [ "zap.get", "zap.post", "zap.delete" ],
      ...
    }, {
      "permissionName": "foo.config",
      "renamedFrom": [ "foo" ],
      ...
    }, {
      "permissionName": "bar",
      "subPermissions": [ "bar.get", "bar.put", "bar.post", "bar.delete" ],
      ...
    }
  ],
  "fromPerms": [
    {
      "permissionName": "foo",
      ...
    }, {
      "permissionName": "bar",
      "subPermissions": [ "bar.get", "bar.post", "bar.delete" ],
      ...
    }, {
      "permissionName": "baz",
      ...
    }
  ]
}

We'd end up with:

{
  "permissionName": "foo.config",
  "subPermissions": [ ],
  "grantedTo": [ "c183b277-9d16-4687-b22a-2f181529d018" ],
  ...
}, {
  "permissionName": "bar",
  "subPermissions": [ "bar.get", "bar.put", "bar.post", "bar.delete" ],
  "grantedTo": [ "c183b277-9d16-4687-b22a-2f181529d018" ],
  ...
}, {
  "permissionName": "zip",
  "subPermissions": [ ],
  "grantedTo": [ "c183b277-9d16-4687-b22a-2f181529d018" ],
  ...
}, {
  "permissionName": "zap",
  "subPermissions": [ "zap.get", "zap.post", "zap.delete" ],
  "grantedTo": [ "c183b277-9d16-4687-b22a-2f181529d018" ],
  ...
}

And

{
  "id": "c183b277-9d16-4687-b22a-2f181529d018",
  "username": "bob",
  "permissions": [ "foo.config", "bar", "zip", "zap", "bar.get", "bar.put", "bar.post", "bar.delete", "zap.get", "zap.post", "zap.delete" ]
  ...
}

Example 2 - Sub-permission provided multiple times

Given the following permissions:

[ {
  "permissionName": "a",
  "subPermissions": [ "x" ]
}, {
  "permissionName": "b",
  "subPermissions": [ "x" ]
} ]

And following user:

{
  "username": "foo",
  "permissions": [ "a", "b", "x" ]
}

When "b" changes from "x" to "y"

[ {
  "permissionName": "a",
  "subPermissions": [ "x" ]
}, {
  "permissionName": "b",
  "subPermissions": [ "y" ]
} ]

take care that we don't delete "x" that is still a provided via "a":

{
  "username": "foo",
  "permissions": [ "a", "b", "x", "y" ]
}

There should be a unit test for this case.

Open Issues

Decisions


Status

Stakeholders
OutcomeStatic permission removal will use a soft delete to accommodate module downgrades
Created date

 

Owner