Skip to content

Commit

Permalink
feat: support for user linked project permissions (#147)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomwwinter authored Jun 17, 2024
1 parent 189badc commit 2c86424
Show file tree
Hide file tree
Showing 7 changed files with 71 additions and 67 deletions.
34 changes: 34 additions & 0 deletions .github/workflows/pr-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: PR Release

on:
pull_request:

jobs:
build_and_publish_branch:
name: Build and publish replication-backend package
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v5
with:
platforms: linux/amd64
context: ./
push: true
tags: ghcr.io/aam-digital/replication-backend:pr-${{ github.event.number }}
64 changes: 3 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ This backend service can be used to filter the replication between a [PouchDB](h
It does this by overriding some of CouchDB`s endpoints where permissions are checked on the transmitted entities.
The permission rules are defined through [CASL](https://casl.js.org/v5/en/).


## Setup
This API functions as a proxy layer between a client (PouchDB) and a standard CouchDB instance.
The backend can either be run as a docker container
Expand All @@ -33,66 +32,9 @@ In case the backend is run through Docker, the args can be provided like this
```
In case the backend is run through npm, the `.env` file can be adjusted.

## Defining permissions
The permissions are also stored in a CouchDB database (default database name `app`).
This can be the same database used for normal application data managed by users.

The structure of the permission document is as follows:
```json
{
"_id": "Config:Permissions",
"data": {
"public": [
{ "subject": "User", "action": "create"}
],
"default": [
{ "subject": "Config", "action": "read" }
],
"role_1": [
{ "subject": "all", "action": "manage"},
...
],
"role_2": [],
...
}
}
```
Important is the exact `_id` as this is how the backend can find this document and that the rules config has the correct structure.

The keys of the `data` object reference to roles that the different users can have and the values are arrays containing valid CASL [JSON rules](https://casl.js.org/v5/en/guide/define-rules#json-objects).
The rules at the value of the `default` key are prepended to other rules that are relevant for a user.
This allows to set user-agnostic rules, e.g. allowing everyone to read the `Config` document.
The default rules can be overwritten by user-specific rules.
The `public` rules are used when a user is **not** authenticated.
This allows to expose a public API, e.g. to integrate a public form.
Subjects refer to the prefixes of the `_id` properties of documents e.g. `_id: Child:123` refers to subject `Child`.
The `all` subject is a wildcard that refers to all documents.

The actions can be:
* `create`
* `read`
* `update`
* `delete`
* `manage` (which is a wildcard for any action)

It is also possible to access information of the user sending the request. E.g.:

```json
{
"subject": "org.couchdb.user",
"action": "update",
"fields": [
"password"
],
"conditions": {
"name": "${user.name}"
}
}
```
This allows users to update the `password` property of their *own* document in the `_users` database.
Another available value is `${user.roles}` which is an array of rules which the user has.
## Defining Permissions

For more information on how to write rules have a look at the [CASL documentation](https://casl.js.org/v5/en/guide/intro).
See our [Developer Documentation](https://aam-digital.github.io/ndb-core/documentation/additional-documentation/concepts/user-roles-and-permissions.html)

## Operation
Besides the CouchDB endpoints, the backend also provides some additional endpoints that are necessary to be used at times.
Expand All @@ -114,4 +56,4 @@ To run and test this project locally:

## Run in a fully local environment with other services
Use the dockerized local environment to run a fully synced app including backend services on your machine:
https://github.com/Aam-Digital/aam-services/tree/main/docs/developer
https://github.com/Aam-Digital/aam-services/tree/main/docs/developer
17 changes: 15 additions & 2 deletions src/auth/guards/jwt-bearer/jwt-bearer.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Injectable } from '@nestjs/common';
import { UserInfo } from '../../../restricted-endpoints/session/user-auth.dto';
import { ConfigService } from '@nestjs/config';
import { AuthModule } from '../../auth.module';
import { CouchdbService } from '../../../couchdb/couchdb.service';
import { firstValueFrom } from 'rxjs';

/**
* Authenticate a user with a foreign bearer JWT using the {@link AuthModule.JWT_PUBLIC_KEY}.
Expand All @@ -13,14 +15,25 @@ export class JwtBearerStrategy extends PassportStrategy(
Strategy,
'jwt-bearer',
) {
constructor(configService: ConfigService) {
constructor(
configService: ConfigService,
private couchdbService: CouchdbService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>(AuthModule.JWT_PUBLIC_KEY),
});
}

async validate(data: any): Promise<UserInfo> {
return new UserInfo(data.username, data['_couchdb.roles']);
const user = await firstValueFrom(
this.couchdbService.get('app', data['username']),
).catch(() => {});

return new UserInfo(
data.username,
data['_couchdb.roles'],
user && user['projects'] ? user['projects'] : [],
);
}
}
17 changes: 15 additions & 2 deletions src/auth/guards/jwt-cookie/jwt-cookie-strategy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { AuthModule } from '../../auth.module';
import { UserInfo } from '../../../restricted-endpoints/session/user-auth.dto';
import { TOKEN_KEY } from '../../cookie/cookie.service';
import { ConfigService } from '@nestjs/config';
import { firstValueFrom } from 'rxjs';
import { CouchdbService } from '../../../couchdb/couchdb.service';

/**
* Authenticate a user using an existing JWT from a cookie in the request.
Expand All @@ -14,7 +16,10 @@ export class JwtCookieStrategy extends PassportStrategy(
Strategy,
'jwt-cookie',
) {
constructor(configService: ConfigService) {
constructor(
configService: ConfigService,
private couchdbService: CouchdbService,
) {
super({
jwtFromRequest: (req) => req?.cookies[TOKEN_KEY],
ignoreExpiration: false,
Expand All @@ -23,6 +28,14 @@ export class JwtCookieStrategy extends PassportStrategy(
}

async validate(data: any): Promise<UserInfo> {
return new UserInfo(data.name, data.sub);
const user = await firstValueFrom(
this.couchdbService.get('app', data['username']),
).catch(() => {});

return new UserInfo(
data.name,
data.sub,
user && user['projects'] ? user['projects'] : [],
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,10 @@ describe('DocumentController', () => {
});

it('should throw exception if the update permission is not given', () => {
const otherUser = {
const otherUser: UserInfo = {
name: 'anotherUser',
roles: [],
projects: [],
};
mockAbility([
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('ChangesController', () => {
childDoc,
deletedChildDoc,
]);
const user: UserInfo = { name: 'username', roles: [] };
const user: UserInfo = { name: 'username', roles: [], projects: [] };
const mockCouchdbService = { get: () => undefined } as CouchdbService;
const getSpy = jest.spyOn(mockCouchdbService, 'get');
const mockRulesService = {
Expand Down
1 change: 1 addition & 0 deletions src/restricted-endpoints/session/user-auth.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class UserInfo {
constructor(
public name: string,
public roles: string[],
public projects: string[] = [],
) {}
}

Expand Down

0 comments on commit 2c86424

Please sign in to comment.