Skip to content

Commit

Permalink
feat: Added user permission system to restrict access and modificatio…
Browse files Browse the repository at this point in the history
…n of data

closes #769, #956 

Co-authored-by: Schottkyc137 <[email protected]>
Co-authored-by: Sebastian <[email protected]>
  • Loading branch information
3 people authored Apr 14, 2022
1 parent 95ade96 commit d3ff1b7
Show file tree
Hide file tree
Showing 185 changed files with 3,372 additions and 2,044 deletions.
1 change: 1 addition & 0 deletions build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ WORKDIR /app

COPY package*.json ./
COPY patch-webpack.js .
COPY patch-casl.js .
RUN npm ci --no-progress

RUN $(npm bin)/ng version
Expand Down
8 changes: 6 additions & 2 deletions doc/compodoc_sources/concepts/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ On the top level of the config file, there are four different kinds of entries:
1. The main navigation menu (`navigationMenu`)
1. Views defining the UI of each page (`view:<path>`)
1. Lists of select options for dropdown fields (`enum:<category-id>`, including available Note categories, etc.)
1. Entity configuration to define [schemas](entity-schema-system.md) or permissions (`entity:<entity-id>`)
1. Entity configuration to define [schemas](./entity-schema.html (`entity:<entity-id>`)

_also see [User Roles & Permissions](user-roles-and-permissions.html)_


### Navigation Menu
Expand Down Expand Up @@ -110,7 +112,9 @@ The only mandatory field for each view is `"component":` telling the app which c
The component part has to refer to an existing angular component within the app. Components that are valid and may
be used for the view have the `@DynamicComponent` decorator present

The two optional fields of each view are `"config":` and `"requiresAdmin":`. The latter is a boolean telling the app whether the user has to be logged in as an administrator in order to be able the see the component.
The two optional fields of each view are `"config":` and `"permittedUserRoles":`.
`"permittedUserRoles"` expects an array of user role strings.
If one or more roles are specified, only users with these roles are able to see this menu item and visit this page in the app.

What comes within the `"config":` object depends on the component being used.
The Dashboard-Component for example takes as `"widgets:"` an array of subcomponents, where every entry has to have a `"component:"` and may have an own `"config:"` object.
Expand Down
148 changes: 148 additions & 0 deletions doc/compodoc_sources/concepts/permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Permissions
Aam Digital allows to specify permissions to restrict access of certain user roles to the various entity types.
Permissions are defined using the [CASL JSON syntax](https://casl.js.org/v5/en/guide/define-rules#the-shape-of-raw-rule).
The permissions are stored in a [config object](../../classes/Config.html) which is persisted together with other entities in the database.

## Permission structure
As an example, we will define a permission object which allows users with the role `user_app` *not* to *create*, *read*, *update* and *delete* `HealthCheck` entities and *not* *create* and *delete* `School` and `Child` entities.
Besides that, the role is allowed to do everything.
A second role `admin_app` is allowed to do everything.
Additionally, we add a `default` rule which allows each user (independent of role) to read the `Config` entities.
Default rules are prepended to the rules of any user and allow to configure user-agnostic permissions.
The default rules can be overwritten in the role-specific rules.

```JSON
{
"_id": "Config:Permissions",
"data": {
"default": [
{
"subject": "Config",
"action": "read"
}
],
"user_app": [
{
"subject": "all",
"action": "manage"
},
{
"subject": "HealthCheck",
"action": "manage",
"inverted": true
},
{
"subject": [
"School",
"Child"
],
"action": [
"create",
"delete"
],
"inverted": true
}
],
"admin_app": [
{
"subject": "all",
"action": "manage"
}
]
}
}
```
The `_id` property needs to be exactly as displayed here, as there is only one permission object allowed in a single database.
In `data`, the permissions for each of the user role are defined.
In this example we have permissions defined for two roles: `user_app` and `admin_app`.
The permissions for a given role consist of an array of rules.

In case of the `user_app`, we first define that the user is allowed to do everything.
`subject` refers to the type of entity and `all` is a wildcard, that matches any entity.
`action` refers to the operation that is allowed or permitted on the given `subject`.
In this case `manage` is also a wildcard which means *any operation is allowed*.
So the first rule states *any operation is allowed on any entity*.

The second and third rule for `user_app` restrict this through the `"inverted": true` keyword.
While the first rule defined what this role is **allowed** to do, when `"inverted": true` is specified, this rule defines what the role is **not allowed** to do.
This allows us to easily take permissions away from a certain role.
In this case we don't allow users with this role to perform *any* operation on the `HealhCheck` entity and no *create* and *update* on `Child` and `School` entities.
Other possible actions are `read` and `update` following the *CRUD* concept.

The `admin_app` role simpy allows user with this role to do everything, without restrictions.

To learn more about how to define rules, have a look at the [CASL documentation](https://casl.js.org/v5/en/guide/define-rules#rules).

## Implementing components with permissions
This section is about code using permissions to read and edit **entities**.
If you want to change the menu items which are shown in the navigation bar have a look at the *views* section in the [Configuration Guide](./configuration.html).

The permission object is automatically fetched whenever a user logs in.
The permissions disable certain buttons based on the users overall permissions.
This is done in the app through the [DisableEntityOperationDirective](../../directives/DisableEntityOperationDirective.html), which connects certain buttons with their operation.

As an example lets say we have a class variable called `note` which holds an object of the `Note` entity.
We want to create a button which allows to *edit* this note.
In the HTML template we could write the following in order to automatically connect it to the permission system:

```HTML
<button
*appDisabledEntityOperation="{
entity: note,
operation: 'update'
}"
>
Edit Note
</button>
```
This will automatically disable the button if the user is not allowed to *update* this specific note.

To check permissions inside a `*.ts` file, you can inject the `EntityAbility`:

```typescript
import { Note } from "./note";
import { Injectable } from "@angular/core";
import { EntityAbility } from "./permission-types";

@Injectable()
export class SomeService {
constructor(private ability: EntityAbility) {
if (this.ability.can('create', Note)) {
// I have permissions to create notes
const note = new Note();
} else {
// I don't have permissions to create notes
throw Error("Missing permissions");
}
}
}
```
In this example the `EntityAbility` service is used to check whether the currently logged in user is allowed to _create_ new objects of the `Note` entity.
In this case a constructor is provided to check for the permissions,
in other cases it might make more sense to use an instance of an object like `this.ability.can('read', new Note())`.

## Permissions in production
As permissions cannot directly be created and edited from within the app at the moment, you can use the following steps to define permissions for a deployed system:

1. using CouchDB Fauxton GUI to edit database documents directly:
Look for or create the document with `"_id": "Config:Permissions"` and define the permissions as described above.
2. After saving the new permissions document, update the replication backend about the updated permissions:
Visit `https://<your-system-domain>/db/api/` to use the OpenAPI interface for this.
3. There in `Servers` select `/db deployed`
4. Use your CouchDB admin credentials in the `POST /_session` endpoint to get a valid access token.
5. Make a request to the `POST /rules/reload` endpoint. If successful, the response will show the newly fetched rules.
6. In case some users might have **gained** access to documents to which they did not have access before,
also trigger the `POST /clear_local` endpoint.
The `/clear_local` endpoint will ensure that each client re-checks whether new objects are available for synchronization.
This should also be used in case an existing user has gotten a new, more powerful role.
In case a user lost permissions for objects that were already synced, this users local DB will automatically be destroyed and the user has to synchronize all data again.

The roles assigned to users are specified in the user documents in the `_users` database of CouchDB.

## Permissions in development
When trying to test out things with the permissions, the [DemoPermissionGeneratorService](../../Injectable/DemoPermissionGeneratorService.html) can be modified to change the permission object which is created in the demo data.
These changes should not be committed however, as this demo data is also used in the publicly available demo.

The demo data comes with two user: `demo` and `demo-admin`.
The `demo` user has the role `user_app`, the `demo-admin` has the roles `user_app` and `admin_app`.
The permissions of the latter overwrite the permissions of the former.
4 changes: 4 additions & 0 deletions doc/compodoc_sources/summary.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@
"title": "Configuration",
"file": "concepts/configuration.md"
},
{
"title": "User Roles and Permissions",
"file": "concepts/permissions.md"
},
{
"title": "UX Guidelines",
"file": "concepts/ux-guidelines.md"
Expand Down
2 changes: 1 addition & 1 deletion e2e/integration/LinkingChildToSchool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe("Scenario: Linking a child to a school - E2E test", () => {

// get the Add School button and click on it
cy.get(
"app-previous-schools.ng-star-inserted > app-entity-subrecord > .mat-elevation-z1 > .mat-table > thead > .mat-header-row > .cdk-column-actions > .mat-focus-indicator"
"app-previous-schools.ng-star-inserted > app-entity-subrecord > .mat-elevation-z1 > .mat-table > thead > .mat-header-row > .cdk-column-actions > app-disabled-wrapper.ng-star-inserted > .mat-tooltip-trigger > .mat-focus-indicator"
)
.should("be.visible")
.click();
Expand Down
4 changes: 3 additions & 1 deletion e2e/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ Cypress.Commands.add("create", create);
// Overwriting default visit function to wait for index creation
Cypress.Commands.overwrite("visit", (originalFun, url, options) => {
originalFun(url, options);
cy.get("app-search").should("be.visible");
cy.get("app-search", { timeout: 4000 }).should("be.visible");
// wait for demo data generation
cy.wait(4000);
// wait for indexing
cy.contains("button", "Continue in background", { timeout: 10000 }).should(
"not.exist"
);
Expand Down
1 change: 1 addition & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,6 @@ module.exports = function (config) {
autoWatch: true,
browsers: ["Chrome"],
singleRun: false,
retryLimit: 10,
});
};
100 changes: 100 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"e2e": "ng e2e",
"e2e-open": "ng run ndb-core-e2e:cypress-open",
"compodoc": "npx compodoc -c doc/compodoc_sources/.compodocrc.json",
"postinstall": "ngcc && node patch-webpack.js",
"postinstall": "ngcc && node patch-webpack.js && node patch-casl.js",
"docs:json": "compodoc -p ./tsconfig.json -e json -d .",
"storybook": "npm run docs:json && start-storybook -p 6006",
"build-storybook": "npm run docs:json && build-storybook",
Expand All @@ -37,6 +37,8 @@
"@angular/platform-browser-dynamic": "^11.2.12",
"@angular/router": "^11.2.12",
"@angular/service-worker": "^11.2.12",
"@casl/ability": "^5.4.3",
"@casl/angular": "^5.1.1",
"@fortawesome/angular-fontawesome": "^0.8.2",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-regular-svg-icons": "^5.15.2",
Expand Down
Loading

0 comments on commit d3ff1b7

Please sign in to comment.