Skip to content

Commit

Permalink
fix(rbac): rework condition policies to bound them to RBAC roles (jan…
Browse files Browse the repository at this point in the history
…us-idp#1330)

* fix(rbac)!: rework condition policies to bound them to RBAC roles

Signed-off-by: Oleksandr Andriienko <[email protected]>
  • Loading branch information
AndrienkoAleksandr authored Apr 3, 2024
1 parent c8c2b13 commit 55c00b2
Show file tree
Hide file tree
Showing 17 changed files with 2,533 additions and 457 deletions.
48 changes: 32 additions & 16 deletions plugins/rbac-backend/docs/apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,11 +454,11 @@ Returns:

## Conditions

The Backstage permission framework provides conditions, and the RBAC backend plugin supports this feature. Conditions work like content filters for Backstage resources (provided by plugins). The RBAC backend plugin checks if a user has access to a particular resource. If the user has access to the resource, the RBAC backend API delegates the condition for this resource to the corresponding plugin by plugin ID.
The Backstage permission framework provides conditions, and the RBAC backend plugin supports this feature. Conditions work like content filters for Backstage resources (provided by plugins). The RBAC backend API stores conditions assigned to the role in the database. When a user requests access to the frontend resources, the RBAC backend API searches for corresponding conditions and delegates the condition for this resource to the corresponding plugin by its plugin ID. If a user was assigned to multiple roles, and each of these roles contains its own condition, the RBAC backend merges conditions using the anyOf criteria.

The corresponding plugin analyzes conditional parameters and makes a decision about which part of the content the user should see. Consequently, the user can view not all resource content but only some allowed parts. The RBAC backend plugin supports conditions on a generic level - conditions are bound to all roles but none to specific roles.
The corresponding plugin analyzes conditional parameters and makes a decision about which part of the content the user should see. Consequently, the user can view not all resource content but only some allowed parts. The RBAC backend plugin supports conditions bounded to the RBAC role.

A Backstage condition consists of a parameter or an array of parameters joined by criteria. The list of supported conditional criteria includes:
A Backstage condition can be a simple condition with a rule and parameters. But also a Backstage condition could consists of a parameter or an array of parameters joined by criteria. The list of supported conditional criteria includes:

- allOf
- anyOf
Expand All @@ -468,12 +468,14 @@ The plugin defines the supported condition parameters. API users can retrieve th

The structure of the condition JSON object is as follows:

| Json field | Description | Type |
| ------------ | --------------------------------------------------------------------- | ------ |
| result | Always has the value "CONDITIONAL" | String |
| pluginId | Corresponding plugin ID (e.g., "catalog") | String |
| resourceType | Resource type provided by the plugin (e.g., "catalog-entity") | String |
| conditions | Condition JSON with parameters or array parameters joined by criteria | JSON |
| Json field | Description | Type |
| ----------------- | --------------------------------------------------------------------- | ------------ |
| result | Always has the value "CONDITIONAL" | String |
| roleEntityRef | String entity reference to the RBAC role ('role:default/dev') | String |
| pluginId | Corresponding plugin ID (e.g., "catalog") | String |
| permissionMapping | Array permission actions (['read', 'update', 'delete']) | String array |
| resourceType | Resource type provided by the plugin (e.g., "catalog-entity") | String |
| conditions | Condition JSON with parameters or array parameters joined by criteria | JSON |

### GET </plugins/condition-rules>

Expand Down Expand Up @@ -633,7 +635,7 @@ For example, consider a condition without criteria: displaying catalogs only if
- criteria: in this example, criteria are not used since we need to use only one conditional parameter
- params: from the schema, it is evident that it should be an object named "claims" with a string array. This string array constitutes a list of user or group string entity references.

Based on the above schema:
Based on the above schema condition is:

```json
{
Expand All @@ -645,13 +647,15 @@ Based on the above schema:
}
```

To utilize this condition to the RBAC REST api you need to wrap it with more info:
To utilize this condition to the RBAC REST api you need to wrap it with more info

```json
{
"result": "CONDITIONAL",
"roleEntityRef": "role:default/test",
"pluginId": "catalog",
"resourceType": "catalog-entity",
"permissionMapping": ["read"],
"conditions": {
"rule": "IS_ENTITY_OWNER",
"resourceType": "catalog-entity",
Expand Down Expand Up @@ -699,8 +703,10 @@ To utilize this condition to the RBAC REST api you need to wrap it with more inf
```json
{
"result": "CONDITIONAL",
"roleEntityRef": "role:default/test",
"pluginId": "catalog",
"resourceType": "catalog-entity",
"permissionMapping": ["read"],
"conditions": {
"anyOf": [
{
Expand All @@ -726,7 +732,7 @@ To utilize this condition to the RBAC REST api you need to wrap it with more inf

### POST condition

POST </api/permission/conditions>
POST </api/permission/roles/conditions>

Creates a new condition.

Expand All @@ -737,8 +743,10 @@ body:
```json
{
"result": "CONDITIONAL",
"roleEntityRef": "role:default/test",
"pluginId": "catalog",
"resourceType": "catalog-entity",
"permissionMapping": ["read"],
"conditions": {
"rule": "IS_ENTITY_OWNER",
"resourceType": "catalog-entity",
Expand All @@ -761,7 +769,7 @@ Returns a status code of 201 and json with id upon success:

### PUT condition

PUT </permission/conditions/:id>
PUT </permission/roles/conditions/:id>

Update conditions by id.

Expand All @@ -772,8 +780,10 @@ body:
```json
{
"result": "CONDITIONAL",
"roleEntityRef": "role:default/test",
"pluginId": "catalog",
"resourceType": "catalog-entity",
"permissionMapping": ["read"],
"conditions": {
"anyOf": [
{
Expand Down Expand Up @@ -801,15 +811,18 @@ Returns a status code of 200 upon success.

### Get condition by id

GET </api/permission/conditions/:id>
GET </api/permission/roles/conditions/:id>

Returns condition by id:

```json
{
"id": 1,
"result": "CONDITIONAL",
"roleEntityRef": "role:default/test",
"pluginId": "catalog",
"resourceType": "catalog-entity",
"permissionMapping": ["read"],
"conditions": {
"anyOf": [
{
Expand Down Expand Up @@ -837,16 +850,19 @@ Returns a status code of 200 upon success.

### GET conditions

GET </api/permission/conditions>
GET </api/permission/roles/conditions>

Returns lists all conditions:

```json
[
{
"id": 1,
"result": "CONDITIONAL",
"roleEntityRef": "role:default/test",
"pluginId": "catalog",
"resourceType": "catalog-entity",
"permissionMapping": ["read"],
"conditions": {
"anyOf": [
{
Expand Down Expand Up @@ -875,7 +891,7 @@ Returns a status code of 200 upon success.

### DELETE condition by id

DELETE </api/permission/conditions/:id>
DELETE </api/permission/roles/conditions/:id>

Deletes condition by id.

Expand Down
19 changes: 19 additions & 0 deletions plugins/rbac-backend/migrations/20240308134410_migrations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = async function up(knex) {
const policyConditionsExist = await knex.schema.hasTable('policy-conditions');

if (policyConditionsExist) {
// We drop policy condition table, because we decided to rework this feature
// and bound policy condition to the role
await knex.schema.dropTable('policy-conditions');
}
};

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function down(_knex) {};
33 changes: 33 additions & 0 deletions plugins/rbac-backend/migrations/20240308134941_migrations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* up - runs migration.
*
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = async function up(knex) {
await knex.schema.createTable('role-condition-policies', table => {
table.increments('id').primary();
table.string('roleEntityRef');
table.string('result');
table.string('pluginId');
table.string('resourceType');
table.string('permissions');
// Conditions is potentially long json.
// In the future maybe we can use `json` or `jsonb` type instead of `text`:
// table.json('conditions') or table.jsonb('conditions').
// But let's start with text type.
// Data type "text" can be unlimited by size for Postgres.
// Also postgres has a lot of build in features for this data type.
table.text('conditionsJson');
});
};

/**
* down - reverts(undo) migration.
*
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = async function down(knex) {
await knex.schema.dropTable('policy-conditions');
};
Loading

0 comments on commit 55c00b2

Please sign in to comment.