feat(server): doc level permission (#9760)

close CLOUD-89 CLOUD-90 CLOUD-91 CLOUD-92
This commit is contained in:
Brooooooklyn
2025-02-05 07:06:57 +00:00
parent 64de83b13d
commit abeff8bb1a
36 changed files with 2257 additions and 324 deletions

View File

@@ -141,7 +141,7 @@ model WorkspaceUserPermission {
id String @id @default(uuid()) @db.VarChar id String @id @default(uuid()) @db.VarChar
workspaceId String @map("workspace_id") @db.VarChar workspaceId String @map("workspace_id") @db.VarChar
userId String @map("user_id") @db.VarChar userId String @map("user_id") @db.VarChar
// Read/Write // Workspace Role, Owner/Admin/Collaborator/External
type Int @db.SmallInt type Int @db.SmallInt
/// @deprecated Whether the permission invitation is accepted by the user /// @deprecated Whether the permission invitation is accepted by the user
accepted Boolean @default(false) accepted Boolean @default(false)
@@ -165,7 +165,7 @@ model WorkspacePageUserPermission {
workspaceId String @map("workspace_id") @db.VarChar workspaceId String @map("workspace_id") @db.VarChar
pageId String @map("page_id") @db.VarChar pageId String @map("page_id") @db.VarChar
userId String @map("user_id") @db.VarChar userId String @map("user_id") @db.VarChar
// Read/Write // External/Reader/Editor/Manager/Owner
type Int @db.SmallInt type Int @db.SmallInt
/// Whether the permission invitation is accepted by the user /// Whether the permission invitation is accepted by the user
accepted Boolean @default(false) accepted Boolean @default(false)

View File

@@ -3,7 +3,8 @@ import { PrismaClient } from '@prisma/client';
import ava, { TestFn } from 'ava'; import ava, { TestFn } from 'ava';
import { Config } from '../../base/config'; import { Config } from '../../base/config';
import { Permission, PublicPageMode } from '../../models/common'; import { WorkspaceRole } from '../../core/permission';
import { PublicPageMode } from '../../models/common';
import { PageModel } from '../../models/page'; import { PageModel } from '../../models/page';
import { type User, UserModel } from '../../models/user'; import { type User, UserModel } from '../../models/user';
import { type Workspace, WorkspaceModel } from '../../models/workspace'; import { type Workspace, WorkspaceModel } from '../../models/workspace';
@@ -131,7 +132,7 @@ test('should grant a member to access a page', async t => {
workspace.id, workspace.id,
'page1', 'page1',
user.id, user.id,
Permission.Write WorkspaceRole.Collaborator
); );
t.false(hasAccess); t.false(hasAccess);
// grant write permission // grant write permission
@@ -139,20 +140,20 @@ test('should grant a member to access a page', async t => {
workspace.id, workspace.id,
'page1', 'page1',
user.id, user.id,
Permission.Write WorkspaceRole.Collaborator
); );
hasAccess = await t.context.page.isMember( hasAccess = await t.context.page.isMember(
workspace.id, workspace.id,
'page1', 'page1',
user.id, user.id,
Permission.Write WorkspaceRole.Collaborator
); );
t.true(hasAccess); t.true(hasAccess);
hasAccess = await t.context.page.isMember( hasAccess = await t.context.page.isMember(
workspace.id, workspace.id,
'page1', 'page1',
user.id, user.id,
Permission.Read WorkspaceRole.Collaborator
); );
t.true(hasAccess); t.true(hasAccess);
// delete member // delete member
@@ -174,14 +175,14 @@ test('should change the page owner', async t => {
workspace.id, workspace.id,
'page1', 'page1',
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
); );
t.true( t.true(
await t.context.page.isMember( await t.context.page.isMember(
workspace.id, workspace.id,
'page1', 'page1',
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
) )
); );
@@ -193,14 +194,14 @@ test('should change the page owner', async t => {
workspace.id, workspace.id,
'page1', 'page1',
otherUser.id, otherUser.id,
Permission.Owner WorkspaceRole.Owner
); );
t.true( t.true(
await t.context.page.isMember( await t.context.page.isMember(
workspace.id, workspace.id,
'page1', 'page1',
otherUser.id, otherUser.id,
Permission.Owner WorkspaceRole.Owner
) )
); );
t.false( t.false(
@@ -208,7 +209,7 @@ test('should change the page owner', async t => {
workspace.id, workspace.id,
'page1', 'page1',
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
) )
); );
}); });
@@ -221,7 +222,7 @@ test('should not delete owner from page', async t => {
workspace.id, workspace.id,
'page1', 'page1',
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
); );
const count = await t.context.page.deleteMember( const count = await t.context.page.deleteMember(
workspace.id, workspace.id,

View File

@@ -4,7 +4,7 @@ import ava, { TestFn } from 'ava';
import Sinon from 'sinon'; import Sinon from 'sinon';
import { EmailAlreadyUsed, EventBus } from '../../base'; import { EmailAlreadyUsed, EventBus } from '../../base';
import { Permission } from '../../models/common'; import { WorkspaceRole } from '../../core/permission';
import { UserModel } from '../../models/user'; import { UserModel } from '../../models/user';
import { WorkspaceMemberStatus } from '../../models/workspace'; import { WorkspaceMemberStatus } from '../../models/workspace';
import { createTestingModule, initTestingDB } from '../utils'; import { createTestingModule, initTestingDB } from '../utils';
@@ -263,7 +263,7 @@ test('should trigger user.deleted event', async t => {
public: false, public: false,
}, },
}, },
type: Permission.Owner, type: WorkspaceRole.Owner,
status: WorkspaceMemberStatus.Accepted, status: WorkspaceMemberStatus.Accepted,
}, },
}, },

View File

@@ -4,7 +4,7 @@ import ava, { TestFn } from 'ava';
import Sinon from 'sinon'; import Sinon from 'sinon';
import { Config, EventBus } from '../../base'; import { Config, EventBus } from '../../base';
import { Permission } from '../../models/common'; import { WorkspaceRole } from '../../core/permission';
import { UserModel } from '../../models/user'; import { UserModel } from '../../models/user';
import { WorkspaceModel } from '../../models/workspace'; import { WorkspaceModel } from '../../models/workspace';
import { createTestingModule, initTestingDB } from '../utils'; import { createTestingModule, initTestingDB } from '../utils';
@@ -92,25 +92,25 @@ test('should workspace owner has all permissions', async t => {
let allowed = await t.context.workspace.isMember( let allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
); );
t.is(allowed, true); t.is(allowed, true);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
user.id, user.id,
Permission.Admin WorkspaceRole.Admin
); );
t.is(allowed, true); t.is(allowed, true);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
user.id, user.id,
Permission.Write WorkspaceRole.Collaborator
); );
t.is(allowed, true); t.is(allowed, true);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
user.id, user.id,
Permission.Read WorkspaceRole.Collaborator
); );
t.is(allowed, true); t.is(allowed, true);
}); });
@@ -127,32 +127,32 @@ test('should workspace admin has all permissions except owner', async t => {
data: { data: {
workspaceId: workspace.id, workspaceId: workspace.id,
userId: otherUser.id, userId: otherUser.id,
type: Permission.Admin, type: WorkspaceRole.Admin,
status: WorkspaceMemberStatus.Accepted, status: WorkspaceMemberStatus.Accepted,
}, },
}); });
let allowed = await t.context.workspace.isMember( let allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Owner WorkspaceRole.Owner
); );
t.is(allowed, false); t.is(allowed, false);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Admin WorkspaceRole.Admin
); );
t.is(allowed, true); t.is(allowed, true);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Write WorkspaceRole.Collaborator
); );
t.is(allowed, true); t.is(allowed, true);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read WorkspaceRole.Collaborator
); );
t.is(allowed, true); t.is(allowed, true);
}); });
@@ -169,32 +169,32 @@ test('should workspace write has write and read permissions', async t => {
data: { data: {
workspaceId: workspace.id, workspaceId: workspace.id,
userId: otherUser.id, userId: otherUser.id,
type: Permission.Write, type: WorkspaceRole.Collaborator,
status: WorkspaceMemberStatus.Accepted, status: WorkspaceMemberStatus.Accepted,
}, },
}); });
let allowed = await t.context.workspace.isMember( let allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Owner WorkspaceRole.Owner
); );
t.is(allowed, false); t.is(allowed, false);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Admin WorkspaceRole.Admin
); );
t.is(allowed, false); t.is(allowed, false);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Write WorkspaceRole.Collaborator
); );
t.is(allowed, true); t.is(allowed, true);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read WorkspaceRole.Collaborator
); );
t.is(allowed, true); t.is(allowed, true);
}); });
@@ -211,32 +211,26 @@ test('should workspace read has read permission only', async t => {
data: { data: {
workspaceId: workspace.id, workspaceId: workspace.id,
userId: otherUser.id, userId: otherUser.id,
type: Permission.Read, type: WorkspaceRole.Collaborator,
status: WorkspaceMemberStatus.Accepted, status: WorkspaceMemberStatus.Accepted,
}, },
}); });
let allowed = await t.context.workspace.isMember( let allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Owner WorkspaceRole.Owner
); );
t.is(allowed, false); t.is(allowed, false);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Admin WorkspaceRole.Admin
); );
t.is(allowed, false); t.is(allowed, false);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Write WorkspaceRole.Collaborator
);
t.is(allowed, false);
allowed = await t.context.workspace.isMember(
workspace.id,
otherUser.id,
Permission.Read
); );
t.is(allowed, true); t.is(allowed, true);
}); });
@@ -252,25 +246,25 @@ test('should user not in workspace has no permissions', async t => {
let allowed = await t.context.workspace.isMember( let allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Owner WorkspaceRole.Owner
); );
t.is(allowed, false); t.is(allowed, false);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Admin WorkspaceRole.Admin
); );
t.is(allowed, false); t.is(allowed, false);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Write WorkspaceRole.Collaborator
); );
t.is(allowed, false); t.is(allowed, false);
allowed = await t.context.workspace.isMember( allowed = await t.context.workspace.isMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read WorkspaceRole.Collaborator
); );
t.is(allowed, false); t.is(allowed, false);
}); });
@@ -313,7 +307,7 @@ test('should grant member with read permission and Pending status by default', a
); );
t.is(member1.workspaceId, workspace.id); t.is(member1.workspaceId, workspace.id);
t.is(member1.userId, otherUser.id); t.is(member1.userId, otherUser.id);
t.is(member1.type, Permission.Read); t.is(member1.type, WorkspaceRole.Collaborator);
t.is(member1.status, WorkspaceMemberStatus.Pending); t.is(member1.status, WorkspaceMemberStatus.Pending);
// grant again should do nothing // grant again should do nothing
@@ -344,18 +338,18 @@ test('should grant Pending status member to Accepted status', async t => {
); );
t.is(member1.workspaceId, workspace.id); t.is(member1.workspaceId, workspace.id);
t.is(member1.userId, otherUser.id); t.is(member1.userId, otherUser.id);
t.is(member1.type, Permission.Read); t.is(member1.type, WorkspaceRole.Collaborator);
t.is(member1.status, WorkspaceMemberStatus.Pending); t.is(member1.status, WorkspaceMemberStatus.Pending);
const member2 = await t.context.workspace.grantMember( const member2 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
t.is(member2.workspaceId, workspace.id); t.is(member2.workspaceId, workspace.id);
t.is(member2.userId, otherUser.id); t.is(member2.userId, otherUser.id);
t.is(member2.type, Permission.Read); t.is(member2.type, WorkspaceRole.Collaborator);
t.is(member2.status, WorkspaceMemberStatus.Accepted); t.is(member2.status, WorkspaceMemberStatus.Accepted);
}); });
@@ -370,27 +364,27 @@ test('should grant new owner and change exists owner to admin', async t => {
const member1 = await t.context.workspace.grantMember( const member1 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
t.is(member1.workspaceId, workspace.id); t.is(member1.workspaceId, workspace.id);
t.is(member1.userId, otherUser.id); t.is(member1.userId, otherUser.id);
t.is(member1.type, Permission.Read); t.is(member1.type, WorkspaceRole.Collaborator);
t.is(member1.status, WorkspaceMemberStatus.Accepted); t.is(member1.status, WorkspaceMemberStatus.Accepted);
const member2 = await t.context.workspace.grantMember( const member2 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Owner, WorkspaceRole.Owner,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
t.is(member2.workspaceId, workspace.id); t.is(member2.workspaceId, workspace.id);
t.is(member2.userId, otherUser.id); t.is(member2.userId, otherUser.id);
t.is(member2.type, Permission.Owner); t.is(member2.type, WorkspaceRole.Owner);
t.is(member2.status, WorkspaceMemberStatus.Accepted); t.is(member2.status, WorkspaceMemberStatus.Accepted);
// check old owner // check old owner
const owner = await t.context.workspace.getMember(workspace.id, user.id); const owner = await t.context.workspace.getMember(workspace.id, user.id);
t.is(owner!.type, Permission.Admin); t.is(owner!.type, WorkspaceRole.Admin);
t.is(owner!.status, WorkspaceMemberStatus.Accepted); t.is(owner!.status, WorkspaceMemberStatus.Accepted);
}); });
@@ -405,23 +399,23 @@ test('should grant write permission on exists member', async t => {
const member1 = await t.context.workspace.grantMember( const member1 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
t.is(member1.workspaceId, workspace.id); t.is(member1.workspaceId, workspace.id);
t.is(member1.userId, otherUser.id); t.is(member1.userId, otherUser.id);
t.is(member1.type, Permission.Read); t.is(member1.type, WorkspaceRole.Collaborator);
t.is(member1.status, WorkspaceMemberStatus.Accepted); t.is(member1.status, WorkspaceMemberStatus.Accepted);
const member2 = await t.context.workspace.grantMember( const member2 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Write, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
t.is(member2.workspaceId, workspace.id); t.is(member2.workspaceId, workspace.id);
t.is(member2.userId, otherUser.id); t.is(member2.userId, otherUser.id);
t.is(member2.type, Permission.Write); t.is(member2.type, WorkspaceRole.Collaborator);
t.is(member2.status, WorkspaceMemberStatus.Accepted); t.is(member2.status, WorkspaceMemberStatus.Accepted);
}); });
@@ -436,23 +430,23 @@ test('should grant UnderReview status member to Accepted status', async t => {
const member1 = await t.context.workspace.grantMember( const member1 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.UnderReview WorkspaceMemberStatus.UnderReview
); );
t.is(member1.workspaceId, workspace.id); t.is(member1.workspaceId, workspace.id);
t.is(member1.userId, otherUser.id); t.is(member1.userId, otherUser.id);
t.is(member1.type, Permission.Read); t.is(member1.type, WorkspaceRole.Collaborator);
t.is(member1.status, WorkspaceMemberStatus.UnderReview); t.is(member1.status, WorkspaceMemberStatus.UnderReview);
const member2 = await t.context.workspace.grantMember( const member2 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
t.is(member2.workspaceId, workspace.id); t.is(member2.workspaceId, workspace.id);
t.is(member2.userId, otherUser.id); t.is(member2.userId, otherUser.id);
t.is(member2.type, Permission.Read); t.is(member2.type, WorkspaceRole.Collaborator);
t.is(member2.status, WorkspaceMemberStatus.Accepted); t.is(member2.status, WorkspaceMemberStatus.Accepted);
}); });
@@ -467,23 +461,23 @@ test('should grant NeedMoreSeat status member to Pending status', async t => {
const member1 = await t.context.workspace.grantMember( const member1 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.NeedMoreSeat WorkspaceMemberStatus.NeedMoreSeat
); );
t.is(member1.workspaceId, workspace.id); t.is(member1.workspaceId, workspace.id);
t.is(member1.userId, otherUser.id); t.is(member1.userId, otherUser.id);
t.is(member1.type, Permission.Read); t.is(member1.type, WorkspaceRole.Collaborator);
t.is(member1.status, WorkspaceMemberStatus.NeedMoreSeat); t.is(member1.status, WorkspaceMemberStatus.NeedMoreSeat);
const member2 = await t.context.workspace.grantMember( const member2 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Pending WorkspaceMemberStatus.Pending
); );
t.is(member2.workspaceId, workspace.id); t.is(member2.workspaceId, workspace.id);
t.is(member2.userId, otherUser.id); t.is(member2.userId, otherUser.id);
t.is(member2.type, Permission.Read); t.is(member2.type, WorkspaceRole.Collaborator);
t.is(member2.status, WorkspaceMemberStatus.Pending); t.is(member2.status, WorkspaceMemberStatus.Pending);
}); });
@@ -498,23 +492,23 @@ test('should grant NeedMoreSeatAndReview status member to UnderReview status', a
const member1 = await t.context.workspace.grantMember( const member1 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.NeedMoreSeatAndReview WorkspaceMemberStatus.NeedMoreSeatAndReview
); );
t.is(member1.workspaceId, workspace.id); t.is(member1.workspaceId, workspace.id);
t.is(member1.userId, otherUser.id); t.is(member1.userId, otherUser.id);
t.is(member1.type, Permission.Read); t.is(member1.type, WorkspaceRole.Collaborator);
t.is(member1.status, WorkspaceMemberStatus.NeedMoreSeatAndReview); t.is(member1.status, WorkspaceMemberStatus.NeedMoreSeatAndReview);
const member2 = await t.context.workspace.grantMember( const member2 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.UnderReview WorkspaceMemberStatus.UnderReview
); );
t.is(member2.workspaceId, workspace.id); t.is(member2.workspaceId, workspace.id);
t.is(member2.userId, otherUser.id); t.is(member2.userId, otherUser.id);
t.is(member2.type, Permission.Read); t.is(member2.type, WorkspaceRole.Collaborator);
t.is(member2.status, WorkspaceMemberStatus.UnderReview); t.is(member2.status, WorkspaceMemberStatus.UnderReview);
}); });
@@ -532,19 +526,19 @@ test('should grant Pending status member to write permission and Accepted status
); );
t.is(member1.workspaceId, workspace.id); t.is(member1.workspaceId, workspace.id);
t.is(member1.userId, otherUser.id); t.is(member1.userId, otherUser.id);
t.is(member1.type, Permission.Read); t.is(member1.type, WorkspaceRole.Collaborator);
t.is(member1.status, WorkspaceMemberStatus.Pending); t.is(member1.status, WorkspaceMemberStatus.Pending);
const member2 = await t.context.workspace.grantMember( const member2 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Write, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
t.is(member2.workspaceId, workspace.id); t.is(member2.workspaceId, workspace.id);
t.is(member2.userId, otherUser.id); t.is(member2.userId, otherUser.id);
// TODO(fengmk2): fix this // TODO(fengmk2): fix this
// t.is(member2.type, Permission.Write); // t.is(member2.type, WorkspaceRole.Collaborator);
t.is(member2.status, WorkspaceMemberStatus.Accepted); t.is(member2.status, WorkspaceMemberStatus.Accepted);
}); });
@@ -559,23 +553,23 @@ test('should grant no thing on invalid status', async t => {
const member1 = await t.context.workspace.grantMember( const member1 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.NeedMoreSeat WorkspaceMemberStatus.NeedMoreSeat
); );
t.is(member1.workspaceId, workspace.id); t.is(member1.workspaceId, workspace.id);
t.is(member1.userId, otherUser.id); t.is(member1.userId, otherUser.id);
t.is(member1.type, Permission.Read); t.is(member1.type, WorkspaceRole.Collaborator);
t.is(member1.status, WorkspaceMemberStatus.NeedMoreSeat); t.is(member1.status, WorkspaceMemberStatus.NeedMoreSeat);
const member2 = await t.context.workspace.grantMember( const member2 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
t.is(member2.workspaceId, workspace.id); t.is(member2.workspaceId, workspace.id);
t.is(member2.userId, otherUser.id); t.is(member2.userId, otherUser.id);
t.is(member2.type, Permission.Read); t.is(member2.type, WorkspaceRole.Collaborator);
t.is(member2.status, WorkspaceMemberStatus.NeedMoreSeat); t.is(member2.status, WorkspaceMemberStatus.NeedMoreSeat);
}); });
@@ -590,7 +584,7 @@ test('should get the accepted status workspace member', async t => {
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
const member = await t.context.workspace.getMember( const member = await t.context.workspace.getMember(
@@ -599,7 +593,7 @@ test('should get the accepted status workspace member', async t => {
); );
t.is(member!.workspaceId, workspace.id); t.is(member!.workspaceId, workspace.id);
t.is(member!.userId, otherUser.id); t.is(member!.userId, otherUser.id);
t.is(member!.type, Permission.Read); t.is(member!.type, WorkspaceRole.Collaborator);
t.is(member!.status, WorkspaceMemberStatus.Accepted); t.is(member!.status, WorkspaceMemberStatus.Accepted);
}); });
@@ -614,7 +608,7 @@ test('should get any status workspace member, including pending and accepted', a
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Pending WorkspaceMemberStatus.Pending
); );
const member = await t.context.workspace.getMemberInAnyStatus( const member = await t.context.workspace.getMemberInAnyStatus(
@@ -623,7 +617,7 @@ test('should get any status workspace member, including pending and accepted', a
); );
t.is(member!.workspaceId, workspace.id); t.is(member!.workspaceId, workspace.id);
t.is(member!.userId, otherUser.id); t.is(member!.userId, otherUser.id);
t.is(member!.type, Permission.Read); t.is(member!.type, WorkspaceRole.Collaborator);
t.is(member!.status, WorkspaceMemberStatus.Pending); t.is(member!.status, WorkspaceMemberStatus.Pending);
}); });
@@ -635,7 +629,7 @@ test('should get workspace owner by workspace id', async t => {
const owner = await t.context.workspace.getOwner(workspace.id); const owner = await t.context.workspace.getOwner(workspace.id);
t.is(owner!.workspaceId, workspace.id); t.is(owner!.workspaceId, workspace.id);
t.is(owner!.userId, user.id); t.is(owner!.userId, user.id);
t.is(owner!.type, Permission.Owner); t.is(owner!.type, WorkspaceRole.Owner);
t.is(owner!.status, WorkspaceMemberStatus.Accepted); t.is(owner!.status, WorkspaceMemberStatus.Accepted);
t.truthy(owner!.user); t.truthy(owner!.user);
t.deepEqual(owner!.user, user); t.deepEqual(owner!.user, user);
@@ -658,27 +652,27 @@ test('should find workspace admin by workspace id', async t => {
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser1.id, otherUser1.id,
Permission.Admin, WorkspaceRole.Admin,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser2.id, otherUser2.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
// pending member should not be admin // pending member should not be admin
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser3.id, otherUser3.id,
Permission.Admin, WorkspaceRole.Admin,
WorkspaceMemberStatus.Pending WorkspaceMemberStatus.Pending
); );
const members = await t.context.workspace.findAdmins(workspace.id); const members = await t.context.workspace.findAdmins(workspace.id);
t.is(members.length, 1); t.is(members.length, 1);
t.is(members[0].workspaceId, workspace.id); t.is(members[0].workspaceId, workspace.id);
t.is(members[0].userId, otherUser1.id); t.is(members[0].userId, otherUser1.id);
t.is(members[0].type, Permission.Admin); t.is(members[0].type, WorkspaceRole.Admin);
t.is(members[0].status, WorkspaceMemberStatus.Accepted); t.is(members[0].status, WorkspaceMemberStatus.Accepted);
}); });
@@ -710,13 +704,13 @@ test('should the workspace member total count, including pending and accepted',
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser1.id, otherUser1.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Pending WorkspaceMemberStatus.Pending
); );
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser2.id, otherUser2.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
const count = await t.context.workspace.getMemberTotalCount(workspace.id); const count = await t.context.workspace.getMemberTotalCount(workspace.id);
@@ -737,13 +731,13 @@ test('should the workspace member used count, only count the accepted member', a
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser1.id, otherUser1.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Pending WorkspaceMemberStatus.Pending
); );
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser2.id, otherUser2.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
const count = await t.context.workspace.getMemberUsedCount(workspace.id); const count = await t.context.workspace.getMemberUsedCount(workspace.id);
@@ -855,7 +849,7 @@ test('should delete workspace member in Pending, Accepted status', async t => {
const member2 = await t.context.workspace.grantMember( const member2 = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
t.is(member2.status, WorkspaceMemberStatus.Accepted); t.is(member2.status, WorkspaceMemberStatus.Accepted);
@@ -874,7 +868,7 @@ test('should trigger workspace.members.requestDeclined event when delete workspa
const member = await t.context.workspace.grantMember( const member = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.UnderReview WorkspaceMemberStatus.UnderReview
); );
t.is(member.status, WorkspaceMemberStatus.UnderReview); t.is(member.status, WorkspaceMemberStatus.UnderReview);
@@ -919,7 +913,7 @@ test('should trigger workspace.members.requestDeclined event when delete workspa
const member = await t.context.workspace.grantMember( const member = await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.NeedMoreSeatAndReview WorkspaceMemberStatus.NeedMoreSeatAndReview
); );
t.is(member.status, WorkspaceMemberStatus.NeedMoreSeatAndReview); t.is(member.status, WorkspaceMemberStatus.NeedMoreSeatAndReview);
@@ -970,19 +964,19 @@ test('should refresh member seat status', async t => {
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser1.id, otherUser1.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.NeedMoreSeatAndReview WorkspaceMemberStatus.NeedMoreSeatAndReview
); );
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser2.id, otherUser2.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Pending WorkspaceMemberStatus.Pending
); );
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser3.id, otherUser3.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.NeedMoreSeat WorkspaceMemberStatus.NeedMoreSeat
); );
let count = await t.context.db.workspaceUserPermission.count({ let count = await t.context.db.workspaceUserPermission.count({
@@ -1043,30 +1037,30 @@ test('should find the workspace members order by type:desc and createdAt:asc', a
await t.context.workspace.grantMember( await t.context.workspace.grantMember(
workspace.id, workspace.id,
otherUser.id, otherUser.id,
Permission.Read, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
} }
let members = await t.context.workspace.findMembers(workspace.id); let members = await t.context.workspace.findMembers(workspace.id);
t.is(members.length, 8); t.is(members.length, 8);
t.is(members[0].type, Permission.Owner); t.is(members[0].type, WorkspaceRole.Owner);
t.is(members[0].status, WorkspaceMemberStatus.Accepted); t.is(members[0].status, WorkspaceMemberStatus.Accepted);
for (let i = 1; i < 8; i++) { for (let i = 1; i < 8; i++) {
t.is(members[i].type, Permission.Read); t.is(members[i].type, WorkspaceRole.Collaborator);
t.is(members[i].status, WorkspaceMemberStatus.Accepted); t.is(members[i].status, WorkspaceMemberStatus.Accepted);
} }
members = await t.context.workspace.findMembers(workspace.id, { take: 100 }); members = await t.context.workspace.findMembers(workspace.id, { take: 100 });
t.is(members.length, 11); t.is(members.length, 11);
t.is(members[0].type, Permission.Owner); t.is(members[0].type, WorkspaceRole.Owner);
t.is(members[0].status, WorkspaceMemberStatus.Accepted); t.is(members[0].status, WorkspaceMemberStatus.Accepted);
for (let i = 1; i < 11; i++) { for (let i = 1; i < 11; i++) {
t.is(members[i].type, Permission.Read); t.is(members[i].type, WorkspaceRole.Collaborator);
t.is(members[i].status, WorkspaceMemberStatus.Accepted); t.is(members[i].status, WorkspaceMemberStatus.Accepted);
} }
// skip should work // skip should work
members = await t.context.workspace.findMembers(workspace.id, { skip: 5 }); members = await t.context.workspace.findMembers(workspace.id, { skip: 5 });
t.is(members.length, 6); t.is(members.length, 6);
t.is(members[0].type, Permission.Read); t.is(members[0].type, WorkspaceRole.Collaborator);
}); });
test('should get the workspace member invitation', async t => { test('should get the workspace member invitation', async t => {

View File

@@ -13,7 +13,7 @@ import { AppModule } from '../app.module';
import { EventBus } from '../base'; import { EventBus } from '../base';
import { AuthService } from '../core/auth'; import { AuthService } from '../core/auth';
import { DocContentService } from '../core/doc-renderer'; import { DocContentService } from '../core/doc-renderer';
import { Permission, PermissionService } from '../core/permission'; import { PermissionService, WorkspaceRole } from '../core/permission';
import { QuotaManagementService, QuotaService, QuotaType } from '../core/quota'; import { QuotaManagementService, QuotaService, QuotaType } from '../core/quota';
import { WorkspaceType } from '../core/workspaces'; import { WorkspaceType } from '../core/workspaces';
import { import {
@@ -29,7 +29,6 @@ import {
inviteUser, inviteUser,
inviteUsers, inviteUsers,
leaveWorkspace, leaveWorkspace,
PermissionEnum,
revokeInviteLink, revokeInviteLink,
revokeMember, revokeMember,
revokeUser, revokeUser,
@@ -105,7 +104,7 @@ const init = async (
const invite = async ( const invite = async (
email: string, email: string,
permission: PermissionEnum = 'Write', permission: WorkspaceRole = WorkspaceRole.Collaborator,
shouldSendEmail: boolean = false shouldSendEmail: boolean = false
) => { ) => {
const member = await signUp(app, email.split('@')[0], email, '123456'); const member = await signUp(app, email.split('@')[0], email, '123456');
@@ -193,9 +192,12 @@ const init = async (
] as const; ] as const;
}; };
const admin = await invite(`${prefix}admin@affine.pro`, 'Admin'); const admin = await invite(`${prefix}admin@affine.pro`, WorkspaceRole.Admin);
const write = await invite(`${prefix}write@affine.pro`); const write = await invite(`${prefix}write@affine.pro`);
const read = await invite(`${prefix}read@affine.pro`, 'Read'); const read = await invite(
`${prefix}read@affine.pro`,
WorkspaceRole.Collaborator
);
return { return {
invite, invite,
@@ -268,7 +270,7 @@ test('should be able to check seat limit', async t => {
{ {
// invite // invite
await t.throwsAsync( await t.throwsAsync(
invite('member3@affine.pro', 'Read'), invite('member3@affine.pro', WorkspaceRole.Collaborator),
{ message: 'You have exceeded your workspace member quota.' }, { message: 'You have exceeded your workspace member quota.' },
'should throw error if exceed member limit' 'should throw error if exceed member limit'
); );
@@ -276,7 +278,7 @@ test('should be able to check seat limit', async t => {
memberLimit: 5, memberLimit: 5,
}); });
await t.notThrowsAsync( await t.notThrowsAsync(
invite('member4@affine.pro', 'Read'), invite('member4@affine.pro', WorkspaceRole.Collaborator),
'should not throw error if not exceed member limit' 'should not throw error if not exceed member limit'
); );
} }
@@ -324,17 +326,35 @@ test('should be able to grant team member permission', async t => {
const { owner, teamWorkspace: ws, admin, write, read } = await init(app); const { owner, teamWorkspace: ws, admin, write, read } = await init(app);
await t.throwsAsync( await t.throwsAsync(
grantMember(app, read.token.token, ws.id, write.id, 'Write'), grantMember(
app,
read.token.token,
ws.id,
write.id,
WorkspaceRole.Collaborator
),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not owner' 'should throw error if not owner'
); );
await t.throwsAsync( await t.throwsAsync(
grantMember(app, write.token.token, ws.id, read.id, 'Write'), grantMember(
app,
write.token.token,
ws.id,
read.id,
WorkspaceRole.Collaborator
),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not owner' 'should throw error if not owner'
); );
await t.throwsAsync( await t.throwsAsync(
grantMember(app, admin.token.token, ws.id, read.id, 'Write'), grantMember(
app,
admin.token.token,
ws.id,
read.id,
WorkspaceRole.Collaborator
),
{ instanceOf: Error }, { instanceOf: Error },
'should throw error if not owner' 'should throw error if not owner'
); );
@@ -342,15 +362,29 @@ test('should be able to grant team member permission', async t => {
{ {
// owner should be able to grant permission // owner should be able to grant permission
t.true( t.true(
await permissions.tryCheckWorkspaceIs(ws.id, read.id, Permission.Read), await permissions.tryCheckWorkspaceIs(
ws.id,
read.id,
WorkspaceRole.Collaborator
),
'should be able to check permission' 'should be able to check permission'
); );
t.truthy( t.truthy(
await grantMember(app, owner.token.token, ws.id, read.id, 'Admin'), await grantMember(
app,
owner.token.token,
ws.id,
read.id,
WorkspaceRole.Admin
),
'should be able to grant permission' 'should be able to grant permission'
); );
t.true( t.true(
await permissions.tryCheckWorkspaceIs(ws.id, read.id, Permission.Admin), await permissions.tryCheckWorkspaceIs(
ws.id,
read.id,
WorkspaceRole.Admin
),
'should be able to check permission' 'should be able to check permission'
); );
} }
@@ -692,17 +726,33 @@ test('should be able to emit events', async t => {
{ {
const { teamWorkspace: tws, owner, read } = await init(app); const { teamWorkspace: tws, owner, read } = await init(app);
await grantMember(app, owner.token.token, tws.id, read.id, 'Admin'); await grantMember(
app,
owner.token.token,
tws.id,
read.id,
WorkspaceRole.Admin
);
t.deepEqual( t.deepEqual(
event.emit.lastCall.args, event.emit.lastCall.args,
[ [
'workspace.members.roleChanged', 'workspace.members.roleChanged',
{ userId: read.id, workspaceId: tws.id, permission: Permission.Admin }, {
userId: read.id,
workspaceId: tws.id,
permission: WorkspaceRole.Admin,
},
], ],
'should emit role changed event' 'should emit role changed event'
); );
await grantMember(app, owner.token.token, tws.id, read.id, 'Owner'); await grantMember(
app,
owner.token.token,
tws.id,
read.id,
WorkspaceRole.Owner
);
const [ownershipTransferred] = event.emit const [ownershipTransferred] = event.emit
.getCalls() .getCalls()
.map(call => call.args) .map(call => call.args)

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client'; import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client';
import { Permission } from '../../core/permission'; import { WorkspaceRole } from '../../core/permission';
import { UserType } from '../../core/user/types'; import { UserType } from '../../core/user/types';
@Injectable() @Injectable()
@@ -14,7 +14,7 @@ export class WorkspaceResolverMock {
public: false, public: false,
permissions: { permissions: {
create: { create: {
type: Permission.Owner, type: WorkspaceRole.Owner,
userId: user.id, userId: user.id,
accepted: true, accepted: true,
status: WorkspaceMemberStatus.Accepted, status: WorkspaceMemberStatus.Accepted,

View File

@@ -1,4 +1,8 @@
import { INestApplication, ModuleMetadata } from '@nestjs/common'; import {
ConsoleLogger,
INestApplication,
ModuleMetadata,
} from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core'; import { APP_GUARD } from '@nestjs/core';
import { Query, Resolver } from '@nestjs/graphql'; import { Query, Resolver } from '@nestjs/graphql';
import { Test, TestingModuleBuilder } from '@nestjs/testing'; import { Test, TestingModuleBuilder } from '@nestjs/testing';
@@ -15,8 +19,6 @@ import { AuthGuard, AuthModule } from '../../core/auth';
import { UserFeaturesInit1698652531198 } from '../../data/migrations/1698652531198-user-features-init'; import { UserFeaturesInit1698652531198 } from '../../data/migrations/1698652531198-user-features-init';
import { ModelsModule } from '../../models'; import { ModelsModule } from '../../models';
export type PermissionEnum = 'Owner' | 'Admin' | 'Write' | 'Read';
async function flushDB(client: PrismaClient) { async function flushDB(client: PrismaClient) {
const result: { tablename: string }[] = const result: { tablename: string }[] =
await client.$queryRaw`SELECT tablename await client.$queryRaw`SELECT tablename
@@ -133,8 +135,11 @@ export async function createTestingApp(moduleDef: TestingModuleMeatdata = {}) {
cors: true, cors: true,
bodyParser: true, bodyParser: true,
rawBody: true, rawBody: true,
logger: ['fatal'],
}); });
const logger = new ConsoleLogger();
logger.setLogLevels(['fatal']);
app.useLogger(logger);
app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter())); app.useGlobalFilters(new GlobalExceptionFilter(app.getHttpAdapter()));
app.use( app.use(

View File

@@ -1,9 +1,9 @@
import type { INestApplication } from '@nestjs/common'; import type { INestApplication } from '@nestjs/common';
import request from 'supertest'; import request from 'supertest';
import { WorkspaceRole } from '../../core/permission/types';
import type { WorkspaceType } from '../../core/workspaces'; import type { WorkspaceType } from '../../core/workspaces';
import { gql } from './common'; import { gql } from './common';
import { PermissionEnum } from './utils';
export async function createWorkspace( export async function createWorkspace(
app: INestApplication, app: INestApplication,
@@ -157,7 +157,7 @@ export async function grantMember(
token: string, token: string,
workspaceId: string, workspaceId: string,
userId: string, userId: string,
permission: PermissionEnum permission: WorkspaceRole
) { ) {
const res = await request(app.getHttpServer()) const res = await request(app.getHttpServer())
.post(gql) .post(gql)
@@ -169,7 +169,7 @@ export async function grantMember(
grantMember( grantMember(
workspaceId: "${workspaceId}" workspaceId: "${workspaceId}"
userId: "${userId}" userId: "${userId}"
permission: ${permission} permission: ${WorkspaceRole[permission]}
) )
} }
`, `,

View File

@@ -5,11 +5,13 @@ import ava from 'ava';
import request from 'supertest'; import request from 'supertest';
import { AppModule } from '../app.module'; import { AppModule } from '../app.module';
import { WorkspaceRole } from '../core/permission/types';
import { import {
acceptInviteById, acceptInviteById,
createTestingApp, createTestingApp,
createWorkspace, createWorkspace,
getWorkspacePublicPages, getWorkspacePublicPages,
grantMember,
inviteUser, inviteUser,
publishPage, publishPage,
revokePublicPage, revokePublicPage,
@@ -116,7 +118,7 @@ test('should share a page', async t => {
const msg1 = await publishPage(app, u2.token.token, 'not_exists_ws', 'page2'); const msg1 = await publishPage(app, u2.token.token, 'not_exists_ws', 'page2');
t.is( t.is(
msg1, msg1,
'You do not have permission to access Space not_exists_ws.', 'You do not have permission to access doc page2 under Space not_exists_ws.',
'unauthorized user can share page' 'unauthorized user can share page'
); );
const msg2 = await revokePublicPage( const msg2 = await revokePublicPage(
@@ -127,7 +129,7 @@ test('should share a page', async t => {
); );
t.is( t.is(
msg2, msg2,
'You do not have permission to access Space not_exists_ws.', 'You do not have permission to access doc page2 under Space not_exists_ws.',
'unauthorized user can share page' 'unauthorized user can share page'
); );
@@ -136,6 +138,21 @@ test('should share a page', async t => {
workspace.id, workspace.id,
await inviteUser(app, u1.token.token, workspace.id, u2.email) await inviteUser(app, u1.token.token, workspace.id, u2.email)
); );
const msg3 = await publishPage(app, u2.token.token, workspace.id, 'page2');
t.is(
msg3,
`You do not have permission to access doc page2 under Space ${workspace.id}.`,
'WorkspaceRole and PageRole is lower than required'
);
await grantMember(
app,
u1.token.token,
workspace.id,
u2.id,
WorkspaceRole.Admin
);
const invited = await publishPage(app, u2.token.token, workspace.id, 'page2'); const invited = await publishPage(app, u2.token.token, workspace.id, 'page2');
t.is(invited.id, 'page2', 'failed to share page'); t.is(invited.id, 'page2', 'failed to share page');
@@ -154,21 +171,21 @@ test('should share a page', async t => {
t.is(pages2.length, 1, 'failed to get shared pages'); t.is(pages2.length, 1, 'failed to get shared pages');
t.is(pages2[0].id, 'page2', 'failed to get shared page: page2'); t.is(pages2[0].id, 'page2', 'failed to get shared page: page2');
const msg3 = await revokePublicPage( const msg4 = await revokePublicPage(
app, app,
u1.token.token, u1.token.token,
workspace.id, workspace.id,
'page3' 'page3'
); );
t.is(msg3, 'Page is not public'); t.is(msg4, 'Page is not public');
const msg4 = await revokePublicPage( const revoked = await revokePublicPage(
app, app,
u1.token.token, u1.token.token,
workspace.id, workspace.id,
'page2' 'page2'
); );
t.false(msg4.public, 'failed to revoke page'); t.false(revoked.public, 'failed to revoke page');
const page3 = await getWorkspacePublicPages( const page3 = await getWorkspacePublicPages(
app, app,
u1.token.token, u1.token.token,
@@ -177,7 +194,7 @@ test('should share a page', async t => {
t.is(page3.length, 0, 'failed to get shared pages'); t.is(page3.length, 0, 'failed to get shared pages');
}); });
test('should can get workspace doc', async t => { test('should be able to get workspace doc', async t => {
const { app } = t.context; const { app } = t.context;
const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1'); const u1 = await signUp(app, 'u1', 'u1@affine.pro', '1');
const u2 = await signUp(app, 'u2', 'u2@affine.pro', '2'); const u2 = await signUp(app, 'u2', 'u2@affine.pro', '2');

View File

@@ -363,6 +363,11 @@ export const USER_FRIENDLY_ERRORS = {
}, },
// Workspace & Userspace & Doc & Sync errors // Workspace & Userspace & Doc & Sync errors
workspace_permission_not_found: {
type: 'internal_server_error',
args: { spaceId: 'string' },
message: ({ spaceId }) => `Space ${spaceId} permission not found.`,
},
space_not_found: { space_not_found: {
type: 'resource_not_found', type: 'resource_not_found',
args: { spaceId: 'string' }, args: { spaceId: 'string' },
@@ -395,6 +400,11 @@ export const USER_FRIENDLY_ERRORS = {
args: { spaceId: 'string' }, args: { spaceId: 'string' },
message: ({ spaceId }) => `Owner of Space ${spaceId} not found.`, message: ({ spaceId }) => `Owner of Space ${spaceId} not found.`,
}, },
space_should_have_only_one_owner: {
type: 'invalid_input',
args: { spaceId: 'string' },
message: 'Space should have only one owner.',
},
doc_not_found: { doc_not_found: {
type: 'resource_not_found', type: 'resource_not_found',
args: { spaceId: 'string', docId: 'string' }, args: { spaceId: 'string', docId: 'string' },
@@ -438,6 +448,24 @@ export const USER_FRIENDLY_ERRORS = {
type: 'invalid_input', type: 'invalid_input',
message: 'Expected to revoke a public page, not a Space.', message: 'Expected to revoke a public page, not a Space.',
}, },
expect_to_grant_doc_user_roles: {
type: 'invalid_input',
args: { spaceId: 'string', docId: 'string' },
message: ({ spaceId, docId }) =>
`Expect grant roles on doc ${docId} under Space ${spaceId}, not a Space.`,
},
expect_to_revoke_doc_user_roles: {
type: 'invalid_input',
args: { spaceId: 'string', docId: 'string' },
message: ({ spaceId, docId }) =>
`Expect revoke roles on doc ${docId} under Space ${spaceId}, not a Space.`,
},
expect_to_update_doc_user_role: {
type: 'invalid_input',
args: { spaceId: 'string', docId: 'string' },
message: ({ spaceId, docId }) =>
`Expect update roles on doc ${docId} under Space ${spaceId}, not a Space.`,
},
page_is_not_public: { page_is_not_public: {
type: 'bad_request', type: 'bad_request',
message: 'Page is not public.', message: 'Page is not public.',

View File

@@ -191,6 +191,16 @@ export class EmailVerificationRequired extends UserFriendlyError {
} }
} }
@ObjectType() @ObjectType()
class WorkspacePermissionNotFoundDataType {
@Field() spaceId!: string
}
export class WorkspacePermissionNotFound extends UserFriendlyError {
constructor(args: WorkspacePermissionNotFoundDataType, message?: string | ((args: WorkspacePermissionNotFoundDataType) => string)) {
super('internal_server_error', 'workspace_permission_not_found', message, args);
}
}
@ObjectType()
class SpaceNotFoundDataType { class SpaceNotFoundDataType {
@Field() spaceId!: string @Field() spaceId!: string
} }
@@ -251,6 +261,16 @@ export class SpaceOwnerNotFound extends UserFriendlyError {
} }
} }
@ObjectType() @ObjectType()
class SpaceShouldHaveOnlyOneOwnerDataType {
@Field() spaceId!: string
}
export class SpaceShouldHaveOnlyOneOwner extends UserFriendlyError {
constructor(args: SpaceShouldHaveOnlyOneOwnerDataType, message?: string | ((args: SpaceShouldHaveOnlyOneOwnerDataType) => string)) {
super('invalid_input', 'space_should_have_only_one_owner', message, args);
}
}
@ObjectType()
class DocNotFoundDataType { class DocNotFoundDataType {
@Field() spaceId!: string @Field() spaceId!: string
@Field() docId!: string @Field() docId!: string
@@ -328,6 +348,39 @@ export class ExpectToRevokePublicPage extends UserFriendlyError {
super('invalid_input', 'expect_to_revoke_public_page', message); super('invalid_input', 'expect_to_revoke_public_page', message);
} }
} }
@ObjectType()
class ExpectToGrantDocUserRolesDataType {
@Field() spaceId!: string
@Field() docId!: string
}
export class ExpectToGrantDocUserRoles extends UserFriendlyError {
constructor(args: ExpectToGrantDocUserRolesDataType, message?: string | ((args: ExpectToGrantDocUserRolesDataType) => string)) {
super('invalid_input', 'expect_to_grant_doc_user_roles', message, args);
}
}
@ObjectType()
class ExpectToRevokeDocUserRolesDataType {
@Field() spaceId!: string
@Field() docId!: string
}
export class ExpectToRevokeDocUserRoles extends UserFriendlyError {
constructor(args: ExpectToRevokeDocUserRolesDataType, message?: string | ((args: ExpectToRevokeDocUserRolesDataType) => string)) {
super('invalid_input', 'expect_to_revoke_doc_user_roles', message, args);
}
}
@ObjectType()
class ExpectToUpdateDocUserRoleDataType {
@Field() spaceId!: string
@Field() docId!: string
}
export class ExpectToUpdateDocUserRole extends UserFriendlyError {
constructor(args: ExpectToUpdateDocUserRoleDataType, message?: string | ((args: ExpectToUpdateDocUserRoleDataType) => string)) {
super('invalid_input', 'expect_to_update_doc_user_role', message, args);
}
}
export class PageIsNotPublic extends UserFriendlyError { export class PageIsNotPublic extends UserFriendlyError {
constructor(message?: string) { constructor(message?: string) {
@@ -679,12 +732,14 @@ export enum ErrorNames {
ACTION_FORBIDDEN, ACTION_FORBIDDEN,
ACCESS_DENIED, ACCESS_DENIED,
EMAIL_VERIFICATION_REQUIRED, EMAIL_VERIFICATION_REQUIRED,
WORKSPACE_PERMISSION_NOT_FOUND,
SPACE_NOT_FOUND, SPACE_NOT_FOUND,
MEMBER_NOT_FOUND_IN_SPACE, MEMBER_NOT_FOUND_IN_SPACE,
NOT_IN_SPACE, NOT_IN_SPACE,
ALREADY_IN_SPACE, ALREADY_IN_SPACE,
SPACE_ACCESS_DENIED, SPACE_ACCESS_DENIED,
SPACE_OWNER_NOT_FOUND, SPACE_OWNER_NOT_FOUND,
SPACE_SHOULD_HAVE_ONLY_ONE_OWNER,
DOC_NOT_FOUND, DOC_NOT_FOUND,
DOC_ACCESS_DENIED, DOC_ACCESS_DENIED,
VERSION_REJECTED, VERSION_REJECTED,
@@ -693,6 +748,9 @@ export enum ErrorNames {
BLOB_NOT_FOUND, BLOB_NOT_FOUND,
EXPECT_TO_PUBLISH_PAGE, EXPECT_TO_PUBLISH_PAGE,
EXPECT_TO_REVOKE_PUBLIC_PAGE, EXPECT_TO_REVOKE_PUBLIC_PAGE,
EXPECT_TO_GRANT_DOC_USER_ROLES,
EXPECT_TO_REVOKE_DOC_USER_ROLES,
EXPECT_TO_UPDATE_DOC_USER_ROLE,
PAGE_IS_NOT_PUBLIC, PAGE_IS_NOT_PUBLIC,
FAILED_TO_SAVE_UPDATES, FAILED_TO_SAVE_UPDATES,
FAILED_TO_UPSERT_SNAPSHOT, FAILED_TO_UPSERT_SNAPSHOT,
@@ -746,5 +804,5 @@ registerEnumType(ErrorNames, {
export const ErrorDataUnionType = createUnionType({ export const ErrorDataUnionType = createUnionType({
name: 'ErrorDataUnion', name: 'ErrorDataUnion',
types: () => types: () =>
[QueryTooLongDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType] as const, [QueryTooLongDataType, WrongSignInCredentialsDataType, UnknownOauthProviderDataType, MissingOauthQueryParameterDataType, InvalidEmailDataType, InvalidPasswordLengthDataType, WorkspacePermissionNotFoundDataType, SpaceNotFoundDataType, MemberNotFoundInSpaceDataType, NotInSpaceDataType, AlreadyInSpaceDataType, SpaceAccessDeniedDataType, SpaceOwnerNotFoundDataType, SpaceShouldHaveOnlyOneOwnerDataType, DocNotFoundDataType, DocAccessDeniedDataType, VersionRejectedDataType, InvalidHistoryTimestampDataType, DocHistoryNotFoundDataType, BlobNotFoundDataType, ExpectToGrantDocUserRolesDataType, ExpectToRevokeDocUserRolesDataType, ExpectToUpdateDocUserRoleDataType, UnsupportedSubscriptionPlanDataType, SubscriptionAlreadyExistsDataType, SubscriptionNotExistsDataType, SameSubscriptionRecurringDataType, SubscriptionPlanNotFoundDataType, CopilotMessageNotFoundDataType, CopilotPromptNotFoundDataType, CopilotProviderSideErrorDataType, RuntimeConfigNotFoundDataType, InvalidRuntimeConfigTypeDataType, InvalidLicenseUpdateParamsDataType, WorkspaceMembersExceedLimitToDowngradeDataType] as const,
}); });

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { CannotDeleteAllAdminAccount } from '../../base'; import { CannotDeleteAllAdminAccount } from '../../base';
import { WorkspaceType } from '../workspaces/types'; import { WorkspaceFeatureType } from '../workspaces/types';
import { FeatureConfigType, getFeature } from './feature'; import { FeatureConfigType, getFeature } from './feature';
import { FeatureKind, FeatureType } from './types'; import { FeatureKind, FeatureType } from './types';
@@ -20,7 +20,7 @@ export class FeatureService {
if (data) { if (data) {
return getFeature(this.prisma, data.id) as Promise<FeatureConfigType<F>>; return getFeature(this.prisma, data.id) as Promise<FeatureConfigType<F>>;
} }
return undefined; return;
} }
// ======== User Features ======== // ======== User Features ========
@@ -315,7 +315,7 @@ export class FeatureService {
async listWorkspacesByFeature( async listWorkspacesByFeature(
feature: FeatureType feature: FeatureType
): Promise<WorkspaceType[]> { ): Promise<WorkspaceFeatureType[]> {
return this.prisma.workspaceFeature return this.prisma.workspaceFeature
.findMany({ .findMany({
where: { where: {
@@ -335,7 +335,7 @@ export class FeatureService {
}, },
}, },
}) })
.then(wss => wss.map(ws => ws.workspace as WorkspaceType)); .then(wss => wss.map(ws => ws.workspace));
} }
async hasWorkspaceFeature(workspaceId: string, feature: FeatureType) { async hasWorkspaceFeature(workspaceId: string, feature: FeatureType) {

View File

@@ -0,0 +1,665 @@
# 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,
}

View File

@@ -0,0 +1,36 @@
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);
}
}
});

View File

@@ -9,4 +9,4 @@ import { PermissionService } from './service';
export class PermissionModule {} export class PermissionModule {}
export { PermissionService } from './service'; export { PermissionService } from './service';
export { Permission, PublicPageMode } from './types'; export { DocRole, PublicPageMode, WorkspaceRole } from './types';

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import type { Prisma } from '@prisma/client'; import type { Prisma } from '@prisma/client';
import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client'; import { PrismaClient, WorkspaceMemberStatus } from '@prisma/client';
import { groupBy } from 'lodash-es'; import { groupBy } from 'lodash-es';
@@ -8,11 +8,22 @@ import {
EventBus, EventBus,
SpaceAccessDenied, SpaceAccessDenied,
SpaceOwnerNotFound, SpaceOwnerNotFound,
SpaceShouldHaveOnlyOneOwner,
WorkspacePermissionNotFound,
} from '../../base'; } from '../../base';
import { Permission, PublicPageMode } from './types'; import {
AllPossibleGraphQLDocActionsKeys,
DocRole,
findMinimalDocRole,
PublicPageMode,
requiredWorkspaceRoleByDocRole,
WorkspaceRole,
} from './types';
@Injectable() @Injectable()
export class PermissionService { export class PermissionService {
private readonly logger = new Logger(PermissionService.name);
constructor( constructor(
private readonly prisma: PrismaClient, private readonly prisma: PrismaClient,
private readonly event: EventBus private readonly event: EventBus
@@ -30,7 +41,7 @@ export class PermissionService {
} }
/// Start regin: workspace permission /// Start regin: workspace permission
async get(ws: string, user: string) { async get(ws: string, user: string): Promise<WorkspaceRole> {
const data = await this.prisma.workspaceUserPermission.findFirst({ const data = await this.prisma.workspaceUserPermission.findFirst({
where: { where: {
workspaceId: ws, workspaceId: ws,
@@ -39,7 +50,11 @@ export class PermissionService {
}, },
}); });
return data?.type as Permission; if (!data) {
throw new WorkspacePermissionNotFound({ spaceId: ws });
}
return data.type;
} }
/** /**
@@ -63,7 +78,7 @@ export class PermissionService {
.findMany({ .findMany({
where: { where: {
userId, userId,
type: Permission.Owner, type: WorkspaceRole.Owner,
OR: this.acceptedCondition, OR: this.acceptedCondition,
}, },
select: { select: {
@@ -77,7 +92,7 @@ export class PermissionService {
const owner = await this.prisma.workspaceUserPermission.findFirst({ const owner = await this.prisma.workspaceUserPermission.findFirst({
where: { where: {
workspaceId, workspaceId,
type: Permission.Owner, type: WorkspaceRole.Owner,
}, },
include: { include: {
user: true, user: true,
@@ -95,7 +110,7 @@ export class PermissionService {
const admin = await this.prisma.workspaceUserPermission.findMany({ const admin = await this.prisma.workspaceUserPermission.findMany({
where: { where: {
workspaceId, workspaceId,
type: Permission.Admin, type: WorkspaceRole.Admin,
}, },
include: { include: {
user: true, user: true,
@@ -117,7 +132,7 @@ export class PermissionService {
return this.prisma.workspaceUserPermission.findFirst({ return this.prisma.workspaceUserPermission.findFirst({
where: { where: {
workspaceId, workspaceId,
type: Permission.Owner, type: WorkspaceRole.Owner,
}, },
include: { include: {
user: true, user: true,
@@ -136,7 +151,7 @@ export class PermissionService {
if (ws === id) { if (ws === id) {
// if workspace is public or have any public page, then allow to access // if workspace is public or have any public page, then allow to access
const [isPublicWorkspace, publicPages] = await Promise.all([ const [isPublicWorkspace, publicPages] = await Promise.all([
this.tryCheckWorkspace(ws, user, Permission.Read), this.tryCheckWorkspace(ws, user, WorkspaceRole.Collaborator),
this.prisma.workspacePage.count({ this.prisma.workspacePage.count({
where: { where: {
workspaceId: ws, workspaceId: ws,
@@ -147,7 +162,7 @@ export class PermissionService {
return isPublicWorkspace || publicPages > 0; return isPublicWorkspace || publicPages > 0;
} }
return this.tryCheckPage(ws, id, user); return this.tryCheckPage(ws, id, 'Doc_Read', user);
} }
async getWorkspaceMemberStatus(ws: string, user: string) { async getWorkspaceMemberStatus(ws: string, user: string) {
@@ -168,7 +183,7 @@ export class PermissionService {
async isWorkspaceMember( async isWorkspaceMember(
ws: string, ws: string,
user: string, user: string,
permission: Permission = Permission.Read permission: WorkspaceRole = WorkspaceRole.Collaborator
): Promise<boolean> { ): Promise<boolean> {
const count = await this.prisma.workspaceUserPermission.count({ const count = await this.prisma.workspaceUserPermission.count({
where: { where: {
@@ -193,7 +208,7 @@ export class PermissionService {
async checkCloudWorkspace( async checkCloudWorkspace(
workspaceId: string, workspaceId: string,
userId?: string, userId?: string,
permission: Permission = Permission.Read permission: WorkspaceRole = WorkspaceRole.Collaborator
) { ) {
const hasWorkspace = await this.hasWorkspace(workspaceId); const hasWorkspace = await this.hasWorkspace(workspaceId);
if (hasWorkspace) { if (hasWorkspace) {
@@ -204,7 +219,7 @@ export class PermissionService {
async checkWorkspace( async checkWorkspace(
ws: string, ws: string,
user?: string, user?: string,
permission: Permission = Permission.Read permission: WorkspaceRole = WorkspaceRole.Collaborator
) { ) {
if (!(await this.tryCheckWorkspace(ws, user, permission))) { if (!(await this.tryCheckWorkspace(ws, user, permission))) {
throw new SpaceAccessDenied({ spaceId: ws }); throw new SpaceAccessDenied({ spaceId: ws });
@@ -214,10 +229,10 @@ export class PermissionService {
async tryCheckWorkspace( async tryCheckWorkspace(
ws: string, ws: string,
user?: string, user?: string,
permission: Permission = Permission.Read permission: WorkspaceRole = WorkspaceRole.Collaborator
) { ) {
// If the permission is read, we should check if the workspace is public // If the permission is read, we should check if the workspace is public
if (permission === Permission.Read) { if (permission === WorkspaceRole.Collaborator) {
const count = await this.prisma.workspace.count({ const count = await this.prisma.workspace.count({
where: { id: ws, public: true }, where: { id: ws, public: true },
}); });
@@ -242,7 +257,15 @@ export class PermissionService {
}, },
}); });
return count > 0; if (count > 0) {
return true;
} else {
this.logger.log("User's WorkspaceRole is lower than required", {
workspaceId: ws,
userId: user,
requiredRole: WorkspaceRole[permission],
});
}
} }
// unsigned in, workspace is not public // unsigned in, workspace is not public
@@ -253,7 +276,7 @@ export class PermissionService {
async checkWorkspaceIs( async checkWorkspaceIs(
ws: string, ws: string,
user: string, user: string,
permission: Permission = Permission.Read permission: WorkspaceRole = WorkspaceRole.Collaborator
) { ) {
if (!(await this.tryCheckWorkspaceIs(ws, user, permission))) { if (!(await this.tryCheckWorkspaceIs(ws, user, permission))) {
throw new SpaceAccessDenied({ spaceId: ws }); throw new SpaceAccessDenied({ spaceId: ws });
@@ -263,7 +286,7 @@ export class PermissionService {
async tryCheckWorkspaceIs( async tryCheckWorkspaceIs(
ws: string, ws: string,
user: string, user: string,
permission: Permission = Permission.Read permission: WorkspaceRole = WorkspaceRole.Collaborator
) { ) {
const count = await this.prisma.workspaceUserPermission.count({ const count = await this.prisma.workspaceUserPermission.count({
where: { where: {
@@ -307,7 +330,7 @@ export class PermissionService {
async grant( async grant(
ws: string, ws: string,
user: string, user: string,
permission: Permission = Permission.Read, permission: WorkspaceRole = WorkspaceRole.Collaborator,
status: WorkspaceMemberStatus = WorkspaceMemberStatus.Pending status: WorkspaceMemberStatus = WorkspaceMemberStatus.Pending
): Promise<string> { ): Promise<string> {
const data = await this.prisma.workspaceUserPermission.findFirst({ const data = await this.prisma.workspaceUserPermission.findFirst({
@@ -315,7 +338,7 @@ export class PermissionService {
}); });
if (data) { if (data) {
const toBeOwner = permission === Permission.Owner; const toBeOwner = permission === WorkspaceRole.Owner;
if (data.accepted && data.status === WorkspaceMemberStatus.Accepted) { if (data.accepted && data.status === WorkspaceMemberStatus.Accepted) {
const [p] = await this.prisma.$transaction( const [p] = await this.prisma.$transaction(
[ [
@@ -331,10 +354,10 @@ export class PermissionService {
? this.prisma.workspaceUserPermission.updateMany({ ? this.prisma.workspaceUserPermission.updateMany({
where: { where: {
workspaceId: ws, workspaceId: ws,
type: Permission.Owner, type: WorkspaceRole.Owner,
userId: { not: user }, userId: { not: user },
}, },
data: { type: Permission.Admin }, data: { type: WorkspaceRole.Admin },
}) })
: null, : null,
].filter(Boolean) as Prisma.PrismaPromise<any>[] ].filter(Boolean) as Prisma.PrismaPromise<any>[]
@@ -441,7 +464,7 @@ export class PermissionService {
// We shouldn't revoke owner permission // We shouldn't revoke owner permission
// should auto deleted by workspace/user delete cascading // should auto deleted by workspace/user delete cascading
if (!permission || permission.type === Permission.Owner) { if (!permission || permission.type === WorkspaceRole.Owner) {
return false; return false;
} }
@@ -490,22 +513,22 @@ export class PermissionService {
async checkCloudPagePermission( async checkCloudPagePermission(
workspaceId: string, workspaceId: string,
pageId: string, pageId: string,
userId?: string, action: AllPossibleGraphQLDocActionsKeys,
permission = Permission.Read userId?: string
) { ) {
const hasWorkspace = await this.hasWorkspace(workspaceId); const hasWorkspace = await this.hasWorkspace(workspaceId);
if (hasWorkspace) { if (hasWorkspace) {
await this.checkPagePermission(workspaceId, pageId, userId, permission); await this.checkPagePermission(workspaceId, pageId, action, userId);
} }
} }
async checkPagePermission( async checkPagePermission(
ws: string, ws: string,
page: string, page: string,
user?: string, action: AllPossibleGraphQLDocActionsKeys,
permission = Permission.Read user?: string
) { ) {
if (!(await this.tryCheckPage(ws, page, user, permission))) { if (!(await this.tryCheckPage(ws, page, action, user))) {
throw new DocAccessDenied({ spaceId: ws, docId: page }); throw new DocAccessDenied({ spaceId: ws, docId: page });
} }
} }
@@ -513,11 +536,12 @@ export class PermissionService {
async tryCheckPage( async tryCheckPage(
ws: string, ws: string,
page: string, page: string,
user?: string, action: AllPossibleGraphQLDocActionsKeys,
permission = Permission.Read user?: string
) { ) {
const role = findMinimalDocRole(action);
// check whether page is public // check whether page is public
if (permission === Permission.Read) { if (action === 'Doc_Read') {
const count = await this.prisma.workspacePage.count({ const count = await this.prisma.workspacePage.count({
where: { where: {
workspaceId: ws, workspaceId: ws,
@@ -541,7 +565,7 @@ export class PermissionService {
userId: user, userId: user,
accepted: true, accepted: true,
type: { type: {
gte: permission, gte: role,
}, },
}, },
}); });
@@ -550,11 +574,23 @@ export class PermissionService {
// accessible // accessible
if (count > 0) { if (count > 0) {
return true; return true;
} else {
this.logger.log("User's PageRole is lower than required", {
workspaceId: ws,
pageId: page,
userId: user,
requiredRole: DocRole[role],
action,
});
} }
} }
// check whether user has workspace related permission // check whether user has workspace related permission
return this.tryCheckWorkspace(ws, user, permission); return this.tryCheckWorkspace(
ws,
user,
requiredWorkspaceRoleByDocRole(role)
);
} }
async isPublicPage(ws: string, page: string) { async isPublicPage(ws: string, page: string) {
@@ -613,8 +649,8 @@ export class PermissionService {
ws: string, ws: string,
page: string, page: string,
user: string, user: string,
permission: Permission = Permission.Read permission: DocRole
) { ): Promise<string> {
const data = await this.prisma.workspacePageUserPermission.findFirst({ const data = await this.prisma.workspacePageUserPermission.findFirst({
where: { where: {
workspaceId: ws, workspaceId: ws,
@@ -637,18 +673,18 @@ export class PermissionService {
}), }),
// If the new permission is owner, we need to revoke old owner // If the new permission is owner, we need to revoke old owner
permission === Permission.Owner permission === DocRole.Owner
? this.prisma.workspacePageUserPermission.updateMany({ ? this.prisma.workspacePageUserPermission.updateMany({
where: { where: {
workspaceId: ws, workspaceId: ws,
pageId: page, pageId: page,
type: Permission.Owner, type: DocRole.Owner,
userId: { userId: {
not: user, not: user,
}, },
}, },
data: { data: {
type: Permission.Admin, type: DocRole.Manager,
}, },
}) })
: null, : null,
@@ -670,20 +706,93 @@ export class PermissionService {
.then(p => p.id); .then(p => p.id);
} }
async revokePage(ws: string, page: string, user: string) { async revokePage(ws: string, page: string, users: string[]) {
const result = await this.prisma.workspacePageUserPermission.deleteMany({ const result = await this.prisma.workspacePageUserPermission.deleteMany({
where: { where: {
workspaceId: ws, workspaceId: ws,
pageId: page, pageId: page,
userId: user, userId: {
in: users,
},
type: { type: {
// We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading // We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
not: Permission.Owner, not: DocRole.Owner,
}, },
}, },
}); });
return result.count > 0; return result.count > 0;
} }
/// End regin: page permission
async grantPagePermission(
workspaceId: string,
pageId: string,
userIds: string[],
role: DocRole
) {
if (userIds.length === 0) {
return [];
}
if (role === DocRole.Owner) {
if (userIds.length > 1) {
throw new SpaceShouldHaveOnlyOneOwner({ spaceId: workspaceId });
}
return [await this.grantPage(workspaceId, pageId, userIds[0], role)];
}
const ret = await this.prisma.$transaction(async tx =>
Promise.all(
userIds.map(id =>
tx.workspacePageUserPermission.upsert({
where: {
workspaceId_pageId_userId: {
workspaceId,
pageId,
userId: id,
},
},
create: {
workspaceId,
pageId,
userId: id,
type: role,
},
update: {
type: role,
},
})
)
)
);
return ret.map(p => p.id);
}
async updatePagePermission(
workspaceId: string,
pageId: string,
userId: string,
role: DocRole
) {
const permission = await this.prisma.workspacePageUserPermission.findFirst({
where: {
workspaceId,
pageId,
userId,
},
});
if (!permission) {
return this.grantPage(workspaceId, pageId, userId, role);
}
const { id } = await this.prisma.workspacePageUserPermission.update({
where: {
id: permission.id,
},
data: {
type: role,
},
});
return id;
}
} }

View File

@@ -1,11 +1,328 @@
export enum Permission { import assert from 'node:assert';
Read = 0,
Write = 1,
Admin = 10,
Owner = 99,
}
export enum PublicPageMode { export enum PublicPageMode {
Page, Page,
Edgeless, Edgeless,
} }
export enum DocRole {
External = 0,
Reader = 10,
Editor = 20,
Manager = 30,
Owner = 99,
}
export enum WorkspaceRole {
External = -99,
Collaborator = 1,
Admin = 10,
Owner = 99,
}
export const Actions = {
Workspace: {
Sync: 1,
CreateDoc: 2,
Delete: 11,
TransferOwner: 12,
Organize: {
Read: 0,
},
Users: {
Read: 3,
Manage: 6,
},
Properties: {
Read: 4,
Create: 8,
Update: 9,
Delete: 10,
},
Settings: {
Read: 5,
Update: 7,
},
},
Doc: {
Read: 13,
Copy: 14,
Duplicate: 17,
Trash: 18,
Restore: 19,
Delete: 20,
Update: 22,
Publish: 23,
TransferOwner: 25,
Properties: {
Read: 15,
Update: 21,
},
Users: {
Read: 16,
Manage: 24,
},
},
} 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;
}
? _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;
export type AllPossibleActionsKeys = ExcludeObjectKeys<
ActionsKeysUnion,
keyof typeof Actions,
'.'
>;
export type AllPossibleGraphQLWorkspaceActionsKeys = ExcludeObjectKeys<
ActionsKeysUnion,
'Workspace',
'_'
>;
export type AllPossibleGraphQLDocActionsKeys = ExcludeObjectKeys<
ActionsKeysUnion,
'Doc',
'_'
>;
type AllPossibleGraphQLActionsKeys =
| AllPossibleGraphQLWorkspaceActionsKeys
| AllPossibleGraphQLDocActionsKeys;
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'
);
function permissionKeyToGraphQLKey(key: string) {
const k = key.split('.');
return k.join('_') as keyof PermissionsList;
}
const DefaultActionsMap = Object.fromEntries(
ActionsKeys.map(key => [permissionKeyToGraphQLKey(key), false])
) as PermissionsList;
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;
});
return Object.fromEntries(
Object.entries(permissionList).filter(([k, _]) =>
k.startsWith('Workspace_')
)
);
}
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
): WorkspaceRole {
switch (docRole) {
case DocRole.Owner:
return WorkspaceRole.Owner;
case DocRole.Manager:
return WorkspaceRole.Admin;
case DocRole.Editor:
case DocRole.Reader:
case DocRole.External:
return WorkspaceRole.Collaborator;
}
}
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;

View File

@@ -27,7 +27,7 @@ import {
PgUserspaceDocStorageAdapter, PgUserspaceDocStorageAdapter,
PgWorkspaceDocStorageAdapter, PgWorkspaceDocStorageAdapter,
} from '../doc'; } from '../doc';
import { Permission, PermissionService } from '../permission'; import { PermissionService, WorkspaceRole } from '../permission';
import { DocID } from '../utils/doc'; import { DocID } from '../utils/doc';
const SubscribeMessage = (event: string) => const SubscribeMessage = (event: string) =>
@@ -615,7 +615,7 @@ abstract class SyncSocketAdapter {
async join(userId: string, spaceId: string, roomType: RoomType = 'sync') { async join(userId: string, spaceId: string, roomType: RoomType = 'sync') {
this.assertNotIn(spaceId, roomType); this.assertNotIn(spaceId, roomType);
await this.assertAccessible(spaceId, userId, Permission.Read); await this.assertAccessible(spaceId, userId, WorkspaceRole.Collaborator);
return this.client.join(this.room(spaceId, roomType)); return this.client.join(this.room(spaceId, roomType));
} }
@@ -643,7 +643,7 @@ abstract class SyncSocketAdapter {
abstract assertAccessible( abstract assertAccessible(
spaceId: string, spaceId: string,
userId: string, userId: string,
permission?: Permission permission?: WorkspaceRole
): Promise<void>; ): Promise<void>;
push(spaceId: string, docId: string, updates: Buffer[], editorId: string) { push(spaceId: string, docId: string, updates: Buffer[], editorId: string) {
@@ -694,7 +694,7 @@ class WorkspaceSyncAdapter extends SyncSocketAdapter {
async assertAccessible( async assertAccessible(
spaceId: string, spaceId: string,
userId: string, userId: string,
permission: Permission = Permission.Read permission: WorkspaceRole = WorkspaceRole.Collaborator
) { ) {
if ( if (
!(await this.permission.isWorkspaceMember(spaceId, userId, permission)) !(await this.permission.isWorkspaceMember(spaceId, userId, permission))
@@ -712,7 +712,7 @@ class UserspaceSyncAdapter extends SyncSocketAdapter {
async assertAccessible( async assertAccessible(
spaceId: string, spaceId: string,
userId: string, userId: string,
_permission: Permission = Permission.Read _permission: WorkspaceRole = WorkspaceRole.Collaborator
) { ) {
if (spaceId !== userId) { if (spaceId !== userId) {
throw new SpaceAccessDenied({ spaceId }); throw new SpaceAccessDenied({ spaceId });

View File

@@ -13,7 +13,7 @@ import {
} from '../../base'; } from '../../base';
import { CurrentUser, Public } from '../auth'; import { CurrentUser, Public } from '../auth';
import { PgWorkspaceDocStorageAdapter } from '../doc'; import { PgWorkspaceDocStorageAdapter } from '../doc';
import { Permission, PermissionService, PublicPageMode } from '../permission'; import { PermissionService, PublicPageMode } from '../permission';
import { WorkspaceBlobStorage } from '../storage'; import { WorkspaceBlobStorage } from '../storage';
import { DocID } from '../utils/doc'; import { DocID } from '../utils/doc';
@@ -147,8 +147,8 @@ export class WorkspacesController {
await this.permission.checkPagePermission( await this.permission.checkPagePermission(
docId.workspace, docId.workspace,
docId.guid, docId.guid,
user.id, 'Doc_Read',
Permission.Write user.id
); );
const history = await this.workspace.getDocHistory( const history = await this.workspace.getDocHistory(

View File

@@ -13,7 +13,7 @@ import { CurrentUser } from '../auth';
import { Admin } from '../common'; import { Admin } from '../common';
import { FeatureManagementService, FeatureType } from '../features'; import { FeatureManagementService, FeatureType } from '../features';
import { PermissionService } from '../permission'; import { PermissionService } from '../permission';
import { WorkspaceType } from './types'; import { WorkspaceFeatureType, WorkspaceType } from './types';
@Resolver(() => WorkspaceType) @Resolver(() => WorkspaceType)
export class WorkspaceManagementResolver { export class WorkspaceManagementResolver {
@@ -41,10 +41,10 @@ export class WorkspaceManagementResolver {
} }
@Admin() @Admin()
@Query(() => [WorkspaceType]) @Query(() => [WorkspaceFeatureType])
async listWorkspaceFeatures( async listWorkspaceFeatures(
@Args('feature', { type: () => FeatureType }) feature: FeatureType @Args('feature', { type: () => FeatureType }) feature: FeatureType
): Promise<WorkspaceType[]> { ): Promise<WorkspaceFeatureType[]> {
return this.feature.listFeatureWorkspaces(feature); return this.feature.listFeatureWorkspaces(feature);
} }

View File

@@ -15,7 +15,7 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import type { FileUpload } from '../../../base'; import type { FileUpload } from '../../../base';
import { BlobQuotaExceeded, CloudThrottlerGuard } from '../../../base'; import { BlobQuotaExceeded, CloudThrottlerGuard } from '../../../base';
import { CurrentUser } from '../../auth'; import { CurrentUser } from '../../auth';
import { Permission, PermissionService } from '../../permission'; import { PermissionService, WorkspaceRole } from '../../permission';
import { QuotaManagementService } from '../../quota'; import { QuotaManagementService } from '../../quota';
import { WorkspaceBlobStorage } from '../../storage'; import { WorkspaceBlobStorage } from '../../storage';
import { WorkspaceBlobSizes, WorkspaceType } from '../types'; import { WorkspaceBlobSizes, WorkspaceType } from '../types';
@@ -102,7 +102,7 @@ export class WorkspaceBlobResolver {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Write WorkspaceRole.Collaborator
); );
const checkExceeded = const checkExceeded =
@@ -174,7 +174,7 @@ export class WorkspaceBlobResolver {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Write WorkspaceRole.Collaborator
); );
await this.storage.release(workspaceId); await this.storage.release(workspaceId);

View File

@@ -13,7 +13,7 @@ import type { SnapshotHistory } from '@prisma/client';
import { CurrentUser } from '../../auth'; import { CurrentUser } from '../../auth';
import { PgWorkspaceDocStorageAdapter } from '../../doc'; import { PgWorkspaceDocStorageAdapter } from '../../doc';
import { Permission, PermissionService } from '../../permission'; import { PermissionService } from '../../permission';
import { DocID } from '../../utils/doc'; import { DocID } from '../../utils/doc';
import { WorkspaceType } from '../types'; import { WorkspaceType } from '../types';
import { EditorType } from './workspace'; import { EditorType } from './workspace';
@@ -79,8 +79,8 @@ export class DocHistoryResolver {
await this.permission.checkPagePermission( await this.permission.checkPagePermission(
docId.workspace, docId.workspace,
docId.guid, docId.guid,
user.id, 'Doc_Restore',
Permission.Write user.id
); );
await this.workspace.rollbackDoc( await this.workspace.rollbackDoc(

View File

@@ -1,6 +1,9 @@
import { Logger } from '@nestjs/common';
import { import {
Args, Args,
Field, Field,
InputType,
Int,
Mutation, Mutation,
ObjectType, ObjectType,
Parent, Parent,
@@ -12,18 +15,25 @@ import type { WorkspacePage as PrismaWorkspacePage } from '@prisma/client';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import { import {
ExpectToGrantDocUserRoles,
ExpectToPublishPage, ExpectToPublishPage,
ExpectToRevokeDocUserRoles,
ExpectToRevokePublicPage, ExpectToRevokePublicPage,
ExpectToUpdateDocUserRole,
PageIsNotPublic, PageIsNotPublic,
} from '../../../base'; } from '../../../base';
import { CurrentUser } from '../../auth'; import { CurrentUser } from '../../auth';
import { import {
Permission, DocRole,
PermissionService, PermissionService,
PublicPageMode, PublicPageMode,
WorkspaceRole,
} from '../../permission'; } from '../../permission';
import { mapRoleToActions, PermissionsList } from '../../permission/types';
import { UserType } from '../../user';
import { DocID } from '../../utils/doc'; import { DocID } from '../../utils/doc';
import { WorkspaceType } from '../types'; import { WorkspaceType } from '../types';
import { WorkspacePermissions } from './workspace';
registerEnumType(PublicPageMode, { registerEnumType(PublicPageMode, {
name: 'PublicPageMode', name: 'PublicPageMode',
@@ -45,8 +55,133 @@ class WorkspacePage implements Partial<PrismaWorkspacePage> {
public!: boolean; public!: boolean;
} }
@InputType()
class GrantDocUserRolesInput {
@Field(() => String)
docId!: string;
@Field(() => String)
workspaceId!: string;
@Field(() => DocRole)
role!: DocRole;
@Field(() => [String])
userIds!: string[];
}
@InputType()
class PageGrantedUsersInput {
@Field(() => Int)
first!: number;
@Field(() => Int)
offset?: number;
@Field(() => String, { description: 'Cursor', nullable: true })
after?: string;
@Field(() => String, { description: 'Cursor', nullable: true })
before?: string;
}
@ObjectType()
class GrantedDocUserType {
@Field(() => UserType)
user!: UserType;
@Field(() => DocRole)
role!: DocRole;
}
@ObjectType()
class PageInfo {
@Field(() => String, { nullable: true })
startCursor?: string;
@Field(() => String, { nullable: true })
endCursor?: string;
@Field(() => Boolean)
hasNextPage!: boolean;
@Field(() => Boolean)
hasPreviousPage!: boolean;
}
@ObjectType()
class GrantedDocUserEdge {
@Field(() => GrantedDocUserType)
user!: GrantedDocUserType;
@Field(() => String)
cursor!: string;
}
@ObjectType()
class GrantedDocUsersConnection {
@Field(() => Int)
totalCount!: number;
@Field(() => [GrantedDocUserEdge])
edges!: GrantedDocUserEdge[];
@Field(() => PageInfo)
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;
}
@ObjectType()
class DocType {
@Field(() => String)
id!: string;
@Field(() => Boolean)
public!: boolean;
@Field(() => DocRole)
role!: DocRole;
@Field(() => RolePermissions)
permissions!: RolePermissions;
}
@Resolver(() => WorkspaceType) @Resolver(() => WorkspaceType)
export class PagePermissionResolver { export class PagePermissionResolver {
private readonly logger = new Logger(PagePermissionResolver.name);
constructor( constructor(
private readonly prisma: PrismaClient, private readonly prisma: PrismaClient,
private readonly permission: PermissionService private readonly permission: PermissionService
@@ -102,6 +237,122 @@ export class PagePermissionResolver {
}); });
} }
@ResolveField(() => DocType, {
description: 'Check if current user has permission to access the page',
complexity: 2,
})
async pagePermission(
@Parent() workspace: WorkspaceType,
@Args('pageId') pageId: string,
@CurrentUser() user: CurrentUser
): Promise<DocType> {
const page = await this.prisma.workspacePage.findFirst({
where: {
workspaceId: workspace.id,
pageId,
},
select: {
public: true,
},
});
const [permission, workspacePermission] = await this.prisma.$transaction(
tx =>
Promise.all([
tx.workspacePageUserPermission.findFirst({
where: {
workspaceId: workspace.id,
pageId,
userId: user.id,
},
}),
tx.workspaceUserPermission.findFirst({
where: {
workspaceId: workspace.id,
userId: user.id,
},
}),
])
);
return {
id: pageId,
public: page?.public ?? false,
role: permission?.type ?? DocRole.External,
permissions: mapRoleToActions(
workspacePermission?.type,
permission?.type
),
};
}
@ResolveField(() => GrantedDocUsersConnection, {
description: 'Page granted users list',
complexity: 4,
})
async pageGrantedUsersList(
@Parent() workspace: WorkspaceType,
@Args('pageId') pageId: string,
@Args('pageGrantedUsersInput')
pageGrantedUsersInput: PageGrantedUsersInput
): Promise<GrantedDocUsersConnection> {
const docId = new DocID(pageId, workspace.id);
const [permissions, totalCount] = await this.prisma.$transaction(tx => {
return Promise.all([
tx.workspacePageUserPermission.findMany({
where: {
workspaceId: workspace.id,
pageId: docId.guid,
},
include: {
user: true,
},
orderBy: {
createdAt: 'desc',
},
take: pageGrantedUsersInput.first,
skip: pageGrantedUsersInput.offset,
cursor: pageGrantedUsersInput.after
? {
id: pageGrantedUsersInput.after,
}
: undefined,
}),
tx.workspacePageUserPermission.count({
where: {
workspaceId: workspace.id,
pageId: docId.guid,
},
}),
]);
});
return {
totalCount,
edges: permissions.map(permission => ({
user: {
user: {
id: permission.user.id,
name: permission.user.name,
email: permission.user.email,
avatarUrl: permission.user.avatarUrl,
emailVerified: permission.user.emailVerifiedAt !== null,
hasPassword: permission.user.password !== null,
},
role: permission.type,
},
cursor: permission.id,
})),
pageInfo: {
startCursor: permissions.at(0)?.id,
endCursor: permissions.at(-1)?.id,
hasNextPage: totalCount > pageGrantedUsersInput.first,
hasPreviousPage:
pageGrantedUsersInput.offset !== undefined &&
pageGrantedUsersInput.offset > 0,
},
};
}
/** /**
* @deprecated * @deprecated
*/ */
@@ -134,15 +385,26 @@ export class PagePermissionResolver {
const docId = new DocID(pageId, workspaceId); const docId = new DocID(pageId, workspaceId);
if (docId.isWorkspace) { if (docId.isWorkspace) {
this.logger.error('Expect to publish page, but it is a workspace', {
workspaceId,
pageId,
});
throw new ExpectToPublishPage(); throw new ExpectToPublishPage();
} }
await this.permission.checkWorkspace( await this.permission.checkPagePermission(
docId.workspace, docId.workspace,
user.id, docId.guid,
Permission.Write 'Doc_Publish',
user.id
); );
this.logger.log('Publish page', {
workspaceId,
pageId,
mode,
});
return this.permission.publishPage(docId.workspace, docId.guid, mode); return this.permission.publishPage(docId.workspace, docId.guid, mode);
} }
@@ -171,13 +433,18 @@ export class PagePermissionResolver {
const docId = new DocID(pageId, workspaceId); const docId = new DocID(pageId, workspaceId);
if (docId.isWorkspace) { if (docId.isWorkspace) {
this.logger.error('Expect to revoke public page, but it is a workspace', {
workspaceId,
pageId,
});
throw new ExpectToRevokePublicPage('Expect page not to be workspace'); throw new ExpectToRevokePublicPage('Expect page not to be workspace');
} }
await this.permission.checkWorkspace( await this.permission.checkPagePermission(
docId.workspace, docId.workspace,
user.id, docId.guid,
Permission.Write 'Doc_Publish',
user.id
); );
const isPublic = await this.permission.isPublicPage( const isPublic = await this.permission.isPublicPage(
@@ -186,9 +453,148 @@ export class PagePermissionResolver {
); );
if (!isPublic) { if (!isPublic) {
this.logger.log('Expect to revoke public page, but it is not public', {
workspaceId,
pageId,
});
throw new PageIsNotPublic('Page is not public'); throw new PageIsNotPublic('Page is not public');
} }
this.logger.log('Revoke public page', {
workspaceId,
pageId,
});
return this.permission.revokePublicPage(docId.workspace, docId.guid); return this.permission.revokePublicPage(docId.workspace, docId.guid);
} }
@Mutation(() => Boolean)
async grantDocUserRoles(
@CurrentUser() user: CurrentUser,
@Args('input') input: GrantDocUserRolesInput
): Promise<boolean> {
const doc = new DocID(input.docId, input.workspaceId);
const pairs = {
spaceId: input.workspaceId,
docId: input.docId,
};
if (doc.isWorkspace) {
this.logger.error(
'Expect to grant doc user roles, but it is a workspace',
pairs
);
throw new ExpectToGrantDocUserRoles(
pairs,
'Expect doc not to be workspace'
);
}
await this.permission.checkPagePermission(
doc.workspace,
doc.guid,
'Doc_Users_Manage',
user.id
);
await this.permission.grantPagePermission(
doc.workspace,
doc.guid,
input.userIds,
input.role
);
this.logger.log('Grant doc user roles', {
...pairs,
userIds: input.userIds,
role: input.role,
});
return true;
}
@Mutation(() => Boolean)
async revokeDocUserRoles(
@CurrentUser() user: CurrentUser,
@Args('docId') docId: string,
@Args('userIds', { type: () => [String] }) userIds: string[]
): Promise<boolean> {
const doc = new DocID(docId);
const pairs = {
spaceId: doc.workspace,
docId: doc.guid,
};
if (doc.isWorkspace) {
this.logger.error(
'Expect to revoke doc user roles, but it is a workspace',
pairs
);
throw new ExpectToRevokeDocUserRoles(
pairs,
'Expect doc not to be workspace'
);
}
await this.permission.checkWorkspace(
doc.workspace,
user.id,
WorkspaceRole.Collaborator
);
await this.permission.revokePage(doc.workspace, doc.guid, userIds);
this.logger.log('Revoke doc user roles', {
...pairs,
userIds: userIds,
});
return true;
}
@Mutation(() => Boolean)
async updateDocUserRole(
@CurrentUser() user: CurrentUser,
@Args('docId') docId: string,
@Args('userId') userId: string,
@Args('role', { type: () => DocRole }) role: DocRole
): Promise<boolean> {
const doc = new DocID(docId);
const pairs = {
spaceId: doc.workspace,
docId: doc.guid,
};
if (doc.isWorkspace) {
this.logger.error(
'Expect to update doc user role, but it is a workspace',
pairs
);
throw new ExpectToUpdateDocUserRole(
pairs,
'Expect doc not to be workspace'
);
}
await this.permission.checkWorkspace(
doc.workspace,
user.id,
WorkspaceRole.Collaborator
);
if (role === DocRole.Owner) {
const ret = await this.permission.grantPagePermission(
doc.workspace,
doc.guid,
[userId],
role
);
this.logger.log('Transfer doc owner', {
...pairs,
userId: userId,
role: role,
});
return ret.length > 0;
} else {
await this.permission.updatePagePermission(
doc.workspace,
doc.guid,
userId,
role
);
this.logger.log('Update doc user role', {
...pairs,
userId: userId,
role: role,
});
return true;
}
}
} }

View File

@@ -11,7 +11,7 @@ import {
} from '../../../base'; } from '../../../base';
import { Models } from '../../../models'; import { Models } from '../../../models';
import { DocContentService } from '../../doc-renderer'; import { DocContentService } from '../../doc-renderer';
import { Permission, PermissionService } from '../../permission'; import { PermissionService, WorkspaceRole } from '../../permission';
import { WorkspaceBlobStorage } from '../../storage'; import { WorkspaceBlobStorage } from '../../storage';
export const defaultWorkspaceAvatar = export const defaultWorkspaceAvatar =
@@ -221,14 +221,14 @@ export class WorkspaceService {
async sendRoleChangedEmail( async sendRoleChangedEmail(
userId: string, userId: string,
ws: { id: string; role: Permission } ws: { id: string; role: WorkspaceRole }
) { ) {
const user = await this.models.user.getPublicUser(userId); const user = await this.models.user.getPublicUser(userId);
if (!user) throw new UserNotFound(); if (!user) throw new UserNotFound();
const workspace = await this.getWorkspaceInfo(ws.id); const workspace = await this.getWorkspaceInfo(ws.id);
if (ws.role === Permission.Admin) { if (ws.role === WorkspaceRole.Admin) {
await this.mailer.sendTeamBecomeAdminMail(user.email, { await this.mailer.sendTeamBecomeAdminMail(user.email, {
workspace, workspace,
url: this.url.link(`/workspace/${workspace.id}`), url: this.url.link(`/workspace/${workspace.id}`),

View File

@@ -21,7 +21,7 @@ import {
} from '../../../base'; } from '../../../base';
import { Models } from '../../../models'; import { Models } from '../../../models';
import { CurrentUser } from '../../auth'; import { CurrentUser } from '../../auth';
import { Permission, PermissionService } from '../../permission'; import { PermissionService, WorkspaceRole } from '../../permission';
import { QuotaManagementService } from '../../quota'; import { QuotaManagementService } from '../../quota';
import { import {
InviteLink, InviteLink,
@@ -71,7 +71,7 @@ export class TeamWorkspaceResolver {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Admin WorkspaceRole.Admin
); );
if (emails.length > 512) { if (emails.length > 512) {
@@ -113,7 +113,7 @@ export class TeamWorkspaceResolver {
ret.inviteId = await this.permissions.grant( ret.inviteId = await this.permissions.grant(
workspaceId, workspaceId,
target.id, target.id,
Permission.Write, WorkspaceRole.Collaborator,
needMoreSeat needMoreSeat
? WorkspaceMemberStatus.NeedMoreSeat ? WorkspaceMemberStatus.NeedMoreSeat
: WorkspaceMemberStatus.Pending : WorkspaceMemberStatus.Pending
@@ -159,7 +159,7 @@ export class TeamWorkspaceResolver {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspace.id, workspace.id,
user.id, user.id,
Permission.Admin WorkspaceRole.Admin
); );
const cacheId = `workspace:inviteLink:${workspace.id}`; const cacheId = `workspace:inviteLink:${workspace.id}`;
@@ -186,7 +186,7 @@ export class TeamWorkspaceResolver {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Admin WorkspaceRole.Admin
); );
const cacheWorkspaceId = `workspace:inviteLink:${workspaceId}`; const cacheWorkspaceId = `workspace:inviteLink:${workspaceId}`;
const invite = await this.cache.get<{ inviteId: string }>(cacheWorkspaceId); const invite = await this.cache.get<{ inviteId: string }>(cacheWorkspaceId);
@@ -222,7 +222,7 @@ export class TeamWorkspaceResolver {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Admin WorkspaceRole.Admin
); );
const cacheId = `workspace:inviteLink:${workspaceId}`; const cacheId = `workspace:inviteLink:${workspaceId}`;
return await this.cache.delete(cacheId); return await this.cache.delete(cacheId);
@@ -237,7 +237,7 @@ export class TeamWorkspaceResolver {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Admin WorkspaceRole.Admin
); );
try { try {
@@ -257,7 +257,7 @@ export class TeamWorkspaceResolver {
const result = await this.permissions.grant( const result = await this.permissions.grant(
workspaceId, workspaceId,
userId, userId,
Permission.Write, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.Accepted WorkspaceMemberStatus.Accepted
); );
@@ -283,12 +283,12 @@ export class TeamWorkspaceResolver {
@CurrentUser() user: CurrentUser, @CurrentUser() user: CurrentUser,
@Args('workspaceId') workspaceId: string, @Args('workspaceId') workspaceId: string,
@Args('userId') userId: string, @Args('userId') userId: string,
@Args('permission', { type: () => Permission }) permission: Permission @Args('permission', { type: () => WorkspaceRole }) permission: WorkspaceRole
) { ) {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
); );
try { try {
@@ -311,7 +311,7 @@ export class TeamWorkspaceResolver {
); );
if (result) { if (result) {
if (permission === Permission.Owner) { if (permission === WorkspaceRole.Owner) {
this.event.emit('workspace.members.ownershipTransferred', { this.event.emit('workspace.members.ownershipTransferred', {
workspaceId, workspaceId,
from: user.id, from: user.id,

View File

@@ -1,4 +1,3 @@
import { Logger } from '@nestjs/common';
import { import {
Args, Args,
Field, Field,
@@ -15,6 +14,7 @@ import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import type { FileUpload } from '../../../base'; import type { FileUpload } from '../../../base';
import { import {
AFFiNELogger,
AlreadyInSpace, AlreadyInSpace,
Cache, Cache,
DocNotFound, DocNotFound,
@@ -33,7 +33,11 @@ import {
import { Models } from '../../../models'; import { Models } from '../../../models';
import { CurrentUser, Public } from '../../auth'; import { CurrentUser, Public } from '../../auth';
import { type Editor, PgWorkspaceDocStorageAdapter } from '../../doc'; import { type Editor, PgWorkspaceDocStorageAdapter } from '../../doc';
import { Permission, PermissionService } from '../../permission'; import { PermissionService, WorkspaceRole } from '../../permission';
import {
mapWorkspaceRoleToWorkspaceActions,
WorkspacePermissionsList,
} from '../../permission/types';
import { QuotaManagementService, QuotaQueryType } from '../../quota'; import { QuotaManagementService, QuotaQueryType } from '../../quota';
import { UserType } from '../../user'; import { UserType } from '../../user';
import { import {
@@ -68,6 +72,45 @@ class WorkspacePageMeta {
updatedBy!: EditorType | null; 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;
}
@ObjectType()
export class WorkspaceRolePermissions {
@Field(() => WorkspaceRole)
role!: WorkspaceRole;
@Field(() => WorkspacePermissions)
permissions!: WorkspacePermissions;
}
/** /**
* Workspace resolver * Workspace resolver
* Public apis rate limit: 10 req/m * Public apis rate limit: 10 req/m
@@ -75,8 +118,6 @@ class WorkspacePageMeta {
*/ */
@Resolver(() => WorkspaceType) @Resolver(() => WorkspaceType)
export class WorkspaceResolver { export class WorkspaceResolver {
private readonly logger = new Logger(WorkspaceResolver.name);
constructor( constructor(
private readonly cache: Cache, private readonly cache: Cache,
private readonly prisma: PrismaClient, private readonly prisma: PrismaClient,
@@ -86,29 +127,32 @@ export class WorkspaceResolver {
private readonly event: EventBus, private readonly event: EventBus,
private readonly mutex: RequestMutex, private readonly mutex: RequestMutex,
private readonly workspaceService: WorkspaceService, private readonly workspaceService: WorkspaceService,
private readonly workspaceStorage: PgWorkspaceDocStorageAdapter private readonly workspaceStorage: PgWorkspaceDocStorageAdapter,
) {} private readonly logger: AFFiNELogger
) {
logger.setContext(WorkspaceResolver.name);
}
@ResolveField(() => Permission, { @ResolveField(() => WorkspaceRole, {
description: 'Permission of current signed in user in workspace', description: 'Role of current signed in user in workspace',
complexity: 2, complexity: 2,
}) })
async permission( async role(
@CurrentUser() user: CurrentUser, @CurrentUser() user: CurrentUser,
@Parent() workspace: WorkspaceType @Parent() workspace: WorkspaceType
) { ) {
// may applied in workspaces query // may applied in workspaces query
if ('permission' in workspace) { if ('role' in workspace) {
return workspace.permission; return workspace.role;
} }
const permission = await this.permissions.get(workspace.id, user.id); const role = await this.permissions.get(workspace.id, user.id);
if (!permission) { if (!role) {
throw new SpaceAccessDenied({ spaceId: workspace.id }); throw new SpaceAccessDenied({ spaceId: workspace.id });
} }
return permission; return role;
} }
@ResolveField(() => Int, { @ResolveField(() => Int, {
@@ -249,7 +293,7 @@ export class WorkspaceResolver {
return this.permissions.tryCheckWorkspaceIs( return this.permissions.tryCheckWorkspaceIs(
workspaceId, workspaceId,
user.id, user.id,
Permission.Admin WorkspaceRole.Admin
); );
} }
@@ -279,6 +323,7 @@ export class WorkspaceResolver {
return { return {
...workspace, ...workspace,
permission: type, permission: type,
role: type,
}; };
}); });
} }
@@ -297,6 +342,25 @@ export class WorkspaceResolver {
return workspace; return workspace;
} }
@Query(() => WorkspaceRolePermissions, {
description: 'Get workspace role permissions',
})
async workspaceRolePermissions(
@CurrentUser() user: CurrentUser,
@Args('id') id: string
) {
const workspace = await this.prisma.workspaceUserPermission.findFirst({
where: { workspaceId: id, userId: user.id },
});
if (!workspace) {
throw new SpaceAccessDenied({ spaceId: id });
}
return {
role: workspace.type,
permissions: mapWorkspaceRoleToWorkspaceActions(workspace.type),
};
}
@Mutation(() => WorkspaceType, { @Mutation(() => WorkspaceType, {
description: 'Create a new workspace', description: 'Create a new workspace',
}) })
@@ -312,7 +376,7 @@ export class WorkspaceResolver {
public: false, public: false,
permissions: { permissions: {
create: { create: {
type: Permission.Owner, type: WorkspaceRole.Owner,
userId: user.id, userId: user.id,
accepted: true, accepted: true,
status: WorkspaceMemberStatus.Accepted, status: WorkspaceMemberStatus.Accepted,
@@ -323,21 +387,18 @@ export class WorkspaceResolver {
if (init) { if (init) {
// convert stream to buffer // convert stream to buffer
const buffer = await new Promise<Buffer>(resolve => { const chunks: Uint8Array[] = [];
const stream = init.createReadStream(); try {
const chunks: Uint8Array[] = []; for await (const chunk of init.createReadStream()) {
stream.on('data', chunk => {
chunks.push(chunk); chunks.push(chunk);
}); }
stream.on('error', () => { } catch (e) {
resolve(Buffer.from([])); this.logger.error('Failed to get file content from upload stream', e);
}); chunks.length = 0;
stream.on('end', () => { }
resolve(Buffer.concat(chunks)); const buffer = chunks.length ? Buffer.concat(chunks) : null;
});
});
if (buffer.length) { if (buffer) {
await this.prisma.snapshot.create({ await this.prisma.snapshot.create({
data: { data: {
id: workspace.id, id: workspace.id,
@@ -364,7 +425,7 @@ export class WorkspaceResolver {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
id, id,
user.id, user.id,
isTeam ? Permission.Owner : Permission.Admin isTeam ? WorkspaceRole.Owner : WorkspaceRole.Admin
); );
return this.prisma.workspace.update({ return this.prisma.workspace.update({
@@ -380,7 +441,7 @@ export class WorkspaceResolver {
@CurrentUser() user: CurrentUser, @CurrentUser() user: CurrentUser,
@Args('id') id: string @Args('id') id: string
) { ) {
await this.permissions.checkWorkspace(id, user.id, Permission.Owner); await this.permissions.checkWorkspace(id, user.id, WorkspaceRole.Owner);
await this.prisma.workspace.delete({ await this.prisma.workspace.delete({
where: { where: {
@@ -401,16 +462,16 @@ export class WorkspaceResolver {
@Args('email') email: string, @Args('email') email: string,
@Args('sendInviteMail', { nullable: true }) sendInviteMail: boolean, @Args('sendInviteMail', { nullable: true }) sendInviteMail: boolean,
@Args('permission', { @Args('permission', {
type: () => Permission, type: () => WorkspaceRole,
nullable: true, nullable: true,
deprecationReason: 'never used', deprecationReason: 'never used',
}) })
_permission?: Permission _permission?: WorkspaceRole
) { ) {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Admin WorkspaceRole.Admin
); );
try { try {
@@ -418,7 +479,7 @@ export class WorkspaceResolver {
const lockFlag = `invite:${workspaceId}`; const lockFlag = `invite:${workspaceId}`;
await using lock = await this.mutex.acquire(lockFlag); await using lock = await this.mutex.acquire(lockFlag);
if (!lock) { if (!lock) {
return new TooManyRequest(); throw new TooManyRequest();
} }
// member limit check // member limit check
@@ -445,7 +506,7 @@ export class WorkspaceResolver {
const inviteId = await this.permissions.grant( const inviteId = await this.permissions.grant(
workspaceId, workspaceId,
target.id, target.id,
Permission.Write WorkspaceRole.Collaborator
); );
if (sendInviteMail) { if (sendInviteMail) {
try { try {
@@ -474,10 +535,10 @@ export class WorkspaceResolver {
} catch (e) { } catch (e) {
// pass through user friendly error // pass through user friendly error
if (e instanceof UserFriendlyError) { if (e instanceof UserFriendlyError) {
return e; throw e;
} }
this.logger.error('failed to invite user', e); this.logger.error('failed to invite user', e);
return new TooManyRequest(); throw new TooManyRequest();
} }
} }
@@ -512,20 +573,20 @@ export class WorkspaceResolver {
const isAdmin = await this.permissions.tryCheckWorkspaceIs( const isAdmin = await this.permissions.tryCheckWorkspaceIs(
workspaceId, workspaceId,
userId, userId,
Permission.Admin WorkspaceRole.Admin
); );
if (isTeam && isAdmin) { if (isTeam && isAdmin) {
// only owner can revoke team workspace admin // only owner can revoke team workspace admin
await this.permissions.checkWorkspaceIs( await this.permissions.checkWorkspaceIs(
workspaceId, workspaceId,
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
); );
} else { } else {
await this.permissions.checkWorkspace( await this.permissions.checkWorkspace(
workspaceId, workspaceId,
user.id, user.id,
Permission.Admin WorkspaceRole.Admin
); );
} }
@@ -543,7 +604,7 @@ export class WorkspaceResolver {
const lockFlag = `invite:${workspaceId}`; const lockFlag = `invite:${workspaceId}`;
await using lock = await this.mutex.acquire(lockFlag); await using lock = await this.mutex.acquire(lockFlag);
if (!lock) { if (!lock) {
return new TooManyRequest(); throw new TooManyRequest();
} }
const isTeam = await this.quota.isTeamWorkspace(workspaceId); const isTeam = await this.quota.isTeamWorkspace(workspaceId);
@@ -553,7 +614,7 @@ export class WorkspaceResolver {
user.id user.id
); );
if (status === WorkspaceMemberStatus.Accepted) { if (status === WorkspaceMemberStatus.Accepted) {
return new AlreadyInSpace({ spaceId: workspaceId }); throw new AlreadyInSpace({ spaceId: workspaceId });
} }
// invite link // invite link
@@ -568,7 +629,7 @@ export class WorkspaceResolver {
await this.permissions.grant( await this.permissions.grant(
workspaceId, workspaceId,
user.id, user.id,
Permission.Write, WorkspaceRole.Collaborator,
WorkspaceMemberStatus.NeedMoreSeatAndReview WorkspaceMemberStatus.NeedMoreSeatAndReview
); );
const memberCount = const memberCount =
@@ -579,7 +640,7 @@ export class WorkspaceResolver {
}); });
return true; return true;
} else if (!status) { } else if (!status) {
return new MemberQuotaExceeded(); throw new MemberQuotaExceeded();
} }
} else { } else {
const inviteId = await this.permissions.grant(workspaceId, user.id); const inviteId = await this.permissions.grant(workspaceId, user.id);

View File

@@ -8,17 +8,28 @@ import {
PickType, PickType,
registerEnumType, registerEnumType,
} from '@nestjs/graphql'; } from '@nestjs/graphql';
import { Workspace, WorkspaceMemberStatus } from '@prisma/client'; import { WorkspaceMemberStatus } from '@prisma/client';
import { SafeIntResolver } from 'graphql-scalars'; import { SafeIntResolver } from 'graphql-scalars';
import { Permission } from '../permission'; import { DocRole, WorkspaceRole } from '../permission';
import { UserType } from '../user/types'; import { UserType } from '../user/types';
registerEnumType(Permission, { registerEnumType(WorkspaceRole, {
name: 'WorkspaceRole',
description: 'User role in workspace',
});
// @deprecated
registerEnumType(WorkspaceRole, {
name: 'Permission', name: 'Permission',
description: 'User permission in workspace', description: 'User permission in workspace',
}); });
registerEnumType(DocRole, {
name: 'DocRole',
description: 'User permission in doc',
});
registerEnumType(WorkspaceMemberStatus, { registerEnumType(WorkspaceMemberStatus, {
name: 'WorkspaceMemberStatus', name: 'WorkspaceMemberStatus',
description: 'Member invite status in workspace', description: 'Member invite status in workspace',
@@ -33,8 +44,14 @@ export class InviteUserType extends OmitType(
@Field(() => ID) @Field(() => ID)
id!: string; id!: string;
@Field(() => Permission, { description: 'User permission in workspace' }) @Field(() => WorkspaceRole, {
permission!: Permission; deprecationReason: 'Use role instead',
description: 'User permission in workspace',
})
permission!: WorkspaceRole;
@Field(() => WorkspaceRole, { description: 'User role in workspace' })
role!: WorkspaceRole;
@Field({ description: 'Invite id' }) @Field({ description: 'Invite id' })
inviteId!: string; inviteId!: string;
@@ -52,22 +69,25 @@ export class InviteUserType extends OmitType(
} }
@ObjectType() @ObjectType()
export class WorkspaceType implements Partial<Workspace> { export class WorkspaceFeatureType {
@Field(() => ID) @Field(() => ID)
id!: string; id!: string;
@Field({ description: 'is Public workspace' }) @Field({ description: 'is Public workspace' })
public!: boolean; public!: boolean;
@Field({ description: 'Workspace created date' })
createdAt!: Date;
}
@ObjectType()
export class WorkspaceType extends WorkspaceFeatureType {
@Field({ description: 'Enable AI' }) @Field({ description: 'Enable AI' })
enableAi!: boolean; enableAi!: boolean;
@Field({ description: 'Enable url previous when sharing' }) @Field({ description: 'Enable url previous when sharing' })
enableUrlPreview!: boolean; enableUrlPreview!: boolean;
@Field({ description: 'Workspace created date' })
createdAt!: Date;
@Field(() => [InviteUserType], { @Field(() => [InviteUserType], {
description: 'Members of workspace', description: 'Members of workspace',
}) })

View File

@@ -1,3 +1,2 @@
export * from './feature'; export * from './feature';
export * from './page'; export * from './page';
export * from './permission';

View File

@@ -1,6 +0,0 @@
export enum Permission {
Read = 0,
Write = 1,
Admin = 10,
Owner = 99,
}

View File

@@ -5,9 +5,9 @@ import {
type WorkspacePageUserPermission as PageUserPermission, type WorkspacePageUserPermission as PageUserPermission,
} from '@prisma/client'; } from '@prisma/client';
import { WorkspaceRole } from '../core/permission';
import { BaseModel } from './base'; import { BaseModel } from './base';
import { Permission, PublicPageMode } from './common'; import { PublicPageMode } from './common';
export type { Page }; export type { Page };
export type UpdatePageInput = { export type UpdatePageInput = {
mode?: PublicPageMode; mode?: PublicPageMode;
@@ -93,7 +93,7 @@ export class PageModel extends BaseModel {
workspaceId: string, workspaceId: string,
pageId: string, pageId: string,
userId: string, userId: string,
permission: Permission = Permission.Read permission: WorkspaceRole = WorkspaceRole.Collaborator
): Promise<PageUserPermission> { ): Promise<PageUserPermission> {
let data = await this.db.workspacePageUserPermission.findUnique({ let data = await this.db.workspacePageUserPermission.findUnique({
where: { where: {
@@ -134,15 +134,15 @@ export class PageModel extends BaseModel {
} }
// If the new permission is owner, we need to revoke old owner // If the new permission is owner, we need to revoke old owner
if (permission === Permission.Owner) { if (permission === WorkspaceRole.Owner) {
await this.db.workspacePageUserPermission.updateMany({ await this.db.workspacePageUserPermission.updateMany({
where: { where: {
workspaceId, workspaceId,
pageId, pageId,
type: Permission.Owner, type: WorkspaceRole.Owner,
userId: { not: userId }, userId: { not: userId },
}, },
data: { type: Permission.Admin }, data: { type: WorkspaceRole.Admin },
}); });
this.logger.log( this.logger.log(
`Change owner of workspace ${workspaceId} page ${pageId} to user ${userId}` `Change owner of workspace ${workspaceId} page ${pageId} to user ${userId}`
@@ -163,7 +163,7 @@ export class PageModel extends BaseModel {
workspaceId: string, workspaceId: string,
pageId: string, pageId: string,
userId: string, userId: string,
permission: Permission = Permission.Read permission: WorkspaceRole = WorkspaceRole.Collaborator
) { ) {
const count = await this.db.workspacePageUserPermission.count({ const count = await this.db.workspacePageUserPermission.count({
where: { where: {
@@ -190,7 +190,7 @@ export class PageModel extends BaseModel {
userId, userId,
type: { type: {
// We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading // We shouldn't revoke owner permission, should auto deleted by workspace/user delete cascading
not: Permission.Owner, not: WorkspaceRole.Owner,
}, },
}, },
}); });

View File

@@ -8,8 +8,8 @@ import {
import { groupBy } from 'lodash-es'; import { groupBy } from 'lodash-es';
import { EventBus } from '../base'; import { EventBus } from '../base';
import { WorkspaceRole } from '../core/permission';
import { BaseModel } from './base'; import { BaseModel } from './base';
import { Permission } from './common';
declare global { declare global {
interface Events { interface Events {
@@ -90,7 +90,7 @@ export class WorkspaceModel extends BaseModel {
public: false, public: false,
permissions: { permissions: {
create: { create: {
type: Permission.Owner, type: WorkspaceRole.Owner,
userId: userId, userId: userId,
accepted: true, accepted: true,
status: WorkspaceMemberStatus.Accepted, status: WorkspaceMemberStatus.Accepted,
@@ -141,7 +141,7 @@ export class WorkspaceModel extends BaseModel {
const rows = await this.db.workspaceUserPermission.findMany({ const rows = await this.db.workspaceUserPermission.findMany({
where: { where: {
userId, userId,
type: Permission.Owner, type: WorkspaceRole.Owner,
OR: this.acceptedCondition, OR: this.acceptedCondition,
}, },
select: { select: {
@@ -177,7 +177,7 @@ export class WorkspaceModel extends BaseModel {
async grantMember( async grantMember(
workspaceId: string, workspaceId: string,
userId: string, userId: string,
permission: Permission = Permission.Read, permission: WorkspaceRole = WorkspaceRole.Collaborator,
status: WorkspaceMemberStatus = WorkspaceMemberStatus.Pending status: WorkspaceMemberStatus = WorkspaceMemberStatus.Pending
): Promise<WorkspaceUserPermission> { ): Promise<WorkspaceUserPermission> {
const data = await this.db.workspaceUserPermission.findUnique({ const data = await this.db.workspaceUserPermission.findUnique({
@@ -191,17 +191,19 @@ export class WorkspaceModel extends BaseModel {
if (!data) { if (!data) {
// Create a new permission // Create a new permission
// TODO(fengmk2): should we check the permission here? Like owner can't be pending?
const created = await this.db.workspaceUserPermission.create({ const created = await this.db.workspaceUserPermission.create({
data: { data: {
workspaceId, workspaceId,
userId, userId,
type: permission, type: permission,
status, status:
permission === WorkspaceRole.Owner
? WorkspaceMemberStatus.Accepted
: status,
}, },
}); });
this.logger.log( this.logger.log(
`Granted workspace ${workspaceId} member ${userId} with permission ${permission}` `Granted workspace ${workspaceId} member ${userId} with permission ${WorkspaceRole[permission]}`
); );
await this.notifyMembersUpdated(workspaceId); await this.notifyMembersUpdated(workspaceId);
return created; return created;
@@ -216,14 +218,14 @@ export class WorkspaceModel extends BaseModel {
data: { type: permission }, data: { type: permission },
}); });
// If the new permission is owner, we need to revoke old owner // If the new permission is owner, we need to revoke old owner
if (permission === Permission.Owner) { if (permission === WorkspaceRole.Owner) {
await this.db.workspaceUserPermission.updateMany({ await this.db.workspaceUserPermission.updateMany({
where: { where: {
workspaceId, workspaceId,
type: Permission.Owner, type: WorkspaceRole.Owner,
userId: { not: userId }, userId: { not: userId },
}, },
data: { type: Permission.Admin }, data: { type: WorkspaceRole.Admin },
}); });
this.logger.log( this.logger.log(
`Change owner of workspace ${workspaceId} to ${userId}` `Change owner of workspace ${workspaceId} to ${userId}`
@@ -318,7 +320,7 @@ export class WorkspaceModel extends BaseModel {
async isMember( async isMember(
workspaceId: string, workspaceId: string,
userId: string, userId: string,
permission: Permission = Permission.Read permission: WorkspaceRole = WorkspaceRole.Collaborator
) { ) {
const count = await this.db.workspaceUserPermission.count({ const count = await this.db.workspaceUserPermission.count({
where: { where: {
@@ -340,7 +342,7 @@ export class WorkspaceModel extends BaseModel {
return await this.db.workspaceUserPermission.findFirst({ return await this.db.workspaceUserPermission.findFirst({
where: { where: {
workspaceId, workspaceId,
type: Permission.Owner, type: WorkspaceRole.Owner,
OR: this.acceptedCondition, OR: this.acceptedCondition,
}, },
include: { include: {
@@ -356,7 +358,7 @@ export class WorkspaceModel extends BaseModel {
return await this.db.workspaceUserPermission.findMany({ return await this.db.workspaceUserPermission.findMany({
where: { where: {
workspaceId, workspaceId,
type: Permission.Admin, type: WorkspaceRole.Admin,
OR: this.acceptedCondition, OR: this.acceptedCondition,
}, },
include: { include: {
@@ -394,7 +396,7 @@ export class WorkspaceModel extends BaseModel {
// We shouldn't revoke owner permission // We shouldn't revoke owner permission
// should auto deleted by workspace/user delete cascading // should auto deleted by workspace/user delete cascading
if (!member || member.type === Permission.Owner) { if (!member || member.type === WorkspaceRole.Owner) {
return false; return false;
} }

View File

@@ -335,6 +335,7 @@ export class CopilotResolver {
await this.permissions.checkCloudPagePermission( await this.permissions.checkCloudPagePermission(
workspaceId, workspaceId,
docId, docId,
'Doc_Read',
user.id user.id
); );
} else { } else {
@@ -368,6 +369,7 @@ export class CopilotResolver {
await this.permissions.checkCloudPagePermission( await this.permissions.checkCloudPagePermission(
options.workspaceId, options.workspaceId,
options.docId, options.docId,
'Doc_Read',
user.id user.id
); );
const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`; const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`;
@@ -401,6 +403,7 @@ export class CopilotResolver {
await this.permissions.checkCloudPagePermission( await this.permissions.checkCloudPagePermission(
workspaceId, workspaceId,
docId, docId,
'Doc_Update',
user.id user.id
); );
const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${workspaceId}`; const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${workspaceId}`;
@@ -428,6 +431,7 @@ export class CopilotResolver {
await this.permissions.checkCloudPagePermission( await this.permissions.checkCloudPagePermission(
options.workspaceId, options.workspaceId,
options.docId, options.docId,
'Doc_Copy',
user.id user.id
); );
const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`; const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`;
@@ -456,6 +460,7 @@ export class CopilotResolver {
await this.permissions.checkCloudPagePermission( await this.permissions.checkCloudPagePermission(
options.workspaceId, options.workspaceId,
options.docId, options.docId,
'Doc_Delete',
user.id user.id
); );
if (!options.sessionIds.length) { if (!options.sessionIds.length) {

View File

@@ -11,7 +11,7 @@ import {
import { ActionForbidden, Config } from '../../base'; import { ActionForbidden, Config } from '../../base';
import { CurrentUser } from '../../core/auth'; import { CurrentUser } from '../../core/auth';
import { Permission, PermissionService } from '../../core/permission'; import { PermissionService, WorkspaceRole } from '../../core/permission';
import { WorkspaceType } from '../../core/workspaces'; import { WorkspaceType } from '../../core/workspaces';
import { SubscriptionRecurring } from '../payment/types'; import { SubscriptionRecurring } from '../payment/types';
import { LicenseService } from './service'; import { LicenseService } from './service';
@@ -61,7 +61,7 @@ export class LicenseResolver {
await this.permission.checkWorkspaceIs( await this.permission.checkWorkspaceIs(
workspace.id, workspace.id,
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
); );
return this.service.getLicense(workspace.id); return this.service.getLicense(workspace.id);
@@ -80,7 +80,7 @@ export class LicenseResolver {
await this.permission.checkWorkspaceIs( await this.permission.checkWorkspaceIs(
workspaceId, workspaceId,
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
); );
return this.service.activateTeamLicense(workspaceId, license); return this.service.activateTeamLicense(workspaceId, license);
@@ -98,7 +98,7 @@ export class LicenseResolver {
await this.permission.checkWorkspaceIs( await this.permission.checkWorkspaceIs(
workspaceId, workspaceId,
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
); );
return this.service.deactivateTeamLicense(workspaceId); return this.service.deactivateTeamLicense(workspaceId);
@@ -116,7 +116,7 @@ export class LicenseResolver {
await this.permission.checkWorkspaceIs( await this.permission.checkWorkspaceIs(
workspaceId, workspaceId,
user.id, user.id,
Permission.Owner WorkspaceRole.Owner
); );
const { url } = await this.service.createCustomerPortal(workspaceId); const { url } = await this.service.createCustomerPortal(workspaceId);

View File

@@ -27,7 +27,7 @@ import {
WorkspaceIdRequiredToUpdateTeamSubscription, WorkspaceIdRequiredToUpdateTeamSubscription,
} from '../../base'; } from '../../base';
import { CurrentUser, Public } from '../../core/auth'; import { CurrentUser, Public } from '../../core/auth';
import { Permission, PermissionService } from '../../core/permission'; import { PermissionService, WorkspaceRole } from '../../core/permission';
import { UserType } from '../../core/user'; import { UserType } from '../../core/user';
import { WorkspaceType } from '../../core/workspaces'; import { WorkspaceType } from '../../core/workspaces';
import { Invoice, Subscription, WorkspaceSubscriptionManager } from './manager'; import { Invoice, Subscription, WorkspaceSubscriptionManager } from './manager';
@@ -541,7 +541,11 @@ export class WorkspaceSubscriptionResolver {
@CurrentUser() me: CurrentUser, @CurrentUser() me: CurrentUser,
@Parent() workspace: WorkspaceType @Parent() workspace: WorkspaceType
) { ) {
await this.permission.checkWorkspace(workspace.id, me.id, Permission.Owner); await this.permission.checkWorkspace(
workspace.id,
me.id,
WorkspaceRole.Owner
);
return this.db.invoice.count({ return this.db.invoice.count({
where: { where: {
targetId: workspace.id, targetId: workspace.id,
@@ -557,7 +561,11 @@ export class WorkspaceSubscriptionResolver {
take: number, take: number,
@Args('skip', { type: () => Int, nullable: true }) skip?: number @Args('skip', { type: () => Int, nullable: true }) skip?: number
) { ) {
await this.permission.checkWorkspace(workspace.id, me.id, Permission.Owner); await this.permission.checkWorkspace(
workspace.id,
me.id,
WorkspaceRole.Owner
);
return this.db.invoice.findMany({ return this.db.invoice.findMany({
where: { where: {

View File

@@ -204,12 +204,28 @@ type DocNotFoundDataType {
spaceId: String! spaceId: String!
} }
"""User permission in doc"""
enum DocRole {
Editor
External
Manager
Owner
Reader
}
type DocType {
id: String!
permissions: RolePermissions!
public: Boolean!
role: DocRole!
}
type EditorType { type EditorType {
avatarUrl: String avatarUrl: String
name: String! name: String!
} }
union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WrongSignInCredentialsDataType union ErrorDataUnion = AlreadyInSpaceDataType | BlobNotFoundDataType | CopilotMessageNotFoundDataType | CopilotPromptNotFoundDataType | CopilotProviderSideErrorDataType | DocAccessDeniedDataType | DocHistoryNotFoundDataType | DocNotFoundDataType | ExpectToGrantDocUserRolesDataType | ExpectToRevokeDocUserRolesDataType | ExpectToUpdateDocUserRoleDataType | InvalidEmailDataType | InvalidHistoryTimestampDataType | InvalidLicenseUpdateParamsDataType | InvalidPasswordLengthDataType | InvalidRuntimeConfigTypeDataType | MemberNotFoundInSpaceDataType | MissingOauthQueryParameterDataType | NotInSpaceDataType | QueryTooLongDataType | RuntimeConfigNotFoundDataType | SameSubscriptionRecurringDataType | SpaceAccessDeniedDataType | SpaceNotFoundDataType | SpaceOwnerNotFoundDataType | SpaceShouldHaveOnlyOneOwnerDataType | SubscriptionAlreadyExistsDataType | SubscriptionNotExistsDataType | SubscriptionPlanNotFoundDataType | UnknownOauthProviderDataType | UnsupportedSubscriptionPlanDataType | VersionRejectedDataType | WorkspaceMembersExceedLimitToDowngradeDataType | WorkspacePermissionNotFoundDataType | WrongSignInCredentialsDataType
enum ErrorNames { enum ErrorNames {
ACCESS_DENIED ACCESS_DENIED
@@ -240,8 +256,11 @@ enum ErrorNames {
EMAIL_ALREADY_USED EMAIL_ALREADY_USED
EMAIL_TOKEN_NOT_FOUND EMAIL_TOKEN_NOT_FOUND
EMAIL_VERIFICATION_REQUIRED EMAIL_VERIFICATION_REQUIRED
EXPECT_TO_GRANT_DOC_USER_ROLES
EXPECT_TO_PUBLISH_PAGE EXPECT_TO_PUBLISH_PAGE
EXPECT_TO_REVOKE_DOC_USER_ROLES
EXPECT_TO_REVOKE_PUBLIC_PAGE EXPECT_TO_REVOKE_PUBLIC_PAGE
EXPECT_TO_UPDATE_DOC_USER_ROLE
FAILED_TO_CHECKOUT FAILED_TO_CHECKOUT
FAILED_TO_SAVE_UPDATES FAILED_TO_SAVE_UPDATES
FAILED_TO_UPSERT_SNAPSHOT FAILED_TO_UPSERT_SNAPSHOT
@@ -279,6 +298,7 @@ enum ErrorNames {
SPACE_ACCESS_DENIED SPACE_ACCESS_DENIED
SPACE_NOT_FOUND SPACE_NOT_FOUND
SPACE_OWNER_NOT_FOUND SPACE_OWNER_NOT_FOUND
SPACE_SHOULD_HAVE_ONLY_ONE_OWNER
SUBSCRIPTION_ALREADY_EXISTS SUBSCRIPTION_ALREADY_EXISTS
SUBSCRIPTION_EXPIRED SUBSCRIPTION_EXPIRED
SUBSCRIPTION_HAS_BEEN_CANCELED SUBSCRIPTION_HAS_BEEN_CANCELED
@@ -296,10 +316,26 @@ enum ErrorNames {
WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION WORKSPACE_ID_REQUIRED_TO_UPDATE_TEAM_SUBSCRIPTION
WORKSPACE_LICENSE_ALREADY_EXISTS WORKSPACE_LICENSE_ALREADY_EXISTS
WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE WORKSPACE_MEMBERS_EXCEED_LIMIT_TO_DOWNGRADE
WORKSPACE_PERMISSION_NOT_FOUND
WRONG_SIGN_IN_CREDENTIALS WRONG_SIGN_IN_CREDENTIALS
WRONG_SIGN_IN_METHOD WRONG_SIGN_IN_METHOD
} }
type ExpectToGrantDocUserRolesDataType {
docId: String!
spaceId: String!
}
type ExpectToRevokeDocUserRolesDataType {
docId: String!
spaceId: String!
}
type ExpectToUpdateDocUserRoleDataType {
docId: String!
spaceId: String!
}
"""The type of workspace feature""" """The type of workspace feature"""
enum FeatureType { enum FeatureType {
AIEarlyAccess AIEarlyAccess
@@ -321,6 +357,29 @@ input ForkChatSessionInput {
workspaceId: String! workspaceId: String!
} }
input GrantDocUserRolesInput {
docId: String!
role: DocRole!
userIds: [String!]!
workspaceId: String!
}
type GrantedDocUserEdge {
cursor: String!
user: GrantedDocUserType!
}
type GrantedDocUserType {
role: DocRole!
user: UserType!
}
type GrantedDocUsersConnection {
edges: [GrantedDocUserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type HumanReadableQuotaType { type HumanReadableQuotaType {
blobLimit: String! blobLimit: String!
copilotActionLimit: String copilotActionLimit: String
@@ -418,7 +477,10 @@ type InviteUserType {
name: String name: String
"""User permission in workspace""" """User permission in workspace"""
permission: Permission! permission: Permission! @deprecated(reason: "Use role instead")
"""User role in workspace"""
role: Permission!
"""Member invite status in workspace""" """Member invite status in workspace"""
status: WorkspaceMemberStatus! status: WorkspaceMemberStatus!
@@ -548,6 +610,7 @@ type Mutation {
"""Create a chat session""" """Create a chat session"""
forkCopilotSession(options: ForkChatSessionInput!): String! forkCopilotSession(options: ForkChatSessionInput!): String!
generateLicenseKey(sessionId: String!): String! generateLicenseKey(sessionId: String!): String!
grantDocUserRoles(input: GrantDocUserRolesInput!): Boolean!
grantMember(permission: Permission!, userId: String!, workspaceId: String!): String! grantMember(permission: Permission!, userId: String!, workspaceId: String!): String!
invite(email: String!, permission: Permission @deprecated(reason: "never used"), sendInviteMail: Boolean, workspaceId: String!): String! invite(email: String!, permission: Permission @deprecated(reason: "never used"), sendInviteMail: Boolean, workspaceId: String!): String!
inviteBatch(emails: [String!]!, sendInviteMail: Boolean, workspaceId: String!): [InviteResult!]! inviteBatch(emails: [String!]!, sendInviteMail: Boolean, workspaceId: String!): [InviteResult!]!
@@ -561,6 +624,7 @@ type Mutation {
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int! removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Int!
resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType! resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType!
revoke(userId: String!, workspaceId: String!): Boolean! revoke(userId: String!, workspaceId: String!): Boolean!
revokeDocUserRoles(docId: String!, userIds: [String!]!): Boolean!
revokeInviteLink(workspaceId: String!): Boolean! revokeInviteLink(workspaceId: String!): Boolean!
revokePage(pageId: String!, workspaceId: String!): Boolean! @deprecated(reason: "use revokePublicPage") revokePage(pageId: String!, workspaceId: String!): Boolean! @deprecated(reason: "use revokePublicPage")
revokePublicPage(pageId: String!, workspaceId: String!): WorkspacePage! revokePublicPage(pageId: String!, workspaceId: String!): WorkspacePage!
@@ -578,6 +642,7 @@ type Mutation {
"""Update a chat session""" """Update a chat session"""
updateCopilotSession(options: UpdateChatSessionInput!): String! updateCopilotSession(options: UpdateChatSessionInput!): String!
updateDocUserRole(docId: String!, role: DocRole!, userId: String!): Boolean!
updateProfile(input: UpdateUserInput!): UserType! updateProfile(input: UpdateUserInput!): UserType!
"""update server runtime configurable setting""" """update server runtime configurable setting"""
@@ -611,6 +676,23 @@ enum OAuthProviderType {
OIDC OIDC
} }
input PageGrantedUsersInput {
"""Cursor"""
after: String
"""Cursor"""
before: String
first: Int!
offset: Int!
}
type PageInfo {
endCursor: String
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
}
type PasswordLimitsType { type PasswordLimitsType {
maxLength: Int! maxLength: Int!
minLength: Int! minLength: Int!
@@ -619,9 +701,9 @@ type PasswordLimitsType {
"""User permission in workspace""" """User permission in workspace"""
enum Permission { enum Permission {
Admin Admin
Collaborator
External
Owner Owner
Read
Write
} }
"""The mode which the public page default in""" """The mode which the public page default in"""
@@ -651,7 +733,7 @@ type Query {
"""List all copilot prompts""" """List all copilot prompts"""
listCopilotPrompts: [CopilotPromptType!]! listCopilotPrompts: [CopilotPromptType!]!
listWorkspaceFeatures(feature: FeatureType!): [WorkspaceType!]! listWorkspaceFeatures(feature: FeatureType!): [WorkspaceFeatureType!]!
prices: [SubscriptionPrice!]! prices: [SubscriptionPrice!]!
"""server config""" """server config"""
@@ -679,6 +761,9 @@ type Query {
"""Get workspace by id""" """Get workspace by id"""
workspace(id: String!): WorkspaceType! workspace(id: String!): WorkspaceType!
"""Get workspace role permissions"""
workspaceRolePermissions(id: String!): WorkspaceRolePermissions!
"""Get all accessible workspaces for current user""" """Get all accessible workspaces for current user"""
workspaces: [WorkspaceType!]! workspaces: [WorkspaceType!]!
} }
@@ -713,6 +798,35 @@ type RemoveAvatar {
success: Boolean! 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 { type RuntimeConfigNotFoundDataType {
key: String! key: String!
} }
@@ -814,6 +928,10 @@ type SpaceOwnerNotFoundDataType {
spaceId: String! spaceId: String!
} }
type SpaceShouldHaveOnlyOneOwnerDataType {
spaceId: String!
}
type SubscriptionAlreadyExistsDataType { type SubscriptionAlreadyExistsDataType {
plan: String! plan: String!
} }
@@ -988,6 +1106,15 @@ type WorkspaceBlobSizes {
size: SafeInt! size: SafeInt!
} }
type WorkspaceFeatureType {
"""Workspace created date"""
createdAt: DateTime!
id: ID!
"""is Public workspace"""
public: Boolean!
}
"""Workspace invite link expire time""" """Workspace invite link expire time"""
enum WorkspaceInviteLinkExpireTime { enum WorkspaceInviteLinkExpireTime {
OneDay OneDay
@@ -1023,6 +1150,31 @@ type WorkspacePageMeta {
updatedBy: EditorType updatedBy: EditorType
} }
type WorkspacePermissionNotFoundDataType {
spaceId: String!
}
type WorkspacePermissions {
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 WorkspaceRolePermissions {
permissions: WorkspacePermissions!
role: Permission!
}
type WorkspaceType { type WorkspaceType {
"""Available features of workspace""" """Available features of workspace"""
availableFeatures: [FeatureType!]! availableFeatures: [FeatureType!]!
@@ -1069,11 +1221,14 @@ type WorkspaceType {
"""Owner of workspace""" """Owner of workspace"""
owner: UserType! owner: UserType!
"""Page granted users list"""
pageGrantedUsersList(pageGrantedUsersInput: PageGrantedUsersInput!, pageId: String!): GrantedDocUsersConnection!
"""Cloud page metadata of workspace""" """Cloud page metadata of workspace"""
pageMeta(pageId: String!): WorkspacePageMeta! pageMeta(pageId: String!): WorkspacePageMeta!
"""Permission of current signed in user in workspace""" """Check if current user has permission to access the page"""
permission: Permission! pagePermission(pageId: String!): DocType!
"""is Public workspace""" """is Public workspace"""
public: Boolean! public: Boolean!
@@ -1087,6 +1242,9 @@ type WorkspaceType {
"""quota of workspace""" """quota of workspace"""
quota: QuotaQueryType! quota: QuotaQueryType!
"""Role of current signed in user in workspace"""
role: Permission!
"""Shared pages of workspace""" """Shared pages of workspace"""
sharedPages: [String!]! @deprecated(reason: "use WorkspaceType.publicPages") sharedPages: [String!]! @deprecated(reason: "use WorkspaceType.publicPages")