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: wip change to permission framework #140

Merged
merged 2 commits into from
Sep 12, 2024
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
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
8 changes: 2 additions & 6 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ The following configuration options are available for your app-config.yaml:
qeta:
allowAnonymous: true
allowMetadataInput: false
allowGlobalEdits: false
entities:
kinds: ['Component']
max: 3
Expand All @@ -16,9 +15,6 @@ qeta:
- test
- tag2
max: 5
moderators:
- group:default/my-group
- user:default/my-user
storage:
disabled: true
type: database
Expand All @@ -29,13 +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. 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
- 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
29 changes: 26 additions & 3 deletions docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

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. These include:

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
Expand All @@ -17,17 +19,38 @@ The default permissions are hard-coded. These include:
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:

- qetaReadPermission - Allows or denies reading of questions and answers
- qetaReadQuestionPermission - Allows or denies reading of questions
- qetaCreateQuestionPermission - Allows or denies creating 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
- qetaEditAnswerPermission - Allows or denies editing of answers
- qetaDeleteAnswerPermission - Allows or denies deleting of answers
- qetaReadCommentPermission - Allows or denies reading of comments
- 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.
10 changes: 8 additions & 2 deletions plugins/qeta-backend/configSchema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ export interface Config {
*/
allowMetadataInput?: boolean;
/**
* Allow all users to edit other users questions and answers
* 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 @@ -58,7 +64,7 @@ export interface Config {
max?: number;
};
/**
* List of users/groups that can moderate questions and answers
* List of users/groups that can moderate questions and answers in case permission framework is not in use.
*
* @visibility backend
*/
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();
5 changes: 4 additions & 1 deletion plugins/qeta-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,11 @@
"express-promise-router": "^4.1.0",
"file-type": "16.5.4",
"knex": "^3.0.0",
"lodash": "^4.17.21",
"multiparty": "^4.2.3",
"uuid": "^9.0.1",
"yn": "^4.0.0"
"yn": "^4.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-test-utils": "^0.5.0",
Expand All @@ -84,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