fix(server): batch grant page roles (#10007)
This commit is contained in:
@@ -987,7 +987,7 @@ test('should be able to grant and revoke doc user role', async t => {
|
|||||||
revokeDocUserRoles(input: {
|
revokeDocUserRoles(input: {
|
||||||
workspaceId: "${ws.id}",
|
workspaceId: "${ws.id}",
|
||||||
docId: "${pageId}",
|
docId: "${pageId}",
|
||||||
userIds: ["${external.id}"]
|
userId: "${external.id}"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -1010,7 +1010,7 @@ test('should be able to grant and revoke doc user role', async t => {
|
|||||||
revokeDocUserRoles(input: {
|
revokeDocUserRoles(input: {
|
||||||
workspaceId: "${ws.id}",
|
workspaceId: "${ws.id}",
|
||||||
docId: "${pageId}",
|
docId: "${pageId}",
|
||||||
userIds: ["${read.id}"]
|
userId: "${read.id}"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
@@ -1019,7 +1019,7 @@ test('should be able to grant and revoke doc user role', async t => {
|
|||||||
t.like(externalRes.body, {
|
t.like(externalRes.body, {
|
||||||
errors: [
|
errors: [
|
||||||
{
|
{
|
||||||
message: `You do not have permission to access Space ${ws.id}.`,
|
message: `You do not have permission to access doc ${pageId} under Space ${ws.id}.`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -474,6 +474,10 @@ export const USER_FRIENDLY_ERRORS = {
|
|||||||
type: 'invalid_input',
|
type: 'invalid_input',
|
||||||
message: 'Page default role can not be owner.',
|
message: 'Page default role can not be owner.',
|
||||||
},
|
},
|
||||||
|
can_not_batch_grant_page_owner_permissions: {
|
||||||
|
type: 'invalid_input',
|
||||||
|
message: 'Can not batch grant page owner permissions.',
|
||||||
|
},
|
||||||
|
|
||||||
// Subscription Errors
|
// Subscription Errors
|
||||||
unsupported_subscription_plan: {
|
unsupported_subscription_plan: {
|
||||||
|
|||||||
@@ -411,6 +411,12 @@ export class PageDefaultRoleCanNotBeOwner extends UserFriendlyError {
|
|||||||
super('invalid_input', 'page_default_role_can_not_be_owner', message);
|
super('invalid_input', 'page_default_role_can_not_be_owner', message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CanNotBatchGrantPageOwnerPermissions extends UserFriendlyError {
|
||||||
|
constructor(message?: string) {
|
||||||
|
super('invalid_input', 'can_not_batch_grant_page_owner_permissions', message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
class UnsupportedSubscriptionPlanDataType {
|
class UnsupportedSubscriptionPlanDataType {
|
||||||
@Field() plan!: string
|
@Field() plan!: string
|
||||||
@@ -768,6 +774,7 @@ export enum ErrorNames {
|
|||||||
FAILED_TO_UPSERT_SNAPSHOT,
|
FAILED_TO_UPSERT_SNAPSHOT,
|
||||||
ACTION_FORBIDDEN_ON_NON_TEAM_WORKSPACE,
|
ACTION_FORBIDDEN_ON_NON_TEAM_WORKSPACE,
|
||||||
PAGE_DEFAULT_ROLE_CAN_NOT_BE_OWNER,
|
PAGE_DEFAULT_ROLE_CAN_NOT_BE_OWNER,
|
||||||
|
CAN_NOT_BATCH_GRANT_PAGE_OWNER_PERMISSIONS,
|
||||||
UNSUPPORTED_SUBSCRIPTION_PLAN,
|
UNSUPPORTED_SUBSCRIPTION_PLAN,
|
||||||
FAILED_TO_CHECKOUT,
|
FAILED_TO_CHECKOUT,
|
||||||
INVALID_CHECKOUT_PARAMETERS,
|
INVALID_CHECKOUT_PARAMETERS,
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import type { Prisma } from '@prisma/client';
|
import type { Prisma, WorkspacePageUserPermission } 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';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CanNotBatchGrantPageOwnerPermissions,
|
||||||
DocAccessDenied,
|
DocAccessDenied,
|
||||||
EventBus,
|
EventBus,
|
||||||
OnEvent,
|
OnEvent,
|
||||||
SpaceAccessDenied,
|
SpaceAccessDenied,
|
||||||
SpaceOwnerNotFound,
|
SpaceOwnerNotFound,
|
||||||
SpaceShouldHaveOnlyOneOwner,
|
|
||||||
WorkspacePermissionNotFound,
|
WorkspacePermissionNotFound,
|
||||||
} from '../../base';
|
} from '../../base';
|
||||||
import {
|
import {
|
||||||
@@ -737,17 +737,15 @@ export class PermissionService {
|
|||||||
].filter(Boolean) as Prisma.PrismaPromise<any>[]
|
].filter(Boolean) as Prisma.PrismaPromise<any>[]
|
||||||
);
|
);
|
||||||
|
|
||||||
return p;
|
return p as WorkspacePageUserPermission;
|
||||||
}
|
}
|
||||||
|
|
||||||
async revokePage(ws: string, page: string, users: string[]) {
|
async revokePage(ws: string, page: string, user: 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: {
|
userId: user,
|
||||||
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: DocRole.Owner,
|
not: DocRole.Owner,
|
||||||
@@ -758,74 +756,30 @@ export class PermissionService {
|
|||||||
return result.count > 0;
|
return result.count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async grantPagePermission(
|
async batchGrantPage(
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
pageId: string,
|
pageId: string,
|
||||||
userIds: string[],
|
userIds: string[],
|
||||||
role: DocRole
|
role: DocRole
|
||||||
) {
|
) {
|
||||||
if (userIds.length === 0) {
|
if (userIds.length === 0) {
|
||||||
return [];
|
return 0;
|
||||||
}
|
|
||||||
if (role === DocRole.Owner && userIds.length > 1) {
|
|
||||||
throw new SpaceShouldHaveOnlyOneOwner({ spaceId: workspaceId });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await this.prisma.$transaction(async tx =>
|
if (role === DocRole.Owner) {
|
||||||
Promise.all(
|
throw new CanNotBatchGrantPageOwnerPermissions();
|
||||||
userIds.map(id =>
|
}
|
||||||
tx.workspacePageUserPermission.upsert({
|
|
||||||
where: {
|
|
||||||
workspaceId_pageId_userId: {
|
|
||||||
workspaceId,
|
|
||||||
pageId,
|
|
||||||
userId: id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
workspaceId,
|
|
||||||
pageId,
|
|
||||||
userId: id,
|
|
||||||
type: role,
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
type: role,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updatePagePermission(
|
const result = await this.prisma.workspacePageUserPermission.createMany({
|
||||||
workspaceId: string,
|
skipDuplicates: true,
|
||||||
pageId: string,
|
data: userIds.map(id => ({
|
||||||
userId: string,
|
|
||||||
role: DocRole
|
|
||||||
) {
|
|
||||||
const permission = await this.prisma.workspacePageUserPermission.findFirst({
|
|
||||||
where: {
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
pageId,
|
pageId,
|
||||||
userId,
|
userId: id,
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!permission) {
|
|
||||||
return this.grantPage(workspaceId, pageId, userId, role);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.prisma.workspacePageUserPermission.update({
|
|
||||||
where: {
|
|
||||||
workspaceId_pageId_userId: {
|
|
||||||
workspaceId,
|
|
||||||
pageId,
|
|
||||||
userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
type: role,
|
type: role,
|
||||||
},
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result.count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import {
|
|||||||
mapDocRoleToPermissions,
|
mapDocRoleToPermissions,
|
||||||
PermissionService,
|
PermissionService,
|
||||||
PublicPageMode,
|
PublicPageMode,
|
||||||
WorkspaceRole,
|
|
||||||
} from '../../permission';
|
} from '../../permission';
|
||||||
import { PublicUserType } from '../../user';
|
import { PublicUserType } from '../../user';
|
||||||
import { DocID } from '../../utils/doc';
|
import { DocID } from '../../utils/doc';
|
||||||
@@ -94,15 +93,15 @@ class UpdateDocUserRoleInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
class RevokeDocUserRolesInput {
|
class RevokeDocUserRoleInput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
docId!: string;
|
docId!: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
workspaceId!: string;
|
workspaceId!: string;
|
||||||
|
|
||||||
@Field(() => [String])
|
@Field(() => String)
|
||||||
userIds!: string[];
|
userId!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
@@ -263,10 +262,18 @@ export class PagePermissionResolver {
|
|||||||
complexity: 4,
|
complexity: 4,
|
||||||
})
|
})
|
||||||
async pageGrantedUsersList(
|
async pageGrantedUsersList(
|
||||||
|
@CurrentUser() user: CurrentUser,
|
||||||
@Parent() workspace: WorkspaceType,
|
@Parent() workspace: WorkspaceType,
|
||||||
@Args('pageId') pageId: string,
|
@Args('pageId') pageId: string,
|
||||||
@Args('pagination') pagination: PaginationInput
|
@Args('pagination') pagination: PaginationInput
|
||||||
): Promise<PaginatedGrantedDocUserType> {
|
): Promise<PaginatedGrantedDocUserType> {
|
||||||
|
await this.permission.checkPagePermission(
|
||||||
|
workspace.id,
|
||||||
|
pageId,
|
||||||
|
'Doc.Users.Read',
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
|
||||||
const docId = new DocID(pageId, workspace.id);
|
const docId = new DocID(pageId, workspace.id);
|
||||||
const [permissions, totalCount] = await this.prisma.$transaction(tx => {
|
const [permissions, totalCount] = await this.prisma.$transaction(tx => {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
@@ -454,7 +461,7 @@ export class PagePermissionResolver {
|
|||||||
'Doc.Users.Manage',
|
'Doc.Users.Manage',
|
||||||
user.id
|
user.id
|
||||||
);
|
);
|
||||||
await this.permission.grantPagePermission(
|
await this.permission.batchGrantPage(
|
||||||
doc.workspace,
|
doc.workspace,
|
||||||
doc.guid,
|
doc.guid,
|
||||||
input.userIds,
|
input.userIds,
|
||||||
@@ -471,7 +478,7 @@ export class PagePermissionResolver {
|
|||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async revokeDocUserRoles(
|
async revokeDocUserRoles(
|
||||||
@CurrentUser() user: CurrentUser,
|
@CurrentUser() user: CurrentUser,
|
||||||
@Args('input') input: RevokeDocUserRolesInput
|
@Args('input') input: RevokeDocUserRoleInput
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const doc = new DocID(input.docId, input.workspaceId);
|
const doc = new DocID(input.docId, input.workspaceId);
|
||||||
const pairs = {
|
const pairs = {
|
||||||
@@ -488,15 +495,16 @@ export class PagePermissionResolver {
|
|||||||
'Expect doc not to be workspace'
|
'Expect doc not to be workspace'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await this.permission.checkWorkspace(
|
await this.permission.checkPagePermission(
|
||||||
doc.workspace,
|
doc.workspace,
|
||||||
user.id,
|
doc.guid,
|
||||||
WorkspaceRole.Collaborator
|
'Doc.Users.Manage',
|
||||||
|
user.id
|
||||||
);
|
);
|
||||||
await this.permission.revokePage(doc.workspace, doc.guid, input.userIds);
|
await this.permission.revokePage(doc.workspace, doc.guid, input.userId);
|
||||||
this.logger.log('Revoke doc user roles', {
|
this.logger.log('Revoke doc user roles', {
|
||||||
...pairs,
|
...pairs,
|
||||||
userIds: input.userIds,
|
userId: input.userId,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -521,38 +529,36 @@ export class PagePermissionResolver {
|
|||||||
'Expect doc not to be workspace'
|
'Expect doc not to be workspace'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await this.permission.checkWorkspace(
|
|
||||||
|
await this.permission.checkPagePermission(
|
||||||
doc.workspace,
|
doc.workspace,
|
||||||
user.id,
|
doc.guid,
|
||||||
WorkspaceRole.Collaborator
|
input.role === DocRole.Owner ? 'Doc.TransferOwner' : 'Doc.Users.Manage',
|
||||||
|
user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.permission.grantPage(
|
||||||
|
doc.workspace,
|
||||||
|
doc.guid,
|
||||||
|
input.userId,
|
||||||
|
input.role
|
||||||
|
);
|
||||||
|
|
||||||
if (input.role === DocRole.Owner) {
|
if (input.role === DocRole.Owner) {
|
||||||
const ret = await this.permission.grantPagePermission(
|
|
||||||
doc.workspace,
|
|
||||||
doc.guid,
|
|
||||||
[input.userId],
|
|
||||||
input.role
|
|
||||||
);
|
|
||||||
this.logger.log('Transfer doc owner', {
|
this.logger.log('Transfer doc owner', {
|
||||||
...pairs,
|
...pairs,
|
||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
role: input.role,
|
role: input.role,
|
||||||
});
|
});
|
||||||
return ret.length > 0;
|
|
||||||
} else {
|
} else {
|
||||||
await this.permission.updatePagePermission(
|
|
||||||
doc.workspace,
|
|
||||||
doc.guid,
|
|
||||||
input.userId,
|
|
||||||
input.role
|
|
||||||
);
|
|
||||||
this.logger.log('Update doc user role', {
|
this.logger.log('Update doc user role', {
|
||||||
...pairs,
|
...pairs,
|
||||||
userId: input.userId,
|
userId: input.userId,
|
||||||
role: input.role,
|
role: input.role,
|
||||||
});
|
});
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
@@ -580,7 +586,7 @@ export class PagePermissionResolver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.permission.checkCloudPagePermission(
|
await this.permission.checkPagePermission(
|
||||||
doc.workspace,
|
doc.workspace,
|
||||||
doc.guid,
|
doc.guid,
|
||||||
'Doc.Users.Manage',
|
'Doc.Users.Manage',
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ enum ErrorNames {
|
|||||||
CANNOT_DELETE_ALL_ADMIN_ACCOUNT
|
CANNOT_DELETE_ALL_ADMIN_ACCOUNT
|
||||||
CANNOT_DELETE_OWN_ACCOUNT
|
CANNOT_DELETE_OWN_ACCOUNT
|
||||||
CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION
|
CANT_UPDATE_ONETIME_PAYMENT_SUBSCRIPTION
|
||||||
|
CAN_NOT_BATCH_GRANT_PAGE_OWNER_PERMISSIONS
|
||||||
CAPTCHA_VERIFICATION_FAILED
|
CAPTCHA_VERIFICATION_FAILED
|
||||||
COPILOT_ACTION_TAKEN
|
COPILOT_ACTION_TAKEN
|
||||||
COPILOT_FAILED_TO_CREATE_MESSAGE
|
COPILOT_FAILED_TO_CREATE_MESSAGE
|
||||||
@@ -629,7 +630,7 @@ type Mutation {
|
|||||||
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean!
|
removeWorkspaceFeature(feature: FeatureType!, workspaceId: String!): Boolean!
|
||||||
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(input: RevokeDocUserRolesInput!): Boolean!
|
revokeDocUserRoles(input: RevokeDocUserRoleInput!): 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!
|
||||||
@@ -809,9 +810,9 @@ type RemoveAvatar {
|
|||||||
success: Boolean!
|
success: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
input RevokeDocUserRolesInput {
|
input RevokeDocUserRoleInput {
|
||||||
docId: String!
|
docId: String!
|
||||||
userIds: [String!]!
|
userId: String!
|
||||||
workspaceId: String!
|
workspaceId: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user