Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: casl RBAC plugin #617

Merged
merged 19 commits into from
Jun 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@ common/autoinstallers/*/.npmrc
.heft
*.tgz
.pnpm-store

.DS_Store
2 changes: 1 addition & 1 deletion .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ blocks:
"plugin-knex", "plugin-logger", "plugin-opentracing", "plugin-prometheus",
"plugin-redis-cluster", "plugin-redis-core", "plugin-redis-sentinel",
"plugin-router", "plugin-router-amqp", "plugin-router-hapi",
"plugin-router-socketio", "plugin-socketio", "plugin-validator", "utils"]
"plugin-router-socketio", "plugin-socketio", "plugin-validator", "plugin-casl", "utils"]

- name: release
dependencies: ["tests"]
Expand Down
22 changes: 22 additions & 0 deletions packages/plugin-casl/.mdeprc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const { basename } = require('path')
const dir = basename(__dirname)

module.exports = {
...require('../../.mdeprc.js'),
"nycCoverage": false,
"coverage": false,
"auto_compose": true,
"node": "16",
"parallel": 3,
"test_framework": "jest --config ./jest.config.js --runTestsByPath --runInBand",
"tests": "__tests__/suites/*.spec.ts",
root: `/src/packages/${dir}/node_modules/.bin`,
services: [
'rabbitmq',
],
extras: {
tester: {
working_dir: `/src/packages/${dir}`,
}
}
}
1 change: 1 addition & 0 deletions packages/plugin-casl/.release-it.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('../../.release-it.cjs')
146 changes: 146 additions & 0 deletions packages/plugin-casl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Microfleet plugin CASL

Adds RBAC/PBAC feature using [CASL](https://casl.js.org/v5/en/).

## Install

```shell
> pnpm add @microfleet/plugin-casl
```

## Configuration

To make use of the plugin, adjust Microfleet configuration in the following way:

```js
// config/app.js

exports.plugins = [
// ...
'casl'
]

// See ./src/rbac.ts
exports.rbac = {
abilities: {
'admin-ability': [
{
subject: 'all',
action: 'manage',
}
],
'user-ability': [
{
subject: 'user:profile',
action: 'manage'
}
]
},
detectSubjectType: (obj) => obj.type
}
```

## Usage

The `plugin-casl` extends the `Microfleet` base type with `RBAC` property which provides access to the `Rbac` class instance. This class includes extended support for scoped subjects like `['user:profile', 'admin:users']` and defines abilities with top-level scopes:

```ts
const scopes = [
// deny access to `admin:*` subject
{
subject: 'admin',
action: 'manage',
inverted: true
},
// allow read access to `admin:users` subject
{
subject: 'admin:users',
action: 'read',
}
]
```

### Service action

Plugin extends `ServiceAction` interface with `rbac` property with the `ActionRbac` type. This property allows specifying action level policy checks.

The `plugin-casl` adds its handler on the `Router.preAllowed` hook and relies on the extended `req.auth` property with the `AuthInfo` type. This property should provide one of the `auth` strategies.

#### Example

```ts
const serviceConfig = {
router: {
auth: {
strategies: {
token: async (request: ServiceRequest) => {
request.auth = {
credentials: {},
scopes: [
{ subject: 'account', action: 'manage' }
]
}
},
},
},
},
}
```

```ts
import { ServiceRequest, ServiceAction, ActionTransport } from '@microfleet/plugin-router'

const actionWithRbac: Partial<ServiceAction> = async (request: ServiceRequest) => {
return {}
}

// ...

actionWithRbac.auth = 'token'
actionWithRbac.rbac = {
action: 'read',
subject: 'user-profile'
}

export default actionWithRbac
```

### Direct usage

It is possible to create custom policy checks using the `Microfleet.rbac` property:

#### Predefined abilities
```ts
// service.rbac.abilities should be filled with predefined abilities: `admin`, `user`
const protected = (this: Microfleet, userGroup: string) => {
const ability = this.rbac.getAbility(userGroup);
if (ability.can('do-smth', 'user')) {
// perform action
}

throw new AccessDenied('')
}

```

#### Dynamic abilities

```ts
const userScopes = [
{
action: 'read',
subject: 'user'
}
]

const protected = (this: Microfleet) => {
const ability = this.rbac.createAbility(userScopes);

const canReadUserProfile = this.rbac.can(ability, 'read', 'user:profile') // true
const canWriteUserProfile = this.rbac.can(ability, 'write', 'user:profile') // false
const canReadUserScope = this.rbac.can(ability, 'read', 'user')

}
```


20 changes: 20 additions & 0 deletions packages/plugin-casl/__tests__/artifacts/actions/protected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ServiceRequest, ServiceAction, ActionTransport } from '@microfleet/plugin-router'

const actionWithRbac: Partial<ServiceAction> = async function actionWithRbac(request: ServiceRequest): Promise<any> {
return {
response: 'success',
token: request.params.token,
user: request.auth,
}
}

actionWithRbac.transports = [
ActionTransport.internal
]
actionWithRbac.auth = 'token'
actionWithRbac.rbac = {
action: 'read',
subject: 'my-subject'
}

export default actionWithRbac
21 changes: 21 additions & 0 deletions packages/plugin-casl/__tests__/artifacts/actions/scoped/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ServiceRequest, ServiceAction, ActionTransport } from '@microfleet/plugin-router'

const actionWithRbac: Partial<ServiceAction> = async function actionWithRbac(request: ServiceRequest): Promise<any> {
return {
response: 'success',
scope: 'app:profile',
token: request.params.token,
user: request.auth,
}
}

actionWithRbac.transports = [
ActionTransport.internal
]
actionWithRbac.auth = 'token'
actionWithRbac.rbac = {
action: 'read',
subject: 'app:profile'
}

export default actionWithRbac
21 changes: 21 additions & 0 deletions packages/plugin-casl/__tests__/artifacts/actions/scoped/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ServiceRequest, ServiceAction, ActionTransport } from '@microfleet/plugin-router'

const actionWithRbac: Partial<ServiceAction> = async function actionWithRbac(request: ServiceRequest): Promise<any> {
return {
response: 'success',
scope: 'app:user',
token: request.params.token,
user: request.auth,
}
}

actionWithRbac.transports = [
ActionTransport.internal
]
actionWithRbac.auth = 'token'
actionWithRbac.rbac = {
action: 'read',
subject: 'app:user'
}

export default actionWithRbac
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "protected",
"type": "object"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "scoped.profile",
"type": "object"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"$id": "scoped.user",
"type": "object"
}
Loading