diff --git a/packages/backend/server/schema.prisma b/packages/backend/server/schema.prisma index 843d7c18e6..9ffabef6fc 100644 --- a/packages/backend/server/schema.prisma +++ b/packages/backend/server/schema.prisma @@ -115,13 +115,15 @@ model Workspace { // NOTE: // We won't make sure every page has a corresponding record in this table. // Only the ones that have ever changed will have records here, -// and for others we will make sure it's has a default value return in our bussiness logic. +// and for others we will make sure it's has a default value return in our business logic. model WorkspacePage { - workspaceId String @map("workspace_id") @db.VarChar - pageId String @map("page_id") @db.VarChar - public Boolean @default(false) + workspaceId String @map("workspace_id") @db.VarChar + pageId String @map("page_id") @db.VarChar + public Boolean @default(false) + // Workspace user's default role in this page, default is `Manager` + defaultRole Int @default(30) @db.SmallInt // Page/Edgeless - mode Int @default(0) @db.SmallInt + mode Int @default(0) @db.SmallInt workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade) @@ -277,7 +279,7 @@ model Snapshot { // user snapshots are special snapshots for user storage like personal app settings, distinguished from workspace snapshots // basically they share the same structure with workspace snapshots -// but for convenience, we don't fork the updates queue and hisotry for user snapshots, until we have to +// but for convenience, we don't fork the updates queue and history for user snapshots, until we have to // which means all operation on user snapshot will happen in-pace model UserSnapshot { userId String @map("user_id") @db.VarChar @@ -299,7 +301,7 @@ model Update { createdAt DateTime @map("created_at") @db.Timestamptz(3) createdBy String? @map("created_by") @db.VarChar - // will delete createor record if createor's account is deleted + // will delete creator record if creator's account is deleted createdByUser User? @relation(name: "createdUpdate", fields: [createdBy], references: [id], onDelete: SetNull) // @deprecated use createdAt only @@ -318,7 +320,7 @@ model SnapshotHistory { expiredAt DateTime @map("expired_at") @db.Timestamptz(3) createdBy String? @map("created_by") @db.VarChar - // will delete createor record if creator's account is deleted + // will delete creator record if creator's account is deleted createdByUser User? @relation(name: "createdHistory", fields: [createdBy], references: [id], onDelete: SetNull) @@id([workspaceId, id, timestamp]) diff --git a/packages/backend/server/src/__tests__/team.e2e.ts b/packages/backend/server/src/__tests__/team.e2e.ts index 3d9e5998bc..8ac092afa1 100644 --- a/packages/backend/server/src/__tests__/team.e2e.ts +++ b/packages/backend/server/src/__tests__/team.e2e.ts @@ -6,7 +6,9 @@ import { getCurrentMailMessageCount } from '@affine-test/kit/utils/cloud'; import { WorkspaceMemberStatus } from '@prisma/client'; import type { TestFn } from 'ava'; import ava from 'ava'; +import { nanoid } from 'nanoid'; import Sinon from 'sinon'; +import request from 'supertest'; import { AppModule } from '../app.module'; import { EventBus } from '../base'; @@ -199,6 +201,11 @@ const init = async ( WorkspaceRole.Collaborator ); + const external = await invite( + `${prefix}external@affine.pro`, + WorkspaceRole.External + ); + return { invite, inviteBatch, @@ -209,12 +216,13 @@ const init = async ( admin, write, read, + external, }; }; test('should be able to invite multiple users', async t => { const { app } = t.context; - const { teamWorkspace: ws, owner, admin, write, read } = await init(app, 4); + const { teamWorkspace: ws, owner, admin, write, read } = await init(app, 5); { // no permission @@ -265,7 +273,7 @@ test('should be able to invite multiple users', async t => { test('should be able to check seat limit', async t => { const { app, permissions, models } = t.context; - const { invite, inviteBatch, teamWorkspace: ws } = await init(app, 4); + const { invite, inviteBatch, teamWorkspace: ws } = await init(app, 5); { // invite @@ -275,7 +283,7 @@ test('should be able to check seat limit', async t => { 'should throw error if exceed member limit' ); models.workspaceFeature.add(ws.id, 'team_plan_v1', 'test', { - memberLimit: 5, + memberLimit: 6, }); await t.notThrowsAsync( invite('member4@affine.pro', WorkspaceRole.Collaborator), @@ -303,7 +311,7 @@ test('should be able to check seat limit', async t => { // refresh seat, fifo sleep(1000); const [[members2]] = await inviteBatch(['member6@affine.pro']); - await permissions.refreshSeatStatus(ws.id, 6); + await permissions.refreshSeatStatus(ws.id, 7); t.is( await permissions.getWorkspaceMemberStatus( @@ -471,7 +479,7 @@ test('should be able to manage invite link', async t => { admin, write, read, - } = await init(app, 4); + } = await init(app); for (const [workspace, managers] of [ [ws, [owner]], @@ -519,7 +527,7 @@ test('should be able to manage invite link', async t => { test('should be able to approve team member', async t => { const { app } = t.context; - const { teamWorkspace: tws, owner, admin, write, read } = await init(app, 5); + const { teamWorkspace: tws, owner, admin, write, read } = await init(app, 6); { const { link } = await createInviteLink( @@ -577,7 +585,7 @@ test('should be able to invite by link', async t => { owner, workspace: ws, teamWorkspace: tws, - } = await init(app, 4); + } = await init(app, 5); const [inviteId, invite] = await createInviteLink(ws); const [teamInviteId, teamInvite, acceptTeamInvite] = await createInviteLink(tws); @@ -594,7 +602,7 @@ test('should be able to invite by link', async t => { { // invite link - for (const [i] of Array.from({ length: 6 }).entries()) { + for (const [i] of Array.from({ length: 5 }).entries()) { const user = await invite(`test${i}@affine.pro`); const status = await permissions.getWorkspaceMemberStatus(ws.id, user.id); t.is( @@ -632,9 +640,9 @@ test('should be able to invite by link', async t => { ); models.workspaceFeature.add(tws.id, 'team_plan_v1', 'test', { - memberLimit: 5, + memberLimit: 6, }); - await permissions.refreshSeatStatus(tws.id, 5); + await permissions.refreshSeatStatus(tws.id, 6); t.is( await permissions.getWorkspaceMemberStatus(tws.id, m3.id), WorkspaceMemberStatus.UnderReview, @@ -647,9 +655,9 @@ test('should be able to invite by link', async t => { ); models.workspaceFeature.add(tws.id, 'team_plan_v1', 'test', { - memberLimit: 6, + memberLimit: 7, }); - await permissions.refreshSeatStatus(tws.id, 6); + await permissions.refreshSeatStatus(tws.id, 7); t.is( await permissions.getWorkspaceMemberStatus(tws.id, m4.id), WorkspaceMemberStatus.UnderReview, @@ -669,7 +677,7 @@ test('should be able to invite by link', async t => { test('should be able to send mails', async t => { const { app } = t.context; - const { inviteBatch } = await init(app, 4); + const { inviteBatch } = await init(app, 5); const primitiveMailCount = await getCurrentMailMessageCount(); { @@ -682,7 +690,7 @@ test('should be able to emit events', async t => { const { app, event } = t.context; { - const { teamWorkspace: tws, inviteBatch } = await init(app, 4); + const { teamWorkspace: tws, inviteBatch } = await init(app, 5); await inviteBatch(['m1@affine.pro', 'm2@affine.pro']); const [membersUpdated] = event.emit @@ -693,7 +701,7 @@ test('should be able to emit events', async t => { 'workspace.members.updated', { workspaceId: tws.id, - count: 6, + count: 7, }, ]); } @@ -787,7 +795,7 @@ test('should be able to emit events', async t => { [ 'workspace.members.updated', { - count: 3, + count: 4, workspaceId: tws.id, }, ], @@ -795,3 +803,225 @@ test('should be able to emit events', async t => { ); } }); + +test('should be able to change the default role in page', async t => { + const { app } = t.context; + const { teamWorkspace: ws, admin } = await init(app, 5); + const pageId = nanoid(); + const res = await request(app.getHttpServer()) + .post('/graphql') + .auth(admin.token.token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + updatePageDefaultRole(input: { + workspaceId: "${ws.id}", + docId: "${pageId}", + role: Reader, + }) + } + `, + }) + .expect(200); + + t.deepEqual(res.body, { + data: { + updatePageDefaultRole: true, + }, + }); +}); + +test('Default page role should be able to override the workspace role', async t => { + const { app } = t.context; + const { + teamWorkspace: workspace, + admin, + read, + external, + } = await init(app, 5); + + const pageId = nanoid(); + + const res = await request(app.getHttpServer()) + .post('/graphql') + .auth(admin.token.token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + updatePageDefaultRole(input: { + workspaceId: "${workspace.id}", + docId: "${pageId}", + role: Manager, + }) + } + `, + }) + .expect(200); + + t.deepEqual(res.body, { + data: { + updatePageDefaultRole: true, + }, + }); + + // reader can manage the page if the page default role is Manager + { + const readerRes = await request(app.getHttpServer()) + .post('/graphql') + .auth(read.token.token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + updatePageDefaultRole(input: { + workspaceId: "${workspace.id}", + docId: "${pageId}", + role: Manager, + }) + } + `, + }) + .expect(200); + + t.deepEqual(readerRes.body, { + data: { + updatePageDefaultRole: true, + }, + }); + } + + // external can't manage the page even if the page default role is Manager + { + const externalRes = await request(app.getHttpServer()) + .post('/graphql') + .auth(external.token.token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + updatePageDefaultRole(input: { + workspaceId: "${workspace.id}", + docId: "${pageId}", + role: Manager, + }) + } + `, + }) + .expect(200); + + t.like(externalRes.body, { + errors: [ + { + message: `You do not have permission to access doc ${pageId} under Space ${workspace.id}.`, + }, + ], + }); + } +}); + +test('should be able to grant and revoke doc user role', async t => { + const { app } = t.context; + const { teamWorkspace: ws, admin, read, external } = await init(app, 5); + const pageId = nanoid(); + const res = await request(app.getHttpServer()) + .post('/graphql') + .auth(admin.token.token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + grantDocUserRoles(input: { + workspaceId: "${ws.id}", + docId: "${pageId}", + role: Manager, + userIds: ["${external.id}"] + }) + } + `, + }) + .expect(200); + + t.deepEqual(res.body, { + data: { + grantDocUserRoles: true, + }, + }); + + // external user now can manage the page + { + const externalRes = await request(app.getHttpServer()) + .post('/graphql') + .auth(external.token.token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + grantDocUserRoles(input: { + workspaceId: "${ws.id}", + docId: "${pageId}", + role: Manager, + userIds: ["${read.id}"] + }) + } + `, + }) + .expect(200); + t.deepEqual(externalRes.body, { + data: { + grantDocUserRoles: true, + }, + }); + } + + // revoke the role of the external user + { + const revokeRes = await request(app.getHttpServer()) + .post('/graphql') + .auth(admin.token.token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + revokeDocUserRoles(input: { + workspaceId: "${ws.id}", + docId: "${pageId}", + userIds: ["${external.id}"] + }) + } + `, + }) + .expect(200); + t.deepEqual(revokeRes.body, { + data: { + revokeDocUserRoles: true, + }, + }); + + // external user can't manage the page + const externalRes = await request(app.getHttpServer()) + .post('/graphql') + .auth(external.token.token, { type: 'bearer' }) + .set({ 'x-request-id': 'test', 'x-operation-name': 'test' }) + .send({ + query: ` + mutation { + revokeDocUserRoles(input: { + workspaceId: "${ws.id}", + docId: "${pageId}", + userIds: ["${read.id}"] + }) + } + `, + }) + .expect(200); + t.like(externalRes.body, { + errors: [ + { + message: `You do not have permission to access Space ${ws.id}.`, + }, + ], + }); + } +}); diff --git a/packages/backend/server/src/__tests__/workspace.e2e.ts b/packages/backend/server/src/__tests__/workspace.e2e.ts index c32ed40b57..adbdb360bf 100644 --- a/packages/backend/server/src/__tests__/workspace.e2e.ts +++ b/packages/backend/server/src/__tests__/workspace.e2e.ts @@ -54,7 +54,7 @@ test('should create a workspace', async t => { t.is(typeof workspace.id, 'string', 'workspace.id is not a string'); }); -test('should can publish workspace', async t => { +test('should be able to publish workspace', async t => { const { app } = t.context; const user = await signUp(app, 'u1', 'u1@affine.pro', '1'); const workspace = await createWorkspace(app, user.token.token); diff --git a/packages/backend/server/src/base/error/def.ts b/packages/backend/server/src/base/error/def.ts index 550a8ffef9..d58ba9e33c 100644 --- a/packages/backend/server/src/base/error/def.ts +++ b/packages/backend/server/src/base/error/def.ts @@ -470,6 +470,10 @@ export const USER_FRIENDLY_ERRORS = { type: 'action_forbidden', message: 'A Team workspace is required to perform this action.', }, + page_default_role_can_not_be_owner: { + type: 'invalid_input', + message: 'Page default role can not be owner.', + }, // Subscription Errors unsupported_subscription_plan: { diff --git a/packages/backend/server/src/base/error/errors.gen.ts b/packages/backend/server/src/base/error/errors.gen.ts index 3d12ed1638..9ba77fbaab 100644 --- a/packages/backend/server/src/base/error/errors.gen.ts +++ b/packages/backend/server/src/base/error/errors.gen.ts @@ -405,6 +405,12 @@ export class ActionForbiddenOnNonTeamWorkspace extends UserFriendlyError { super('action_forbidden', 'action_forbidden_on_non_team_workspace', message); } } + +export class PageDefaultRoleCanNotBeOwner extends UserFriendlyError { + constructor(message?: string) { + super('invalid_input', 'page_default_role_can_not_be_owner', message); + } +} @ObjectType() class UnsupportedSubscriptionPlanDataType { @Field() plan!: string @@ -761,6 +767,7 @@ export enum ErrorNames { FAILED_TO_SAVE_UPDATES, FAILED_TO_UPSERT_SNAPSHOT, ACTION_FORBIDDEN_ON_NON_TEAM_WORKSPACE, + PAGE_DEFAULT_ROLE_CAN_NOT_BE_OWNER, UNSUPPORTED_SUBSCRIPTION_PLAN, FAILED_TO_CHECKOUT, INVALID_CHECKOUT_PARAMETERS, diff --git a/packages/backend/server/src/core/permission/service.ts b/packages/backend/server/src/core/permission/service.ts index 34915c9d88..372bb5962e 100644 --- a/packages/backend/server/src/core/permission/service.ts +++ b/packages/backend/server/src/core/permission/service.ts @@ -571,30 +571,69 @@ export class PermissionService { } if (user) { - const count = await this.prisma.workspacePageUserPermission.count({ - where: { - workspaceId: ws, - pageId: page, - userId: user, - type: { - gte: role, + const [roleEntity, pageEntity, workspaceRoleEntity] = await Promise.all([ + this.prisma.workspacePageUserPermission.findFirst({ + where: { + workspaceId: ws, + pageId: page, + userId: user, }, - }, - }); + select: { + type: true, + }, + }), + this.prisma.workspacePage.findFirst({ + where: { + workspaceId: ws, + pageId: page, + }, + select: { + defaultRole: true, + }, + }), + this.prisma.workspaceUserPermission.findFirst({ + where: { + workspaceId: ws, + userId: user, + OR: this.acceptedCondition, + }, + select: { + type: true, + }, + }), + ]); - // page shared to user - // accessible - if (count > 0) { + if ( + // Page role exists, check it first + (roleEntity && roleEntity.type >= role) || + // if + // - page has a default role + // - the user is in this workspace + // - the user is not an external user in this workspace + // then use the max of the two + (workspaceRoleEntity && + workspaceRoleEntity.type !== WorkspaceRole.External && + Math.max( + roleEntity?.type ?? Number.MIN_SAFE_INTEGER, + pageEntity?.defaultRole ?? Number.MIN_SAFE_INTEGER + ) >= role) + ) { return true; - } else { - this.logger.log("User's PageRole is lower than required", { - workspaceId: ws, - pageId: page, - userId: user, - requiredRole: DocRole[role], - action, - }); } + this.logger.log("User's role is lower than required", { + workspaceId: ws, + docId: page, + userId: user, + workspaceRole: workspaceRoleEntity + ? WorkspaceRole[workspaceRoleEntity.type] + : undefined, + pageRole: roleEntity ? DocRole[roleEntity.type] : undefined, + pageDefaultRole: pageEntity + ? DocRole[pageEntity.defaultRole] + : undefined, + requiredRole: DocRole[role], + action, + }); } // check whether user has workspace related permission diff --git a/packages/backend/server/src/core/permission/types.ts b/packages/backend/server/src/core/permission/types.ts index a14cde9817..2e97636279 100644 --- a/packages/backend/server/src/core/permission/types.ts +++ b/packages/backend/server/src/core/permission/types.ts @@ -240,7 +240,7 @@ export function mapDocRoleToPermissions(docRole: DocRole) { export function fixupDocRole( workspaceRole: WorkspaceRole = WorkspaceRole.External, docRole: DocRole = DocRole.External -) { +): DocRole { switch (workspaceRole) { case WorkspaceRole.External: // Workspace External user won't be able to have any high permission doc role diff --git a/packages/backend/server/src/core/workspaces/resolvers/page.ts b/packages/backend/server/src/core/workspaces/resolvers/page.ts index 53d2138c58..c0255a1c87 100644 --- a/packages/backend/server/src/core/workspaces/resolvers/page.ts +++ b/packages/backend/server/src/core/workspaces/resolvers/page.ts @@ -14,11 +14,13 @@ import type { WorkspacePage as PrismaWorkspacePage } from '@prisma/client'; import { PrismaClient } from '@prisma/client'; import { + DocAccessDenied, ExpectToGrantDocUserRoles, ExpectToPublishPage, ExpectToRevokeDocUserRoles, ExpectToRevokePublicPage, ExpectToUpdateDocUserRole, + PageDefaultRoleCanNotBeOwner, PageIsNotPublic, paginate, Paginated, @@ -74,6 +76,45 @@ class GrantDocUserRolesInput { userIds!: string[]; } +@InputType() +class UpdateDocUserRoleInput { + @Field(() => String) + docId!: string; + + @Field(() => String) + workspaceId!: string; + + @Field(() => String) + userId!: string; + + @Field(() => DocRole) + role!: DocRole; +} + +@InputType() +class RevokeDocUserRolesInput { + @Field(() => String) + docId!: string; + + @Field(() => String) + workspaceId!: string; + + @Field(() => [String]) + userIds!: string[]; +} + +@InputType() +class UpdatePageDefaultRoleInput { + @Field(() => String) + docId!: string; + + @Field(() => String) + workspaceId!: string; + + @Field(() => DocRole) + role!: DocRole; +} + @ObjectType() class GrantedDocUserType { @Field(() => String) @@ -413,12 +454,11 @@ export class PagePermissionResolver { @Mutation(() => Boolean) async revokeDocUserRoles( @CurrentUser() user: CurrentUser, - @Args('docId') docId: string, - @Args('userIds', { type: () => [String] }) userIds: string[] + @Args('input') input: RevokeDocUserRolesInput ): Promise { - const doc = new DocID(docId); + const doc = new DocID(input.docId, input.workspaceId); const pairs = { - spaceId: doc.workspace, + spaceId: input.workspaceId, docId: doc.guid, }; if (doc.isWorkspace) { @@ -436,10 +476,10 @@ export class PagePermissionResolver { user.id, WorkspaceRole.Collaborator ); - await this.permission.revokePage(doc.workspace, doc.guid, userIds); + await this.permission.revokePage(doc.workspace, doc.guid, input.userIds); this.logger.log('Revoke doc user roles', { ...pairs, - userIds: userIds, + userIds: input.userIds, }); return true; } @@ -447,11 +487,9 @@ export class PagePermissionResolver { @Mutation(() => Boolean) async updateDocUserRole( @CurrentUser() user: CurrentUser, - @Args('docId') docId: string, - @Args('userId') userId: string, - @Args('role', { type: () => DocRole }) role: DocRole + @Args('input') input: UpdateDocUserRoleInput ): Promise { - const doc = new DocID(docId); + const doc = new DocID(input.docId, input.workspaceId); const pairs = { spaceId: doc.workspace, docId: doc.guid, @@ -471,32 +509,94 @@ export class PagePermissionResolver { user.id, WorkspaceRole.Collaborator ); - if (role === DocRole.Owner) { + if (input.role === DocRole.Owner) { const ret = await this.permission.grantPagePermission( doc.workspace, doc.guid, - [userId], - role + [input.userId], + input.role ); this.logger.log('Transfer doc owner', { ...pairs, - userId: userId, - role: role, + userId: input.userId, + role: input.role, }); return ret.length > 0; } else { await this.permission.updatePagePermission( doc.workspace, doc.guid, - userId, - role + input.userId, + input.role ); this.logger.log('Update doc user role', { ...pairs, - userId: userId, - role: role, + userId: input.userId, + role: input.role, }); return true; } } + + @Mutation(() => Boolean) + async updatePageDefaultRole( + @CurrentUser() user: CurrentUser, + @Args('input') input: UpdatePageDefaultRoleInput + ) { + if (input.role === DocRole.Owner) { + this.logger.log('Page default role can not be owner', input); + throw new PageDefaultRoleCanNotBeOwner(); + } + const doc = new DocID(input.docId, input.workspaceId); + const pairs = { + spaceId: doc.workspace, + docId: doc.guid, + }; + if (doc.isWorkspace) { + this.logger.error( + 'Expect to update page default role, but it is a workspace', + pairs + ); + throw new ExpectToUpdateDocUserRole( + pairs, + 'Expect doc not to be workspace' + ); + } + try { + await this.permission.checkCloudPagePermission( + doc.workspace, + doc.guid, + 'Doc.Users.Manage', + user.id + ); + } catch (error) { + if (error instanceof DocAccessDenied) { + this.logger.log( + 'User does not have permission to update page default role', + { + ...pairs, + userId: user.id, + } + ); + } + throw error; + } + await this.prisma.workspacePage.upsert({ + where: { + workspaceId_pageId: { + workspaceId: doc.workspace, + pageId: doc.guid, + }, + }, + update: { + defaultRole: input.role, + }, + create: { + workspaceId: doc.workspace, + pageId: doc.guid, + defaultRole: input.role, + }, + }); + return true; + } } diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 2c6dbd2980..2d50a7d714 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -305,6 +305,7 @@ enum ErrorNames { NO_COPILOT_PROVIDER_AVAILABLE OAUTH_ACCOUNT_ALREADY_CONNECTED OAUTH_STATE_EXPIRED + PAGE_DEFAULT_ROLE_CAN_NOT_BE_OWNER PAGE_IS_NOT_PUBLIC PASSWORD_REQUIRED QUERY_TOO_LONG @@ -630,7 +631,7 @@ type Mutation { removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean! resumeSubscription(idempotencyKey: String @deprecated(reason: "use header `Idempotency-Key`"), plan: SubscriptionPlan = Pro, workspaceId: String): SubscriptionType! revoke(userId: String!, workspaceId: String!): Boolean! - revokeDocUserRoles(docId: String!, userIds: [String!]!): Boolean! + revokeDocUserRoles(input: RevokeDocUserRolesInput!): Boolean! revokeInviteLink(workspaceId: String!): Boolean! revokePage(pageId: String!, workspaceId: String!): Boolean! @deprecated(reason: "use revokePublicPage") revokePublicPage(pageId: String!, workspaceId: String!): WorkspacePage! @@ -647,7 +648,8 @@ type Mutation { """Update a chat session""" updateCopilotSession(options: UpdateChatSessionInput!): String! - updateDocUserRole(docId: String!, role: DocRole!, userId: String!): Boolean! + updateDocUserRole(input: UpdateDocUserRoleInput!): Boolean! + updatePageDefaultRole(input: UpdatePageDefaultRoleInput!): Boolean! updateProfile(input: UpdateUserInput!): UserType! """update server runtime configurable setting""" @@ -802,6 +804,12 @@ type RemoveAvatar { success: Boolean! } +input RevokeDocUserRolesInput { + docId: String! + userIds: [String!]! + workspaceId: String! +} + type RuntimeConfigNotFoundDataType { key: String! } @@ -996,6 +1004,19 @@ input UpdateChatSessionInput { sessionId: String! } +input UpdateDocUserRoleInput { + docId: String! + role: DocRole! + userId: String! + workspaceId: String! +} + +input UpdatePageDefaultRoleInput { + docId: String! + role: DocRole! + workspaceId: String! +} + input UpdateUserInput { """User name""" name: String