From d3843d8f11314ef23c01fad89e07ba32243fc2f6 Mon Sep 17 00:00:00 2001 From: forehalo Date: Thu, 6 Feb 2025 04:54:34 +0000 Subject: [PATCH] refactor(server): role actions definition (#9962) --- .../server/src/base/graphql/register.ts | 20 + packages/backend/server/src/base/index.ts | 1 + .../backend/server/src/base/utils/types.ts | 8 + .../__snapshots__/actions.spec.ts.md | 293 ++++++++ .../__snapshots__/actions.spec.ts.snap | Bin 0 -> 1217 bytes .../__tests__/__snapshots__/role.spec.ts.md | 665 ------------------ .../__tests__/__snapshots__/role.spec.ts.snap | Bin 947 -> 0 bytes .../core/permission/__tests__/actions.spec.ts | 83 +++ .../core/permission/__tests__/role.spec.ts | 36 - .../server/src/core/permission/index.ts | 13 +- .../server/src/core/permission/service.ts | 20 +- .../server/src/core/permission/types.ts | 548 ++++++++------- .../server/src/core/workspaces/controller.ts | 2 +- .../src/core/workspaces/resolvers/history.ts | 2 +- .../src/core/workspaces/resolvers/page.ts | 60 +- .../core/workspaces/resolvers/workspace.ts | 50 +- .../server/src/plugins/copilot/resolver.ts | 10 +- packages/backend/server/src/schema.gql | 47 +- 18 files changed, 773 insertions(+), 1085 deletions(-) create mode 100644 packages/backend/server/src/base/graphql/register.ts create mode 100644 packages/backend/server/src/core/permission/__tests__/__snapshots__/actions.spec.ts.md create mode 100644 packages/backend/server/src/core/permission/__tests__/__snapshots__/actions.spec.ts.snap delete mode 100644 packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.md delete mode 100644 packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.snap create mode 100644 packages/backend/server/src/core/permission/__tests__/actions.spec.ts delete mode 100644 packages/backend/server/src/core/permission/__tests__/role.spec.ts diff --git a/packages/backend/server/src/base/graphql/register.ts b/packages/backend/server/src/base/graphql/register.ts new file mode 100644 index 0000000000..1155c8dda5 --- /dev/null +++ b/packages/backend/server/src/base/graphql/register.ts @@ -0,0 +1,20 @@ +import { Type } from '@nestjs/common'; +import { Field, ObjectType } from '@nestjs/graphql'; + +import { ApplyType } from '../utils/types'; + +export function registerObjectType( + fields: Record>, + options: { + name: string; + } +) { + const Inner = ApplyType(); + for (const [key, value] of Object.entries(fields)) { + Field(() => value)(Inner.prototype, key); + } + + ObjectType(options.name)(Inner); + + return Inner; +} diff --git a/packages/backend/server/src/base/index.ts b/packages/backend/server/src/base/index.ts index 599a19a754..588c02447b 100644 --- a/packages/backend/server/src/base/index.ts +++ b/packages/backend/server/src/base/index.ts @@ -16,6 +16,7 @@ export { export * from './error'; export { EventBus, OnEvent } from './event'; export type { GraphqlContext } from './graphql'; +export { registerObjectType } from './graphql/register'; export * from './guard'; export { CryptoHelper, URLHelper } from './helpers'; export { AFFiNELogger } from './logger'; diff --git a/packages/backend/server/src/base/utils/types.ts b/packages/backend/server/src/base/utils/types.ts index 57b95fa631..ddc422a3ad 100644 --- a/packages/backend/server/src/base/utils/types.ts +++ b/packages/backend/server/src/base/utils/types.ts @@ -45,6 +45,14 @@ export type LeafPaths< }[keyof T] : never; +export type LeafVisitor = { + [K in keyof T]: T[K] extends object + ? LeafVisitor> + : P extends '' + ? K + : Join; +}; + export interface FileUpload { filename: string; mimetype: string; diff --git a/packages/backend/server/src/core/permission/__tests__/__snapshots__/actions.spec.ts.md b/packages/backend/server/src/core/permission/__tests__/__snapshots__/actions.spec.ts.md new file mode 100644 index 0000000000..72c15d4960 --- /dev/null +++ b/packages/backend/server/src/core/permission/__tests__/__snapshots__/actions.spec.ts.md @@ -0,0 +1,293 @@ +# Snapshot report for `src/core/permission/__tests__/actions.spec.ts` + +The actual snapshot is saved in `actions.spec.ts.snap`. + +Generated by [AVA](https://avajs.dev). + +## should be able to fixup doc role from workspace role and doc role + +> WorkspaceRole: External, DocRole: External + + 'External' + +> WorkspaceRole: External, DocRole: Reader + + 'Reader' + +> WorkspaceRole: External, DocRole: Editor + + 'Editor' + +> WorkspaceRole: External, DocRole: Manager + + 'Editor' + +> WorkspaceRole: External, DocRole: Owner + + 'Editor' + +> WorkspaceRole: Collaborator, DocRole: External + + 'External' + +> WorkspaceRole: Collaborator, DocRole: Reader + + 'Reader' + +> WorkspaceRole: Collaborator, DocRole: Editor + + 'Editor' + +> WorkspaceRole: Collaborator, DocRole: Manager + + 'Manager' + +> WorkspaceRole: Collaborator, DocRole: Owner + + 'Owner' + +> WorkspaceRole: Admin, DocRole: External + + 'Manager' + +> WorkspaceRole: Admin, DocRole: Reader + + 'Manager' + +> WorkspaceRole: Admin, DocRole: Editor + + 'Manager' + +> WorkspaceRole: Admin, DocRole: Manager + + 'Manager' + +> WorkspaceRole: Admin, DocRole: Owner + + 'Owner' + +> WorkspaceRole: Owner, DocRole: External + + 'Owner' + +> WorkspaceRole: Owner, DocRole: Reader + + 'Owner' + +> WorkspaceRole: Owner, DocRole: Editor + + 'Owner' + +> WorkspaceRole: Owner, DocRole: Manager + + 'Owner' + +> WorkspaceRole: Owner, DocRole: Owner + + 'Owner' + +## should be able to get correct permissions from WorkspaceRole + +> WorkspaceRole: External + + { + 'Workspace.CreateDoc': false, + 'Workspace.Delete': false, + 'Workspace.Organize.Read': true, + 'Workspace.Properties.Create': false, + 'Workspace.Properties.Delete': false, + 'Workspace.Properties.Read': false, + 'Workspace.Properties.Update': false, + 'Workspace.Settings.Read': false, + 'Workspace.Settings.Update': false, + 'Workspace.Sync': false, + 'Workspace.TransferOwner': false, + 'Workspace.Users.Manage': false, + 'Workspace.Users.Read': false, + } + +> WorkspaceRole: Collaborator + + { + 'Workspace.CreateDoc': true, + 'Workspace.Delete': false, + 'Workspace.Organize.Read': true, + 'Workspace.Properties.Create': false, + 'Workspace.Properties.Delete': false, + 'Workspace.Properties.Read': true, + 'Workspace.Properties.Update': false, + 'Workspace.Settings.Read': true, + 'Workspace.Settings.Update': false, + 'Workspace.Sync': true, + 'Workspace.TransferOwner': false, + 'Workspace.Users.Manage': false, + 'Workspace.Users.Read': true, + } + +> WorkspaceRole: Admin + + { + 'Workspace.CreateDoc': true, + 'Workspace.Delete': false, + 'Workspace.Organize.Read': true, + 'Workspace.Properties.Create': true, + 'Workspace.Properties.Delete': true, + 'Workspace.Properties.Read': true, + 'Workspace.Properties.Update': true, + 'Workspace.Settings.Read': true, + 'Workspace.Settings.Update': true, + 'Workspace.Sync': true, + 'Workspace.TransferOwner': false, + 'Workspace.Users.Manage': true, + 'Workspace.Users.Read': true, + } + +> WorkspaceRole: Owner + + { + 'Workspace.CreateDoc': true, + 'Workspace.Delete': true, + 'Workspace.Organize.Read': true, + 'Workspace.Properties.Create': true, + 'Workspace.Properties.Delete': true, + 'Workspace.Properties.Read': true, + 'Workspace.Properties.Update': true, + 'Workspace.Settings.Read': true, + 'Workspace.Settings.Update': true, + 'Workspace.Sync': true, + 'Workspace.TransferOwner': true, + 'Workspace.Users.Manage': true, + 'Workspace.Users.Read': true, + } + +## should be able to get correct permissions from DocRole + +> DocRole: External + + { + 'Doc.Copy': true, + 'Doc.Delete': false, + 'Doc.Duplicate': false, + 'Doc.Properties.Read': true, + 'Doc.Properties.Update': false, + 'Doc.Publish': false, + 'Doc.Read': true, + 'Doc.Restore': false, + 'Doc.TransferOwner': false, + 'Doc.Trash': false, + 'Doc.Update': false, + 'Doc.Users.Manage': false, + 'Doc.Users.Read': false, + } + +> DocRole: Reader + + { + 'Doc.Copy': true, + 'Doc.Delete': false, + 'Doc.Duplicate': true, + 'Doc.Properties.Read': true, + 'Doc.Properties.Update': false, + 'Doc.Publish': false, + 'Doc.Read': true, + 'Doc.Restore': false, + 'Doc.TransferOwner': false, + 'Doc.Trash': false, + 'Doc.Update': false, + 'Doc.Users.Manage': false, + 'Doc.Users.Read': true, + } + +> DocRole: Editor + + { + 'Doc.Copy': true, + 'Doc.Delete': true, + 'Doc.Duplicate': true, + 'Doc.Properties.Read': true, + 'Doc.Properties.Update': true, + 'Doc.Publish': false, + 'Doc.Read': true, + 'Doc.Restore': true, + 'Doc.TransferOwner': false, + 'Doc.Trash': true, + 'Doc.Update': true, + 'Doc.Users.Manage': false, + 'Doc.Users.Read': true, + } + +> DocRole: Manager + + { + 'Doc.Copy': true, + 'Doc.Delete': true, + 'Doc.Duplicate': true, + 'Doc.Properties.Read': true, + 'Doc.Properties.Update': true, + 'Doc.Publish': true, + 'Doc.Read': true, + 'Doc.Restore': true, + 'Doc.TransferOwner': false, + 'Doc.Trash': true, + 'Doc.Update': true, + 'Doc.Users.Manage': true, + 'Doc.Users.Read': true, + } + +> DocRole: Owner + + { + 'Doc.Copy': true, + 'Doc.Delete': true, + 'Doc.Duplicate': true, + 'Doc.Properties.Read': true, + 'Doc.Properties.Update': true, + 'Doc.Publish': true, + 'Doc.Read': true, + 'Doc.Restore': true, + 'Doc.TransferOwner': true, + 'Doc.Trash': true, + 'Doc.Update': true, + 'Doc.Users.Manage': true, + 'Doc.Users.Read': true, + } + +## should be able to find minimal workspace role from action + +> Snapshot 1 + + { + 'Workspace.CreateDoc': 'Collaborator', + 'Workspace.Delete': 'Owner', + 'Workspace.Organize.Read': 'External', + 'Workspace.Properties.Create': 'Admin', + 'Workspace.Properties.Delete': 'Admin', + 'Workspace.Properties.Read': 'Collaborator', + 'Workspace.Properties.Update': 'Admin', + 'Workspace.Settings.Read': 'Collaborator', + 'Workspace.Settings.Update': 'Admin', + 'Workspace.Sync': 'Collaborator', + 'Workspace.TransferOwner': 'Owner', + 'Workspace.Users.Manage': 'Admin', + 'Workspace.Users.Read': 'Collaborator', + } + +## should be able to find minimal doc role from action + +> Snapshot 1 + + { + 'Doc.Copy': 'External', + 'Doc.Delete': 'Editor', + 'Doc.Duplicate': 'Reader', + 'Doc.Properties.Read': 'External', + 'Doc.Properties.Update': 'Editor', + 'Doc.Publish': 'Manager', + 'Doc.Read': 'External', + 'Doc.Restore': 'Editor', + 'Doc.TransferOwner': 'Owner', + 'Doc.Trash': 'Editor', + 'Doc.Update': 'Editor', + 'Doc.Users.Manage': 'Manager', + 'Doc.Users.Read': 'Reader', + } diff --git a/packages/backend/server/src/core/permission/__tests__/__snapshots__/actions.spec.ts.snap b/packages/backend/server/src/core/permission/__tests__/__snapshots__/actions.spec.ts.snap new file mode 100644 index 0000000000000000000000000000000000000000..b4612b4a23629b1c55431430ff67aec18c93e260 GIT binary patch literal 1217 zcmV;y1U~ygRzV6@%EHB+ll zzGQJ6oga$`00000000B+TF-CXL=^tUk(1EUWLIg6s!HGkP=18HRH{_yfhws$NGPan zKyYX^YflolUXN?fDqC(9RGdIU;)sx1!GUWf_!Cg!AK(r*xN$+^gx2=1y|Xi8Z?bKZ zYNI_wJDv@}Xp+TCJ;CCT1}va$ z7W7{GXh3zD2kcE}Fudxlb9eNsjPB|P-TY-nJr?I=uT+y|cqP>(&lh|GstjLE^=dWM z`!uB6b<8?b&9$)`4w%fYZ%JHhJP2r;$5ilmPwro>M_6|YYSNXrVRM9TA$M!e_eMRw zk-fMwLTIPH&FNmRM_1YoqjmDmUi3PCsNDu5)k_sro~ zkzJjRXG_sRrn!P$%=E{6aOg%+Qhz8!ru-xyHhgZYD~jqi^Zu{)##H2dNW~h(#qmP4L&Y|OH0eb zW>=G$52mnX4L-HPu4mKUg)Q?>Mc6XwlM(iEo)1n-$}Sf=Gj$u(UGIsqMFurZN!7BY{{VQN0B;lEIsxwFaq=qx{vtqg^7vR>=Wc6_N4M3!Inp0Cz2uLjfNm7{t~M#J z^f9%hc|t;(QvJ~s+ieGavZF?%Ytv75G#gI(MHt6;=5x zZYHYzib_TtdQpv)iUv_d>XX@-st%2+WZXeeRj59vKd-vj4Lzshhkl0!@{Mj<_)}N- zJS^(|JNGhy&Ye{W^jiSG0k8=076AkS9uVMf0-UjcYZmae1^i+G|4p$kwwvVH@{v@n zF4-R3c}&tQ+oe0yN!#ZBgz4GBq}{Xb!HOoj!Yh?6wQgEtXI(LAMW3ykQq^u(%w%l7 zb$f5>!F9IjTXEUKK_H&32*lAJTIfR1`auf z0qt#`1|>TxS~-i@}bu6r-J8lWKID fl`@W&R?a+HpgIHTU7$)u#TNb#|GUhtU?czl2e3{3 literal 0 HcmV?d00001 diff --git a/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.md b/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.md deleted file mode 100644 index de420b5ae3..0000000000 --- a/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.md +++ /dev/null @@ -1,665 +0,0 @@ -# Snapshot report for `src/core/permission/__tests__/role.spec.ts` - -The actual snapshot is saved in `role.spec.ts.snap`. - -Generated by [AVA](https://avajs.dev). - -## should be able to get correct permissions from WorkspaceRole: External and DocRole: External - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: false, - Doc_Duplicate: false, - Doc_Properties_Read: true, - Doc_Properties_Update: false, - Doc_Publish: false, - Doc_Read: true, - Doc_Restore: false, - Doc_TransferOwner: false, - Doc_Trash: false, - Doc_Update: false, - Doc_Users_Manage: false, - Doc_Users_Read: false, - Workspace_CreateDoc: false, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: false, - Workspace_Properties_Delete: false, - Workspace_Properties_Read: false, - Workspace_Properties_Update: false, - Workspace_Settings_Read: false, - Workspace_Settings_Update: false, - Workspace_Sync: false, - Workspace_TransferOwner: false, - Workspace_Users_Manage: false, - Workspace_Users_Read: false, - } - -## should be able to get correct permissions from WorkspaceRole: External and DocRole: Reader - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: false, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: false, - Doc_Publish: false, - Doc_Read: true, - Doc_Restore: false, - Doc_TransferOwner: false, - Doc_Trash: false, - Doc_Update: false, - Doc_Users_Manage: false, - Doc_Users_Read: true, - Workspace_CreateDoc: false, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: false, - Workspace_Properties_Delete: false, - Workspace_Properties_Read: false, - Workspace_Properties_Update: false, - Workspace_Settings_Read: false, - Workspace_Settings_Update: false, - Workspace_Sync: false, - Workspace_TransferOwner: false, - Workspace_Users_Manage: false, - Workspace_Users_Read: false, - } - -## should be able to get correct permissions from WorkspaceRole: External and DocRole: Editor - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: false, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: false, - Doc_Users_Read: true, - Workspace_CreateDoc: false, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: false, - Workspace_Properties_Delete: false, - Workspace_Properties_Read: false, - Workspace_Properties_Update: false, - Workspace_Settings_Read: false, - Workspace_Settings_Update: false, - Workspace_Sync: false, - Workspace_TransferOwner: false, - Workspace_Users_Manage: false, - Workspace_Users_Read: false, - } - -## should be able to get correct permissions from WorkspaceRole: External and DocRole: Manager - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: false, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: false, - Workspace_Properties_Delete: false, - Workspace_Properties_Read: false, - Workspace_Properties_Update: false, - Workspace_Settings_Read: false, - Workspace_Settings_Update: false, - Workspace_Sync: false, - Workspace_TransferOwner: false, - Workspace_Users_Manage: false, - Workspace_Users_Read: false, - } - -## should be able to get correct permissions from WorkspaceRole: External and DocRole: Owner - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: true, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: false, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: false, - Workspace_Properties_Delete: false, - Workspace_Properties_Read: false, - Workspace_Properties_Update: false, - Workspace_Settings_Read: false, - Workspace_Settings_Update: false, - Workspace_Sync: false, - Workspace_TransferOwner: false, - Workspace_Users_Manage: false, - Workspace_Users_Read: false, - } - -## should be able to get correct permissions from WorkspaceRole: Collaborator and DocRole: External - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: false, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: false, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: false, - Workspace_Properties_Delete: false, - Workspace_Properties_Read: true, - Workspace_Properties_Update: false, - Workspace_Settings_Read: true, - Workspace_Settings_Update: false, - Workspace_Sync: true, - Workspace_TransferOwner: false, - Workspace_Users_Manage: false, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Collaborator and DocRole: Reader - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: false, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: false, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: false, - Workspace_Properties_Delete: false, - Workspace_Properties_Read: true, - Workspace_Properties_Update: false, - Workspace_Settings_Read: true, - Workspace_Settings_Update: false, - Workspace_Sync: true, - Workspace_TransferOwner: false, - Workspace_Users_Manage: false, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Collaborator and DocRole: Editor - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: false, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: false, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: false, - Workspace_Properties_Delete: false, - Workspace_Properties_Read: true, - Workspace_Properties_Update: false, - Workspace_Settings_Read: true, - Workspace_Settings_Update: false, - Workspace_Sync: true, - Workspace_TransferOwner: false, - Workspace_Users_Manage: false, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Collaborator and DocRole: Manager - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: false, - Workspace_Properties_Delete: false, - Workspace_Properties_Read: true, - Workspace_Properties_Update: false, - Workspace_Settings_Read: true, - Workspace_Settings_Update: false, - Workspace_Sync: true, - Workspace_TransferOwner: false, - Workspace_Users_Manage: false, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Collaborator and DocRole: Owner - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: true, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: false, - Workspace_Properties_Delete: false, - Workspace_Properties_Read: true, - Workspace_Properties_Update: false, - Workspace_Settings_Read: true, - Workspace_Settings_Update: false, - Workspace_Sync: true, - Workspace_TransferOwner: false, - Workspace_Users_Manage: false, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Admin and DocRole: External - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: true, - Workspace_Properties_Delete: true, - Workspace_Properties_Read: true, - Workspace_Properties_Update: true, - Workspace_Settings_Read: true, - Workspace_Settings_Update: true, - Workspace_Sync: true, - Workspace_TransferOwner: false, - Workspace_Users_Manage: true, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Admin and DocRole: Reader - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: true, - Workspace_Properties_Delete: true, - Workspace_Properties_Read: true, - Workspace_Properties_Update: true, - Workspace_Settings_Read: true, - Workspace_Settings_Update: true, - Workspace_Sync: true, - Workspace_TransferOwner: false, - Workspace_Users_Manage: true, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Admin and DocRole: Editor - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: true, - Workspace_Properties_Delete: true, - Workspace_Properties_Read: true, - Workspace_Properties_Update: true, - Workspace_Settings_Read: true, - Workspace_Settings_Update: true, - Workspace_Sync: true, - Workspace_TransferOwner: false, - Workspace_Users_Manage: true, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Admin and DocRole: Manager - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: true, - Workspace_Properties_Delete: true, - Workspace_Properties_Read: true, - Workspace_Properties_Update: true, - Workspace_Settings_Read: true, - Workspace_Settings_Update: true, - Workspace_Sync: true, - Workspace_TransferOwner: false, - Workspace_Users_Manage: true, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Admin and DocRole: Owner - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: true, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: false, - Workspace_Organize_Read: true, - Workspace_Properties_Create: true, - Workspace_Properties_Delete: true, - Workspace_Properties_Read: true, - Workspace_Properties_Update: true, - Workspace_Settings_Read: true, - Workspace_Settings_Update: true, - Workspace_Sync: true, - Workspace_TransferOwner: false, - Workspace_Users_Manage: true, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Owner and DocRole: External - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: true, - Workspace_Organize_Read: true, - Workspace_Properties_Create: true, - Workspace_Properties_Delete: true, - Workspace_Properties_Read: true, - Workspace_Properties_Update: true, - Workspace_Settings_Read: true, - Workspace_Settings_Update: true, - Workspace_Sync: true, - Workspace_TransferOwner: true, - Workspace_Users_Manage: true, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Owner and DocRole: Reader - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: true, - Workspace_Organize_Read: true, - Workspace_Properties_Create: true, - Workspace_Properties_Delete: true, - Workspace_Properties_Read: true, - Workspace_Properties_Update: true, - Workspace_Settings_Read: true, - Workspace_Settings_Update: true, - Workspace_Sync: true, - Workspace_TransferOwner: true, - Workspace_Users_Manage: true, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Owner and DocRole: Editor - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: true, - Workspace_Organize_Read: true, - Workspace_Properties_Create: true, - Workspace_Properties_Delete: true, - Workspace_Properties_Read: true, - Workspace_Properties_Update: true, - Workspace_Settings_Read: true, - Workspace_Settings_Update: true, - Workspace_Sync: true, - Workspace_TransferOwner: true, - Workspace_Users_Manage: true, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Owner and DocRole: Manager - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: false, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: true, - Workspace_Organize_Read: true, - Workspace_Properties_Create: true, - Workspace_Properties_Delete: true, - Workspace_Properties_Read: true, - Workspace_Properties_Update: true, - Workspace_Settings_Read: true, - Workspace_Settings_Update: true, - Workspace_Sync: true, - Workspace_TransferOwner: true, - Workspace_Users_Manage: true, - Workspace_Users_Read: true, - } - -## should be able to get correct permissions from WorkspaceRole: Owner and DocRole: Owner - -> Snapshot 1 - - { - Doc_Copy: true, - Doc_Delete: true, - Doc_Duplicate: true, - Doc_Properties_Read: true, - Doc_Properties_Update: true, - Doc_Publish: true, - Doc_Read: true, - Doc_Restore: true, - Doc_TransferOwner: true, - Doc_Trash: true, - Doc_Update: true, - Doc_Users_Manage: true, - Doc_Users_Read: true, - Workspace_CreateDoc: true, - Workspace_Delete: true, - Workspace_Organize_Read: true, - Workspace_Properties_Create: true, - Workspace_Properties_Delete: true, - Workspace_Properties_Read: true, - Workspace_Properties_Update: true, - Workspace_Settings_Read: true, - Workspace_Settings_Update: true, - Workspace_Sync: true, - Workspace_TransferOwner: true, - Workspace_Users_Manage: true, - Workspace_Users_Read: true, - } diff --git a/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.snap b/packages/backend/server/src/core/permission/__tests__/__snapshots__/role.spec.ts.snap deleted file mode 100644 index 78c71e36c5e42448fdaf8f92eb46047942f395ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 947 zcmV;k15EruRzV90d#NT#>U1r z@#;JN!6%_?<%qrK{+74vSY}O_yygf~d1gZ>v+hYL>dI^h*|dG%_FUiGlwQ+Z^W=`- z;&rj=IpVIlw5No0xnpwIG8eu2=~r#v^(`EL&`iz~i9@@<%1X6+wi%$R>qzdura zrTRhji|V$)*eaEx`at!C>WJz$)vY1MmZ-L=4ye9U{iK>7W^9@24b^9=BdUoJ#_mvU zP`#s?F-E72(UsaZEz2-Y{;X6g%$Q)tO=e6{W@y$JX2zvyV~iG4UGQ4_!3#rE+S^y# zZ!Zc*DA5U2w7^6V*ljs>okwM6+L`;(qfJ(}@T;qWTjA4P(~^%`R^+JTL3X!ByT28l z2-X%o*?8w|UwJZOis^RZq2#W=Ddfs4SIF?(mA|>D6A5-PZY`A{;0x(jm$}Ou5wXc` zth=nf-9EW>s|!-llyp$UtlebXCBD&(t;hy?xzHn` zw+KNpgY8-21Tzwbs|ot{%ja0_1VrvH5(&FbNvr5%I$G!HP_ z^9TtpPjRUa0xt99vTwJ+CAb_=E|)BuK5Zr9QV*OTm!aBa%cY(;E0>|W7`GO12`&eW z%V&9T**PhAIT&8*i3{(g9)p+g^1QrU&%?`L0noBsTIxAy2`!7Jo&P|OS30D_nhGqIQ{+vWGHrtPBh z11<4 { + t.is(Action.Workspace.CreateDoc, 'Workspace.CreateDoc'); + t.is(Action.Workspace.Users.Read, 'Workspace.Users.Read'); + t.is(Action.Doc.Copy, 'Doc.Copy'); + t.is(Action.Doc.Users.Manage, 'Doc.Users.Manage'); + + t.not(Action.Workspace.Delete, 'Wrong.Action.Name'); + + function test(_action: Action) {} + // Action visitor result can be passed to function that accepts [ActionName] + test(Action.Workspace.CreateDoc); + // @ts-expect-error make sure type checked + test('Wrong.Action.Name'); +}); + +const workspaceRoles = Object.values(WorkspaceRole).filter( + r => typeof r === 'number' +) as WorkspaceRole[]; +const docRoles = Object.values(DocRole).filter( + r => typeof r === 'number' +) as DocRole[]; + +test(`should be able to fixup doc role from workspace role and doc role`, t => { + for (const workspaceRole of workspaceRoles) { + for (const docRole of docRoles) { + t.snapshot( + DocRole[fixupDocRole(workspaceRole, docRole)], + `WorkspaceRole: ${WorkspaceRole[workspaceRole]}, DocRole: ${DocRole[docRole]}` + ); + } + } +}); + +test(`should be able to get correct permissions from WorkspaceRole`, t => { + for (const workspaceRole of workspaceRoles) { + t.snapshot( + mapWorkspaceRoleToPermissions(workspaceRole), + `WorkspaceRole: ${WorkspaceRole[workspaceRole]}` + ); + } +}); + +test(`should be able to get correct permissions from DocRole`, t => { + for (const docRole of docRoles) { + t.snapshot( + mapDocRoleToPermissions(docRole), + `DocRole: ${DocRole[docRole]}` + ); + } +}); + +test('should be able to find minimal workspace role from action', t => { + t.snapshot( + Object.fromEntries( + Array.from(WORKSPACE_ACTION_TO_MINIMAL_ROLE_MAP.entries()).map( + ([action, role]) => [action, WorkspaceRole[role]] + ) + ) + ); +}); + +test('should be able to find minimal doc role from action', t => { + t.snapshot( + Object.fromEntries( + Array.from(DOC_ACTION_TO_MINIMAL_ROLE_MAP.entries()).map( + ([action, role]) => [action, DocRole[role]] + ) + ) + ); +}); diff --git a/packages/backend/server/src/core/permission/__tests__/role.spec.ts b/packages/backend/server/src/core/permission/__tests__/role.spec.ts deleted file mode 100644 index 88f0b837da..0000000000 --- a/packages/backend/server/src/core/permission/__tests__/role.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import test from 'ava'; - -import { DocRole, WorkspaceRole } from '../index'; -import { Actions, ActionsKeys, mapRoleToActions } from '../types'; - -// create a matrix representing the all possible permission of WorkspaceRole and DocRole -const matrix = Object.values(WorkspaceRole) - .filter(r => typeof r !== 'string') - .flatMap(workspaceRole => - Object.values(DocRole) - .filter(r => typeof r !== 'string') - .map(docRole => ({ - workspaceRole, - docRole, - })) - ); - -for (const { workspaceRole, docRole } of matrix) { - const permission = mapRoleToActions(workspaceRole, docRole); - test(`should be able to get correct permissions from WorkspaceRole: ${WorkspaceRole[workspaceRole]} and DocRole: ${DocRole[docRole]}`, t => { - t.snapshot(permission); - }); -} - -test('ActionsKeys value should be the same order of the Actions objects', t => { - for (const [index, value] of ActionsKeys.entries()) { - const [k, k1, k2] = value.split('.'); - if (k2) { - // @ts-expect-error - t.is(Actions[k][k1][k2], index); - } else { - // @ts-expect-error - t.is(Actions[k][k1], index); - } - } -}); diff --git a/packages/backend/server/src/core/permission/index.ts b/packages/backend/server/src/core/permission/index.ts index 5747358780..6cbfa85011 100644 --- a/packages/backend/server/src/core/permission/index.ts +++ b/packages/backend/server/src/core/permission/index.ts @@ -9,4 +9,15 @@ import { PermissionService } from './service'; export class PermissionModule {} export { PermissionService } from './service'; -export { DocRole, PublicPageMode, WorkspaceRole } from './types'; +export { + DOC_ACTIONS, + type DocActionPermissions, + DocRole, + fixupDocRole, + mapDocRoleToPermissions, + mapWorkspaceRoleToPermissions, + PublicPageMode, + WORKSPACE_ACTIONS, + type WorkspaceActionPermissions, + WorkspaceRole, +} from './types'; diff --git a/packages/backend/server/src/core/permission/service.ts b/packages/backend/server/src/core/permission/service.ts index 11eef54eca..430b204454 100644 --- a/packages/backend/server/src/core/permission/service.ts +++ b/packages/backend/server/src/core/permission/service.ts @@ -13,11 +13,11 @@ import { WorkspacePermissionNotFound, } from '../../base'; import { - AllPossibleGraphQLDocActionsKeys, + DocAction, + docActionRequiredRole, + docActionRequiredWorkspaceRole, DocRole, - findMinimalDocRole, PublicPageMode, - requiredWorkspaceRoleByDocRole, WorkspaceRole, } from './types'; @@ -175,7 +175,7 @@ export class PermissionService { return isPublicWorkspace || publicPages > 0; } - return this.tryCheckPage(ws, id, 'Doc_Read', user); + return this.tryCheckPage(ws, id, 'Doc.Read', user); } async getWorkspaceMemberStatus(ws: string, user: string) { @@ -526,7 +526,7 @@ export class PermissionService { async checkCloudPagePermission( workspaceId: string, pageId: string, - action: AllPossibleGraphQLDocActionsKeys, + action: DocAction, userId?: string ) { const hasWorkspace = await this.hasWorkspace(workspaceId); @@ -538,7 +538,7 @@ export class PermissionService { async checkPagePermission( ws: string, page: string, - action: AllPossibleGraphQLDocActionsKeys, + action: DocAction, user?: string ) { if (!(await this.tryCheckPage(ws, page, action, user))) { @@ -549,12 +549,12 @@ export class PermissionService { async tryCheckPage( ws: string, page: string, - action: AllPossibleGraphQLDocActionsKeys, + action: DocAction, user?: string ) { - const role = findMinimalDocRole(action); + const role = docActionRequiredRole(action); // check whether page is public - if (action === 'Doc_Read') { + if (action === 'Doc.Read') { const count = await this.prisma.workspacePage.count({ where: { workspaceId: ws, @@ -602,7 +602,7 @@ export class PermissionService { return this.tryCheckWorkspace( ws, user, - requiredWorkspaceRoleByDocRole(role) + docActionRequiredWorkspaceRole(action) ); } diff --git a/packages/backend/server/src/core/permission/types.ts b/packages/backend/server/src/core/permission/types.ts index b34588ead5..a14cde9817 100644 --- a/packages/backend/server/src/core/permission/types.ts +++ b/packages/backend/server/src/core/permission/types.ts @@ -1,4 +1,4 @@ -import assert from 'node:assert'; +import { LeafPaths, LeafVisitor } from '../../base'; export enum PublicPageMode { Page, @@ -20,234 +20,322 @@ export enum WorkspaceRole { Owner = 99, } +/** + * Definitions of all possible actions + * + * NOTE(@forehalo): if you add any new actions, please don't forget to add the corresponding role in [RoleActionsMap] + */ export const Actions = { + // Workspace Actions Workspace: { - Sync: 1, - CreateDoc: 2, - Delete: 11, - TransferOwner: 12, + Sync: '', + CreateDoc: '', + Delete: '', + TransferOwner: '', Organize: { - Read: 0, + Read: '', }, Users: { - Read: 3, - Manage: 6, + Read: '', + Manage: '', }, Properties: { - Read: 4, - Create: 8, - Update: 9, - Delete: 10, + Read: '', + Create: '', + Update: '', + Delete: '', }, Settings: { - Read: 5, - Update: 7, + Read: '', + Update: '', }, }, + + // Doc Actions Doc: { - Read: 13, - Copy: 14, - Duplicate: 17, - Trash: 18, - Restore: 19, - Delete: 20, - Update: 22, - Publish: 23, - TransferOwner: 25, + Read: '', + Copy: '', + Duplicate: '', + Trash: '', + Restore: '', + Delete: '', + Update: '', + Publish: '', + TransferOwner: '', Properties: { - Read: 15, - Update: 21, + Read: '', + Update: '', }, Users: { - Read: 16, - Manage: 24, + Read: '', + Manage: '', }, }, } as const; -type ActionsKeysUnion = typeof Actions extends { - [k in infer _K extends string]: infer _V; -} - ? _V extends { - [k1 in infer _K1 extends string]: infer _V1; - } - ? _V1 extends { - [k2 in infer _K2 extends string]: number; +export const RoleActionsMap = { + WorkspaceRole: { + get [WorkspaceRole.External]() { + return [Action.Workspace.Organize.Read]; + }, + get [WorkspaceRole.Collaborator]() { + return [ + ...this[WorkspaceRole.External], + Action.Workspace.Sync, + Action.Workspace.CreateDoc, + Action.Workspace.Users.Read, + Action.Workspace.Properties.Read, + Action.Workspace.Settings.Read, + ]; + }, + get [WorkspaceRole.Admin]() { + return [ + ...this[WorkspaceRole.Collaborator], + Action.Workspace.Users.Manage, + Action.Workspace.Settings.Update, + Action.Workspace.Properties.Create, + Action.Workspace.Properties.Update, + Action.Workspace.Properties.Delete, + ]; + }, + get [WorkspaceRole.Owner]() { + return [ + ...this[WorkspaceRole.Admin], + Action.Workspace.Delete, + Action.Workspace.TransferOwner, + ]; + }, + }, + DocRole: { + get [DocRole.External]() { + return [Action.Doc.Read, Action.Doc.Copy, Action.Doc.Properties.Read]; + }, + get [DocRole.Reader]() { + return [ + ...this[DocRole.External], + Action.Doc.Users.Read, + Action.Doc.Duplicate, + ]; + }, + get [DocRole.Editor]() { + return [ + ...this[DocRole.Reader], + Action.Doc.Trash, + Action.Doc.Restore, + Action.Doc.Delete, + Action.Doc.Properties.Update, + Action.Doc.Update, + ]; + }, + get [DocRole.Manager]() { + return [ + ...this[DocRole.Editor], + Action.Doc.Publish, + Action.Doc.Users.Manage, + ]; + }, + get [DocRole.Owner]() { + return [...this[DocRole.Manager], Action.Doc.TransferOwner]; + }, + }, +} as const; + +type ResourceActionName = + `${T}.${LeafPaths<(typeof Actions)[T]>}`; + +export type WorkspaceAction = ResourceActionName<'Workspace'>; +export type DocAction = ResourceActionName<'Doc'>; +export type Action = WorkspaceAction | DocAction; +export type WorkspaceActionPermissions = { + [key in WorkspaceAction]: boolean; +}; +export type DocActionPermissions = { + [key in DocAction]: boolean; +}; + +const cache = new WeakMap(); +const buildPathReader = ( + obj: any, + isLeaf: (val: any) => boolean, + prefix?: string +): any => { + if (cache.has(obj)) { + return cache.get(obj); + } + + const reader = new Proxy(obj, { + get(target, prop) { + if (typeof prop === 'symbol') { + return undefined; } - ? _K1 extends keyof (typeof Actions)[_K] - ? _K2 extends keyof (typeof Actions)[_K][_K1] - ? `${_K}.${_K1}.${_K2}` - : never - : never - : _V1 extends number - ? `${_K}.${_K1}` - : never - : never - : never; -type ExcludeObjectKeys< - T, - Key extends keyof typeof Actions, - Split extends string, -> = T extends `${infer _K extends Key}.${infer _K1}.${infer _K2}` - ? _K1 extends keyof (typeof Actions)[_K] - ? _K2 extends keyof (typeof Actions)[_K][_K1] - ? `${_K}${Split}${_K1}${Split}${_K2}` - : never - : never - : T extends `${infer _K extends Key}.${infer _K1}` - ? _K1 extends keyof (typeof Actions)[_K] - ? (typeof Actions)[_K][_K1] extends number - ? `${_K}${Split}${_K1}` - : never - : never - : never; + const newPath = prefix ? `${prefix}.${prop}` : prop; -export type AllPossibleActionsKeys = ExcludeObjectKeys< - ActionsKeysUnion, - keyof typeof Actions, - '.' ->; + if (isLeaf(target[prop])) { + return newPath; + } -export type AllPossibleGraphQLWorkspaceActionsKeys = ExcludeObjectKeys< - ActionsKeysUnion, - 'Workspace', - '_' ->; -export type AllPossibleGraphQLDocActionsKeys = ExcludeObjectKeys< - ActionsKeysUnion, - 'Doc', - '_' ->; + return buildPathReader(target[prop], isLeaf, newPath); + }, + }); -type AllPossibleGraphQLActionsKeys = - | AllPossibleGraphQLWorkspaceActionsKeys - | AllPossibleGraphQLDocActionsKeys; + cache.set(obj, reader); + return reader; +}; -export const ActionsKeys: AllPossibleActionsKeys[] = [ - 'Workspace.Organize.Read', - 'Workspace.Sync', - 'Workspace.CreateDoc', - 'Workspace.Users.Read', - 'Workspace.Properties.Read', - 'Workspace.Settings.Read', - 'Workspace.Users.Manage', - 'Workspace.Settings.Update', - 'Workspace.Properties.Create', - 'Workspace.Properties.Update', - 'Workspace.Properties.Delete', - 'Workspace.Delete', - 'Workspace.TransferOwner', - 'Doc.Read', - 'Doc.Copy', - 'Doc.Properties.Read', - 'Doc.Users.Read', - 'Doc.Duplicate', - 'Doc.Trash', - 'Doc.Restore', - 'Doc.Delete', - 'Doc.Properties.Update', - 'Doc.Update', - 'Doc.Publish', - 'Doc.Users.Manage', - 'Doc.TransferOwner', -] as const; - -assert( - ActionsKeys.length === Actions.Doc.TransferOwner + 1, - 'ActionsKeys length is not correct' +// Create the proxy that returns the path string +export const Action: LeafVisitor = buildPathReader( + Actions, + val => typeof val === 'string' ); -function permissionKeyToGraphQLKey(key: string) { - const k = key.split('.'); - return k.join('_') as keyof PermissionsList; +export const WORKSPACE_ACTIONS = + RoleActionsMap.WorkspaceRole[WorkspaceRole.Owner]; +export const DOC_ACTIONS = RoleActionsMap.DocRole[DocRole.Owner]; + +export function mapWorkspaceRoleToPermissions(workspaceRole: WorkspaceRole) { + const permissions = WORKSPACE_ACTIONS.reduce( + (map, action) => { + map[action] = false; + return map; + }, + {} as Record + ); + + RoleActionsMap.WorkspaceRole[workspaceRole].forEach(action => { + permissions[action] = true; + }); + + return permissions; } -const DefaultActionsMap = Object.fromEntries( - ActionsKeys.map(key => [permissionKeyToGraphQLKey(key), false]) -) as PermissionsList; +export function mapDocRoleToPermissions(docRole: DocRole) { + const permissions = DOC_ACTIONS.reduce( + (map, action) => { + map[action] = false; + return map; + }, + {} as Record + ); -export type WorkspacePermissionsList = { - [k in AllPossibleGraphQLWorkspaceActionsKeys]: boolean; -}; - -export type PermissionsList = { - [key in AllPossibleGraphQLActionsKeys]: boolean; -}; - -export function mapWorkspaceRoleToWorkspaceActions( - workspaceRole: WorkspaceRole -) { - const permissionList = { ...DefaultActionsMap }; - (RoleActionsMap.WorkspaceRole[workspaceRole] ?? []).forEach(action => { - permissionList[permissionKeyToGraphQLKey(ActionsKeys[action])] = true; + RoleActionsMap.DocRole[docRole].forEach(action => { + permissions[action] = true; }); - return Object.fromEntries( - Object.entries(permissionList).filter(([k, _]) => - k.startsWith('Workspace_') + + return permissions; +} + +/** + * Exchange the real operatable [DocRole] with [WorkspaceRole]. + * + * Some [WorkspaceRole] has higher permission than the specified [DocRole]. + * for example the owner of the workspace can edit all the docs by default, + * So [WorkspaceRole.Owner] will fixup [Doc.External] to [Doc.Manager] + * + * @example + * + * // Owner of the workspace but not specified a role in the doc + * fixupDocRole(WorkspaceRole.Owner, DocRole.External) // returns DocRole.Manager + */ +export function fixupDocRole( + workspaceRole: WorkspaceRole = WorkspaceRole.External, + docRole: DocRole = DocRole.External +) { + switch (workspaceRole) { + case WorkspaceRole.External: + // Workspace External user won't be able to have any high permission doc role + // set the maximum to Editor in case we have [Can Edit with share link] feature + return Math.min(DocRole.Editor, docRole); + // Workspace Owner will always fallback to Doc Owner + case WorkspaceRole.Owner: + return DocRole.Owner; + // Workspace Admin will always fallback to Doc Manager + case WorkspaceRole.Admin: + return Math.max(DocRole.Manager, docRole); + default: + return docRole; + } +} + +/** + * a map from [WorkspaceRole] to { [WorkspaceActionName]: boolean } + */ +const WorkspaceRolePermissionsMap = new Map( + Object.values(WorkspaceRole) + .filter(r => typeof r === 'number') + .map( + role => + [role, mapWorkspaceRoleToPermissions(role as WorkspaceRole)] as [ + WorkspaceRole, + Record, + ] ) +); + +/** + * a map from [WorkspaceActionName] to required [WorkspaceRole] + * + * @testonly use [workspaceActionRequiredRole] instead + */ +export const WORKSPACE_ACTION_TO_MINIMAL_ROLE_MAP = new Map( + RoleActionsMap.WorkspaceRole[WorkspaceRole.Owner].map( + action => + [ + action, + Math.min( + ...[...WorkspaceRolePermissionsMap.entries()] + .filter(([_, permissions]) => permissions[action]) + .map(([role, _]) => role) + ), + ] as [WorkspaceAction, WorkspaceRole] + ) +); + +/** + * a map from [DocRole] to { [DocActionName]: boolean } + */ +const DocRolePermissionsMap = new Map( + Object.values(DocRole) + .filter(r => typeof r === 'number') + .map(docRole => { + const permissions = mapDocRoleToPermissions(docRole as DocRole); + return [docRole, permissions] as [DocRole, Record]; + }) +); + +/** + * a map from [DocActionName] to required [DocRole] + * @testonly use [docActionRequiredRole] instead + */ +export const DOC_ACTION_TO_MINIMAL_ROLE_MAP = new Map( + RoleActionsMap.DocRole[DocRole.Owner].map( + action => + [ + action, + Math.min( + ...[...DocRolePermissionsMap.entries()] + .filter(([_, permissions]) => permissions[action]) + .map(([role, _]) => role) + ), + ] as [DocAction, DocRole] + ) +); + +export function docActionRequiredRole(action: DocAction): DocRole { + return ( + DOC_ACTION_TO_MINIMAL_ROLE_MAP.get(action) ?? + /* if we forget to put new action to [RoleActionsMap.DocRole] */ DocRole.Owner ); } -export function mapRoleToActions( - workspaceRole?: WorkspaceRole, - docRole?: DocRole -) { - const workspaceActions = workspaceRole - ? (RoleActionsMap.WorkspaceRole[workspaceRole] ?? []) - : []; - const docActions = (function () { - // Doc owner/manager permission can not be overridden by workspace role - if (docRole !== undefined && docRole >= DocRole.Manager) { - return RoleActionsMap.DocRole[docRole]; - } - switch (workspaceRole) { - case WorkspaceRole.Admin: - case WorkspaceRole.Owner: - return RoleActionsMap.DocRole[DocRole.Manager]; - case WorkspaceRole.Collaborator: - return RoleActionsMap.DocRole[DocRole.Editor]; - default: - return docRole !== undefined - ? (RoleActionsMap.DocRole[docRole] ?? []) - : []; - } - })(); - const permissionList = { ...DefaultActionsMap }; - [...workspaceActions, ...docActions].forEach(action => { - permissionList[permissionKeyToGraphQLKey(ActionsKeys[action])] = true; - }); - return permissionList; -} - -export function findMinimalDocRole( - action: AllPossibleGraphQLDocActionsKeys -): DocRole { - const [_, actionKey, actionKey2] = action.split('_'); - - const actionValue: number = actionKey2 - ? // @ts-expect-error Actions[actionKey] exists - Actions.Doc[actionKey][actionKey2] - : // @ts-expect-error Actions[actionKey] exists - Actions.Doc[actionKey]; - if (actionValue <= Actions.Doc.Properties.Read) { - return DocRole.External; - } - if (actionValue <= Actions.Doc.Duplicate) { - return DocRole.Reader; - } - if (actionValue <= Actions.Doc.Update) { - return DocRole.Editor; - } - if (actionValue <= Actions.Doc.Users.Manage) { - return DocRole.Manager; - } - return DocRole.Owner; -} - -export function requiredWorkspaceRoleByDocRole( - docRole: DocRole +/** + * Useful when a workspace member doesn't have a specified role in the doc, but want to check the permission of the action + */ +export function docActionRequiredWorkspaceRole( + action: DocAction ): WorkspaceRole { + const docRole = docActionRequiredRole(action); + switch (docRole) { case DocRole.Owner: return WorkspaceRole.Owner; @@ -260,69 +348,11 @@ export function requiredWorkspaceRoleByDocRole( } } -export const RoleActionsMap = { - WorkspaceRole: { - get [WorkspaceRole.External]() { - return [Actions.Workspace.Organize.Read]; - }, - get [WorkspaceRole.Collaborator]() { - return [ - ...this[WorkspaceRole.External], - Actions.Workspace.Sync, - Actions.Workspace.CreateDoc, - Actions.Workspace.Users.Read, - Actions.Workspace.Properties.Read, - Actions.Workspace.Settings.Read, - ]; - }, - get [WorkspaceRole.Admin]() { - return [ - ...this[WorkspaceRole.Collaborator], - Actions.Workspace.Users.Manage, - Actions.Workspace.Settings.Update, - Actions.Workspace.Properties.Create, - Actions.Workspace.Properties.Update, - Actions.Workspace.Properties.Delete, - ]; - }, - get [WorkspaceRole.Owner]() { - return [ - ...this[WorkspaceRole.Admin], - Actions.Workspace.Delete, - Actions.Workspace.TransferOwner, - ]; - }, - }, - DocRole: { - get [DocRole.External]() { - return [Actions.Doc.Read, Actions.Doc.Copy, Actions.Doc.Properties.Read]; - }, - get [DocRole.Reader]() { - return [ - ...this[DocRole.External], - Actions.Doc.Users.Read, - Actions.Doc.Duplicate, - ]; - }, - get [DocRole.Editor]() { - return [ - ...this[DocRole.Reader], - Actions.Doc.Trash, - Actions.Doc.Restore, - Actions.Doc.Delete, - Actions.Doc.Properties.Update, - Actions.Doc.Update, - ]; - }, - get [DocRole.Manager]() { - return [ - ...this[DocRole.Editor], - Actions.Doc.Publish, - Actions.Doc.Users.Manage, - ]; - }, - get [DocRole.Owner]() { - return [...this[DocRole.Manager], Actions.Doc.TransferOwner]; - }, - }, -} as const; +export function workspaceActionRequiredRole( + action: WorkspaceAction +): WorkspaceRole { + return ( + WORKSPACE_ACTION_TO_MINIMAL_ROLE_MAP.get(action) ?? + /* if we forget to put new action to [RoleActionsMap.WorkspaceRole] */ WorkspaceRole.Owner + ); +} diff --git a/packages/backend/server/src/core/workspaces/controller.ts b/packages/backend/server/src/core/workspaces/controller.ts index 0d4e8eaaa2..3638571391 100644 --- a/packages/backend/server/src/core/workspaces/controller.ts +++ b/packages/backend/server/src/core/workspaces/controller.ts @@ -147,7 +147,7 @@ export class WorkspacesController { await this.permission.checkPagePermission( docId.workspace, docId.guid, - 'Doc_Read', + 'Doc.Read', user.id ); diff --git a/packages/backend/server/src/core/workspaces/resolvers/history.ts b/packages/backend/server/src/core/workspaces/resolvers/history.ts index d5afdf0a9e..b3a858624c 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/history.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/history.ts @@ -79,7 +79,7 @@ export class DocHistoryResolver { await this.permission.checkPagePermission( docId.workspace, docId.guid, - 'Doc_Restore', + 'Doc.Update', user.id ); diff --git a/packages/backend/server/src/core/workspaces/resolvers/page.ts b/packages/backend/server/src/core/workspaces/resolvers/page.ts index fd3996a0d4..25cb330eca 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/page.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/page.ts @@ -21,19 +21,22 @@ import { ExpectToRevokePublicPage, ExpectToUpdateDocUserRole, PageIsNotPublic, + registerObjectType, } from '../../../base'; import { CurrentUser } from '../../auth'; import { + DOC_ACTIONS, + type DocActionPermissions, DocRole, + fixupDocRole, + mapDocRoleToPermissions, PermissionService, PublicPageMode, WorkspaceRole, } from '../../permission'; -import { mapRoleToActions, PermissionsList } from '../../permission/types'; import { UserType } from '../../user'; import { DocID } from '../../utils/doc'; import { WorkspaceType } from '../types'; -import { WorkspacePermissions } from './workspace'; registerEnumType(PublicPageMode, { name: 'PublicPageMode', @@ -130,38 +133,12 @@ class GrantedDocUsersConnection { pageInfo!: PageInfo; } -@ObjectType() -export class RolePermissions - extends WorkspacePermissions - implements PermissionsList -{ - @Field() - Doc_Read!: boolean; - @Field() - Doc_Copy!: boolean; - @Field() - Doc_Properties_Read!: boolean; - @Field() - Doc_Users_Read!: boolean; - @Field() - Doc_Duplicate!: boolean; - @Field() - Doc_Trash!: boolean; - @Field() - Doc_Restore!: boolean; - @Field() - Doc_Delete!: boolean; - @Field() - Doc_Properties_Update!: boolean; - @Field() - Doc_Update!: boolean; - @Field() - Doc_Publish!: boolean; - @Field() - Doc_Users_Manage!: boolean; - @Field() - Doc_TransferOwner!: boolean; -} +const DocPermissions = registerObjectType( + Object.fromEntries( + DOC_ACTIONS.map(action => [action.replaceAll('.', '_'), Boolean]) + ), + { name: 'DocPermissions' } +); @ObjectType() class DocType { @@ -174,8 +151,8 @@ class DocType { @Field(() => DocRole) role!: DocRole; - @Field(() => RolePermissions) - permissions!: RolePermissions; + @Field(() => DocPermissions) + permissions!: DocActionPermissions; } @Resolver(() => WorkspaceType) @@ -278,9 +255,8 @@ export class PagePermissionResolver { id: pageId, public: page?.public ?? false, role: permission?.type ?? DocRole.External, - permissions: mapRoleToActions( - workspacePermission?.type, - permission?.type + permissions: mapDocRoleToPermissions( + fixupDocRole(workspacePermission?.type, permission?.type) ), }; } @@ -395,7 +371,7 @@ export class PagePermissionResolver { await this.permission.checkPagePermission( docId.workspace, docId.guid, - 'Doc_Publish', + 'Doc.Publish', user.id ); @@ -443,7 +419,7 @@ export class PagePermissionResolver { await this.permission.checkPagePermission( docId.workspace, docId.guid, - 'Doc_Publish', + 'Doc.Publish', user.id ); @@ -491,7 +467,7 @@ export class PagePermissionResolver { await this.permission.checkPagePermission( doc.workspace, doc.guid, - 'Doc_Users_Manage', + 'Doc.Users.Manage', user.id ); await this.permission.grantPagePermission( diff --git a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts index d735000fca..1986753555 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/workspace.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/workspace.ts @@ -22,6 +22,7 @@ import { InternalServerError, MemberQuotaExceeded, QueryTooLong, + registerObjectType, RequestMutex, SpaceAccessDenied, SpaceNotFound, @@ -33,11 +34,13 @@ import { import { Models } from '../../../models'; import { CurrentUser, Public } from '../../auth'; import { type Editor, PgWorkspaceDocStorageAdapter } from '../../doc'; -import { PermissionService, WorkspaceRole } from '../../permission'; import { - mapWorkspaceRoleToWorkspaceActions, - WorkspacePermissionsList, -} from '../../permission/types'; + mapWorkspaceRoleToPermissions, + PermissionService, + WORKSPACE_ACTIONS, + type WorkspaceActionPermissions, + WorkspaceRole, +} from '../../permission'; import { QuotaService, WorkspaceQuotaType } from '../../quota'; import { UserType } from '../../user'; import { @@ -72,35 +75,12 @@ class WorkspacePageMeta { updatedBy!: EditorType | null; } -@ObjectType() -export class WorkspacePermissions implements WorkspacePermissionsList { - @Field() - Workspace_Organize_Read!: boolean; - @Field() - Workspace_Sync!: boolean; - @Field() - Workspace_CreateDoc!: boolean; - @Field() - Workspace_Users_Read!: boolean; - @Field() - Workspace_Properties_Read!: boolean; - @Field() - Workspace_Settings_Read!: boolean; - @Field() - Workspace_Users_Manage!: boolean; - @Field() - Workspace_Settings_Update!: boolean; - @Field() - Workspace_Properties_Create!: boolean; - @Field() - Workspace_Properties_Update!: boolean; - @Field() - Workspace_Properties_Delete!: boolean; - @Field() - Workspace_Delete!: boolean; - @Field() - Workspace_TransferOwner!: boolean; -} +const WorkspacePermissions = registerObjectType( + Object.fromEntries( + WORKSPACE_ACTIONS.map(action => [action.replaceAll('.', '_'), Boolean]) + ), + { name: 'WorkspacePermissions' } +); @ObjectType() export class WorkspaceRolePermissions { @@ -108,7 +88,7 @@ export class WorkspaceRolePermissions { role!: WorkspaceRole; @Field(() => WorkspacePermissions) - permissions!: WorkspacePermissions; + permissions!: WorkspaceActionPermissions; } /** @@ -363,7 +343,7 @@ export class WorkspaceResolver { } return { role: workspace.type, - permissions: mapWorkspaceRoleToWorkspaceActions(workspace.type), + permissions: mapWorkspaceRoleToPermissions(workspace.type), }; } diff --git a/packages/backend/server/src/plugins/copilot/resolver.ts b/packages/backend/server/src/plugins/copilot/resolver.ts index 5300d84e3a..afd5d89a43 100644 --- a/packages/backend/server/src/plugins/copilot/resolver.ts +++ b/packages/backend/server/src/plugins/copilot/resolver.ts @@ -335,7 +335,7 @@ export class CopilotResolver { await this.permissions.checkCloudPagePermission( workspaceId, docId, - 'Doc_Read', + 'Doc.Read', user.id ); } else { @@ -369,7 +369,7 @@ export class CopilotResolver { await this.permissions.checkCloudPagePermission( options.workspaceId, options.docId, - 'Doc_Read', + 'Doc.Update', user.id ); const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`; @@ -403,7 +403,7 @@ export class CopilotResolver { await this.permissions.checkCloudPagePermission( workspaceId, docId, - 'Doc_Update', + 'Doc.Update', user.id ); const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${workspaceId}`; @@ -431,7 +431,7 @@ export class CopilotResolver { await this.permissions.checkCloudPagePermission( options.workspaceId, options.docId, - 'Doc_Copy', + 'Doc.Update', user.id ); const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`; @@ -460,7 +460,7 @@ export class CopilotResolver { await this.permissions.checkCloudPagePermission( options.workspaceId, options.docId, - 'Doc_Delete', + 'Doc.Update', user.id ); if (!options.sessionIds.length) { diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 8fda75ec32..15baaba95c 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -204,6 +204,22 @@ type DocNotFoundDataType { spaceId: String! } +type DocPermissions { + Doc_Copy: Boolean! + Doc_Delete: Boolean! + Doc_Duplicate: Boolean! + Doc_Properties_Read: Boolean! + Doc_Properties_Update: Boolean! + Doc_Publish: Boolean! + Doc_Read: Boolean! + Doc_Restore: Boolean! + Doc_TransferOwner: Boolean! + Doc_Trash: Boolean! + Doc_Update: Boolean! + Doc_Users_Manage: Boolean! + Doc_Users_Read: Boolean! +} + """User permission in doc""" enum DocRole { Editor @@ -215,7 +231,7 @@ enum DocRole { type DocType { id: String! - permissions: RolePermissions! + permissions: DocPermissions! public: Boolean! role: DocRole! } @@ -778,35 +794,6 @@ type RemoveAvatar { success: Boolean! } -type RolePermissions { - Doc_Copy: Boolean! - Doc_Delete: Boolean! - Doc_Duplicate: Boolean! - Doc_Properties_Read: Boolean! - Doc_Properties_Update: Boolean! - Doc_Publish: Boolean! - Doc_Read: Boolean! - Doc_Restore: Boolean! - Doc_TransferOwner: Boolean! - Doc_Trash: Boolean! - Doc_Update: Boolean! - Doc_Users_Manage: Boolean! - Doc_Users_Read: Boolean! - Workspace_CreateDoc: Boolean! - Workspace_Delete: Boolean! - Workspace_Organize_Read: Boolean! - Workspace_Properties_Create: Boolean! - Workspace_Properties_Delete: Boolean! - Workspace_Properties_Read: Boolean! - Workspace_Properties_Update: Boolean! - Workspace_Settings_Read: Boolean! - Workspace_Settings_Update: Boolean! - Workspace_Sync: Boolean! - Workspace_TransferOwner: Boolean! - Workspace_Users_Manage: Boolean! - Workspace_Users_Read: Boolean! -} - type RuntimeConfigNotFoundDataType { key: String! }