Skip to content

Commit

Permalink
Merge pull request #267 from drodil/new_access_rights
Browse files Browse the repository at this point in the history
feat: add new resource types and rules for tags and collections
  • Loading branch information
drodil authored Feb 14, 2025
2 parents f31b4fb + 6c68272 commit 5d6e946
Show file tree
Hide file tree
Showing 62 changed files with 1,876 additions and 739 deletions.
8 changes: 8 additions & 0 deletions docs/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,13 @@ The Q&A permissions are exported from `@drodil/backstage-plugin-qeta-common` pac
- qetaCreateCommentPermission - Allows or denies commenting on posts or answers
- qetaEditCommentPermission - Allows or denies editing of comments
- qetaDeleteCommentPermission - Allows or denies deleting of comments
- qetaReadTagPermission - Allows or denies reading of tags
- qetaCreateTagPermission - Allows or denies creating of tags
- qetaEditTagPermission - Allows or denies editing of tags
- qetaDeleteTagPermission - Allows or denies deleting of tags
- qetaReadCollectionPermission - Allows or denies reading of collections
- qetaCreateCollectionPermission - Allows or denies creating of collections
- qetaEditCollectionPermission - Allows or denies editing of collections
- qetaDeleteCollectionPermission - Allows or denies deleting of collections

You can find example permission policy in the `plugins/qeta-backend/dev/PermissionPolicy.ts` file.
2 changes: 1 addition & 1 deletion plugins/qeta-backend/configSchema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface Config {
/**
* Use permissions framework to control access to questions and answers.
*
* @visibility backend
* @visibility frontend
*/
permissions?: boolean;
/**
Expand Down
84 changes: 83 additions & 1 deletion plugins/qeta-backend/dev/PermissionPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,30 @@ import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
import {
AuthorizeResult,
isResourcePermission,
isUpdatePermission,
PolicyDecision,
} from '@backstage/plugin-permission-common';
import { PolicyQuery } from '@backstage/plugin-permission-node';
import {
ANSWER_RESOURCE_TYPE,
COLLECTION_RESOUCE_TYPE,
COMMENT_RESOURCE_TYPE,
isQetaPermission,
POST_RESOURCE_TYPE,
TAG_RESOURCE_TYPE,
} from '@drodil/backstage-plugin-qeta-common';
import {
answerAuthorConditionFactory,
answerQuestionEntitiesConditionFactory,
collectionOwnerConditionFactory,
commentAuthorConditionFactory,
createAnswerConditionalDecision,
createCollectionConditionalDecision,
createCommentConditionalDecision,
createPostConditionalDecision,
postAuthorConditionFactory,
postHasEntitiesConditionFactory,
} from '@drodil/backstage-plugin-qeta-backend';
} from '@drodil/backstage-plugin-qeta-node';
import { AuthService, DiscoveryService } from '@backstage/backend-plugin-api';
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { stringifyEntityRef } from '@backstage/catalog-model';
Expand Down Expand Up @@ -150,6 +155,62 @@ export class PermissionPolicy implements PermissionPolicy {
/** if (isPermission(request.permission, qetaCreateCommentPermission)) {
return { result: AuthorizeResult.DENY };
}*/

// Disable tag creation
/** if (isPermission(request.permission, qetaCreateTagPermission)) {
return { result: AuthorizeResult.DENY };
}*/

// Disable collection creation
/** if (isPermission(request.permission, qetaCreateCollectionPermission)) {
return { result: AuthorizeResult.DENY };
}*/

// Allow reading only own collections
/** if (isResourcePermission(request.permission, COLLECTION_RESOUCE_TYPE)) {
return createCollectionConditionalDecision(request.permission, {
allOf: [
collectionOwnerConditionFactory({
userRef: user.identity.userEntityRef,
}),
],
});
}
*/

// Allow reading only specific tags
/** if (isResourcePermission(request.permission, TAG_RESOURCE_TYPE)) {
return createTagConditionalDecision(request.permission, {
allOf: [
tagConditionFactory({
tag: 'test',
}),
],
});
} */

// Test that only collections with specific tags can be seen
/** if (isResourcePermission(request.permission, COLLECTION_RESOUCE_TYPE)) {
return createCollectionConditionalDecision(request.permission, {
allOf: [
collectionHasTagsConditionFactory({
tags: ['test'],
}),
],
});
}*/

// Test that only collections with specific entities can be seen
/** if (isResourcePermission(request.permission, COLLECTION_RESOUCE_TYPE)) {
return createCollectionConditionalDecision(request.permission, {
allOf: [
collectionHasEntitiesConditionFactory({
entityRefs: ['group:default/child-group'],
}),
],
});
} */

return { result: AuthorizeResult.ALLOW };
}

Expand Down Expand Up @@ -224,6 +285,7 @@ export class PermissionPolicy implements PermissionPolicy {
});
}

// Allow updating and deleting only own answers and answers with test-component in the question
if (isResourcePermission(request.permission, ANSWER_RESOURCE_TYPE)) {
return createAnswerConditionalDecision(request.permission, {
anyOf: [
Expand All @@ -237,6 +299,7 @@ export class PermissionPolicy implements PermissionPolicy {
});
}

// Allow deleting and updating only own comments
if (isResourcePermission(request.permission, COMMENT_RESOURCE_TYPE)) {
return createCommentConditionalDecision(request.permission, {
allOf: [
Expand All @@ -246,6 +309,25 @@ export class PermissionPolicy implements PermissionPolicy {
],
});
}

// Allow deleting and updating only own collections
if (isResourcePermission(request.permission, COLLECTION_RESOUCE_TYPE)) {
return createCollectionConditionalDecision(request.permission, {
allOf: [
collectionOwnerConditionFactory({
userRef: user.identity.userEntityRef,
}),
],
});
}

// Allow updating any tag by anyone
if (
isResourcePermission(request.permission, TAG_RESOURCE_TYPE) &&
isUpdatePermission(request.permission)
) {
return { result: AuthorizeResult.ALLOW };
}
}

return { result: AuthorizeResult.DENY };
Expand Down
19 changes: 19 additions & 0 deletions plugins/qeta-backend/migrations/20250114_collection_permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
await knex.schema.alterTable('collections', table => {
table.dropColumn('readAccess');
table.dropColumn('editAccess');
});
};

/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
await knex.schema.createTable('collections', table => {
table.string('readAccess').defaultTo('private').notNullable();
table.string('editAccess').defaultTo('private').notNullable();
});
};
148 changes: 148 additions & 0 deletions plugins/qeta-backend/migrations/20250114_x_comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* @param {import('knex').Knex} knex
*/
exports.up = async function up(knex) {
// Combine comments to single table
await knex.schema.createTable('comments', table => {
table.string('author').notNullable();
table.text('content').notNullable();
table.datetime('created').notNullable();
table.datetime('updated').nullable();
table.string('updatedBy').nullable();
table.integer('postId').unsigned().nullable();
table.integer('answerId').unsigned().nullable();
table.increments('id');
table
.foreign('postId')
.references('id')
.inTable('posts')
.onDelete('CASCADE');
table
.foreign('answerId')
.references('id')
.inTable('answers')
.onDelete('CASCADE');
});

const postComments = await knex('post_comments').select();
for (const comment of postComments) {
await knex('comments').insert({
author: comment.author,
content: comment.content,
created: comment.created,
updated: comment.updated,
updatedBy: comment.updatedBy,
postId: comment.postId,
});
}

const answerComments = await knex('answer_comments').select();
for (const comment of answerComments) {
await knex('comments').insert({
author: comment.author,
content: comment.content,
created: comment.created,
updated: comment.updated,
updatedBy: comment.updatedBy,
answerId: comment.answerId,
});
}

await knex.schema.dropView('unique_authors');

await knex.schema.createView('unique_authors', view => {
view.columns(['author']);
view.as(
knex.union([
knex('posts').select('author'),
knex('answers').select('author'),
knex('comments').select('author'),
knex('post_views').select('author'),
]),
);
});

await knex.schema.alterTable('post_comments', table => {
table.dropForeign('postId', 'question_comments_questionid_foreign');
});
await knex.schema.alterTable('answer_comments', table => {
table.dropForeign('answerId');
});

await knex.schema.dropTable('post_comments');
await knex.schema.dropTable('answer_comments');
};

/**
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
await knex.schema.createTable('post_comments', table => {
table.string('author').notNullable();
table.text('content').notNullable();
table.datetime('created').notNullable();
table.datetime('updated').nullable();
table.string('updatedBy').nullable();
table.integer('postId').unsigned().notNullable();
table.increments('id');
table
.foreign('postId')
.references('id')
.inTable('questions')
.onDelete('CASCADE');
});

await knex.schema.createTable('answer_comments', table => {
table.string('author').notNullable();
table.text('content').notNullable();
table.datetime('created').notNullable();
table.datetime('updated').nullable();
table.string('updatedBy').nullable();
table.integer('answerId').unsigned().notNullable();
table.increments('id');
table
.foreign('answerId')
.references('id')
.inTable('answers')
.onDelete('CASCADE');
});

const comments = await knex('comments').select();
for (const comment of comments) {
if (comment.postId) {
await knex('post_comments').insert({
author: comment.author,
content: comment.content,
created: comment.created,
updated: comment.updated,
updatedBy: comment.updatedBy,
postId: comment.postId,
});
} else {
await knex('answer_comments').insert({
author: comment.author,
content: comment.content,
created: comment.created,
updated: comment.updated,
updatedBy: comment.updatedBy,
answerId: comment.answerId,
});
}
}

await knex.schema.dropView('unique_authors');
await knex.schema.createView('unique_authors', view => {
view.columns(['author']);
view.as(
knex.union([
knex('posts').select('author'),
knex('answers').select('author'),
knex('answer_comments').select('author'),
knex('post_comments').select('author'),
knex('post_views').select('author'),
]),
);
});

await knex.schema.dropTable('comments');
};
3 changes: 1 addition & 2 deletions plugins/qeta-backend/src/database/DatabaseQetaStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ describe.each(databases.eachSupportedId())(
await knex('answer_votes').del();
await knex('post_views').del();
await knex('tags').del();
await knex('post_comments').del();
await knex('answer_comments').del();
await knex('comments').del();
await knex('user_tags').del();
});

Expand Down
Loading

0 comments on commit 5d6e946

Please sign in to comment.