Skip to content

Commit

Permalink
feat: support old permissions if permission service not available
Browse files Browse the repository at this point in the history
  • Loading branch information
drodil committed Sep 12, 2024
1 parent a33963d commit d376851
Show file tree
Hide file tree
Showing 23 changed files with 1,105 additions and 420 deletions.
4 changes: 4 additions & 0 deletions app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ auth:
providers:
guest: {}

permission:
enabled: true

catalog:
orphanStrategy: delete
processingInterval: { minutes: 1 }
Expand All @@ -61,6 +64,7 @@ qeta:
allowAnonymous: true
allowMetadataInput: false
#allowGlobalEdits: true
permissions: true
entities:
max: 2
tags:
Expand Down
2 changes: 2 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ The configuration values are:
- allowAnonymous, boolean, allows anonymous users to post questions and answers anonymously. This enables also guest users to be able to post. Adds checkbox to the forms to post as anonymous. Default is `false`
- allowMetadataInput, boolean, allows `created` and `user` fields to be passed when creating questions, answers, or comments. Useful if migrating information into the system. Default is `false`
- allowGlobalEdits, boolean, allows editing other users' questions and answers by any user. Only if permission framework is not in use. Default is `false`.
- entities.kinds, string array, what kind of catalog entities can be attached to a question. Default is ['Component']
- entities.max, integer, maximum number of entities to attach to a question. Default is `3`
- tags.allowCreation, boolean, determines whether it's possible to add new tags when creating question. Default is `true`
- tags.allowedTags, string array, list of allowed tags to be attached to questions. Only used if `tags.allowCreation` is set to `false`.
- tags.max, integer, maximum number of tags to be attached to a question. Default is `5`.
- moderators, string array, list of moderator groups or users who can edit, delete questions/answers/comments and mark answers correct for any question. Only if permissions framework is not in use.
- storage.type, string, what kind of storage is used to upload images used in questions. Default is `database`. Available values are 'filesystem', 'database' and 's3'.
- storage.maxSizeImage, number, the maximum allowed size of upload files in bytes. Default is `2500000`
- storage.folder, string, what folder is used to storage temporarily images to convert and send to frontend. Default is `/tmp/backstage-qeta-images`
Expand Down
26 changes: 24 additions & 2 deletions docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,46 @@
Q&A plugin is utilizing Backstage permissions framework. Using the framework is optional
but if you want to define who can read questions or post new questions or answers, follow this guide.

The default permissions are hard-coded. This means if you are not using permissions framework in
your installation. These include:

- Only author can edit and delete own questions
- Only author can edit and delete own answers
- Only author can delete their own comments
- User cannot vote their own questions or answers
- Only question author can mark answer as correct
- Except for moderators, who can do these for any questions/answer/comment

## Set up

```ts
import { createBackend } from '@backstage/backend-defaults';

const backend = createBackend();
backend.add(import('@backstage/permissions-backend'));
backend.add(import('@backstage/plugin-permission-backend'));
backend.add(import('@drodil/backstage-plugin-qeta-backend'));

backend.start();
```

In your config you must add both:

```yaml
permission:
enabled: true

qeta:
permissions: true
```
Now handle the permissions in your own PermissionPolicy. See details from
https://backstage.io/docs/permissions/plugin-authors/02-adding-a-basic-permission-check
The Q&A permissions are exported from `@drodil/backstage-plugin-qeta-common` package and are:

- qetaReadQuestionPermission - Allows or denies reading of questions
- qetaCreateQuestionPermission - Allows or denies creating of questions
- qetaEditQuestionPermission - Allows or denies editing of questions
- qetaEditQuestionPermission - Allows or denies editing of questions and marking correct answers
- qetaDeleteQuestionPermission - Allows or denies deleting of questions
- qetaReadAnswerPermission - Allows or denies reading of answers
- qetaCreateAnswerPermission - Allows or denies answering questions
Expand All @@ -32,3 +52,5 @@ The Q&A permissions are exported from `@drodil/backstage-plugin-qeta-common` pac
- qetaCreateCommentPermission - Allows or denies commenting on questions or answers
- qetaEditCommentPermission - Allows or denies editing of comments
- qetaDeleteCommentPermission - Allows or denies deleting of comments

You can find example permission policy in the `plugins/qeta-backend/dev/PermissionPolicy.ts` file.
18 changes: 18 additions & 0 deletions plugins/qeta-backend/configSchema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ export interface Config {
* @visibility backend
*/
allowMetadataInput?: boolean;
/**
* Allow all users to edit other users questions and answers in case permission framework is not in use.
*
* @visibility backend
*/
allowGlobalEdits?: boolean;
/**
* Use permissions framework to control access to questions and answers.
*
* @visibility backend
*/
permissions?: boolean;
/**
* Entities configuration for questions.
*
Expand Down Expand Up @@ -51,6 +63,12 @@ export interface Config {
*/
max?: number;
};
/**
* List of users/groups that can moderate questions and answers in case permission framework is not in use.
*
* @visibility backend
*/
moderators?: string[];
/**
* Configuration about images attachments storage
*
Expand Down
170 changes: 170 additions & 0 deletions plugins/qeta-backend/dev/PermissionPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* SPDX-FileCopyrightText: Copyright 2024 OP Financial Group (https://op.fi). All Rights Reserved.
* SPDX-License-Identifier: LicenseRef-OpAllRightsReserved
*/
import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
import {
AuthorizeResult,
isResourcePermission,
PolicyDecision,
} from '@backstage/plugin-permission-common';
import { PolicyQuery } from '@backstage/plugin-permission-node';
import {
ANSWER_RESOURCE_TYPE,
COMMENT_RESOURCE_TYPE,
isQetaPermission,
QUESTION_RESOURCE_TYPE,
} from '@drodil/backstage-plugin-qeta-common';
import {
answerAuthorConditionFactory,
answerQuestionEntitiesConditionFactory,
commentAuthorConditionFactory,
createAnswerConditionalDecision,
createCommentConditionalDecision,
createQuestionConditionalDecision,
questionAuthorConditionFactory,
questionHasEntitiesConditionFactory,
} from '@drodil/backstage-plugin-qeta-backend';

export class PermissionPolicy implements PermissionPolicy {
async handle(
request: PolicyQuery,
user?: BackstageIdentityResponse,
): Promise<PolicyDecision> {
// Check that we are asking Q&A permissions
if (!isQetaPermission(request.permission)) {
return { result: AuthorizeResult.ALLOW };
}

if (
request.permission.attributes.action === 'create' ||
request.permission.attributes.action === 'read'
) {
// Testing so that only own questions can be seen
/**
if (
isResourcePermission(request.permission, QUESTION_RESOURCE_TYPE) &&
user
) {
return createQuestionConditionalDecision(request.permission, {
allOf: [
questionAuthorConditionFactory({
userRef: user.identity.userEntityRef,
}),
],
});
}
*/

// Testing so that only questions with specific tag can be seen
/**
if (isResourcePermission(request.permission, QUESTION_RESOURCE_TYPE)) {
return createQuestionConditionalDecision(request.permission, {
allOf: [
questionHasTagsConditionFactory({
tags: ['test'],
}),
],
});
}
*/

// Testing so that only questions with specific entity can be seen
/**
if (isResourcePermission(request.permission, QUESTION_RESOURCE_TYPE)) {
return createQuestionConditionalDecision(request.permission, {
allOf: [
questionHasEntitiesConditionFactory({
entityRefs: ['component:default/test-component'],
}),
],
});
}
*/

// Disable question asking
/**
if (isPermission(request.permission, qetaCreateQuestionPermission)) {
return { result: AuthorizeResult.DENY };
}
*/

// Disable answering
/**
if (isPermission(request.permission, qetaCreateAnswerPermission)) {
return { result: AuthorizeResult.DENY };
}
*/

// Disable commenting
/** if (isPermission(request.permission, qetaCreateCommentPermission)) {
return { result: AuthorizeResult.DENY };
}*/
return { result: AuthorizeResult.ALLOW };
}

if (!user) {
return { result: AuthorizeResult.DENY };
}

// Allow updating and deleting only own questions/answers/comments
if (
request.permission.attributes.action === 'update' ||
request.permission.attributes.action === 'delete'
) {
// Can edit and delete only questions with specific tag
/**
if (isResourcePermission(request.permission, QUESTION_RESOURCE_TYPE)) {
return createQuestionConditionalDecision(request.permission, {
allOf: [
questionHasTagsConditionFactory({
tags: ['test'],
}),
],
});
}
*/

if (isResourcePermission(request.permission, QUESTION_RESOURCE_TYPE)) {
return createQuestionConditionalDecision(request.permission, {
anyOf: [
// Can edit and delete own questions
questionAuthorConditionFactory({
userRef: user.identity.userEntityRef,
}),
// Each owned component should have it's own condition factory as the rule requires that the
// question has ALL of the entityRefs attached passed in this array
questionHasEntitiesConditionFactory({
entityRefs: ['component:default/test-component'],
}),
],
});
}

if (isResourcePermission(request.permission, ANSWER_RESOURCE_TYPE)) {
return createAnswerConditionalDecision(request.permission, {
anyOf: [
answerAuthorConditionFactory({
userRef: user.identity.userEntityRef,
}),
answerQuestionEntitiesConditionFactory({
entityRefs: ['component:default/test-component'],
}),
],
});
}

if (isResourcePermission(request.permission, COMMENT_RESOURCE_TYPE)) {
return createCommentConditionalDecision(request.permission, {
allOf: [
commentAuthorConditionFactory({
userRef: user.identity.userEntityRef,
}),
],
});
}
}

return { result: AuthorizeResult.DENY };
}
}
21 changes: 21 additions & 0 deletions plugins/qeta-backend/dev/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { createBackend } from '@backstage/backend-defaults';
import { createBackendModule } from '@backstage/backend-plugin-api';
import { policyExtensionPoint } from '@backstage/plugin-permission-node/alpha';
import { PermissionPolicy } from './PermissionPolicy';

const backend = createBackend();
backend.add(import('@backstage/plugin-catalog-backend/alpha'));
Expand All @@ -7,6 +10,24 @@ backend.add(import('@backstage/plugin-signals-backend'));
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
backend.add(import('@backstage/plugin-notifications-backend'));
backend.add(import('@backstage/plugin-permission-backend/alpha'));
backend.add(
createBackendModule({
pluginId: 'permission',
moduleId: 'qeta-permission-policy',
register(reg) {
reg.registerInit({
deps: {
policy: policyExtensionPoint,
},
async init({ policy }) {
policy.setPolicy(new PermissionPolicy());
},
});
},
}),
);

backend.add(import('../src'));

backend.start();
1 change: 1 addition & 0 deletions plugins/qeta-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"@backstage/plugin-catalog-backend": "^1.25.0",
"@backstage/plugin-events-backend": "^0.3.10",
"@backstage/plugin-notifications-backend": "^0.3.4",
"@backstage/plugin-permission-backend": "^0.5.48",
"@backstage/plugin-signals-backend": "^0.1.9",
"@types/supertest": "^2.0.12",
"@types/uuid": "^9.0.4",
Expand Down
Loading

0 comments on commit d376851

Please sign in to comment.