Skip to content

Commit 66d9a6d

Browse files
Fix Admin control to disable workspace creation for non-admin users (#14895)
fix #13460 --------- Co-authored-by: Félix Malfait <[email protected]>
1 parent cd4800e commit 66d9a6d

File tree

5 files changed

+48
-1
lines changed

5 files changed

+48
-1
lines changed

packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,10 @@ export class AuthResolver {
490490
@AuthUser() currentUser: User,
491491
@AuthProvider() authProvider: AuthProviderEnum,
492492
): Promise<SignUpOutput> {
493+
await this.signInUpService.checkWorkspaceCreationIsAllowedOrThrow(
494+
currentUser,
495+
);
496+
493497
const { user, workspace } = await this.signInUpService.signUpOnNewWorkspace(
494498
{ type: 'existingUser', existingUser: currentUser },
495499
);

packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,36 @@ export class SignInUpService {
355355
return { canImpersonate: false, canAccessFullAdminPanel: false };
356356
}
357357

358+
private isWorkspaceCreationLimitedToServerAdmins(): boolean {
359+
return this.twentyConfigService.get(
360+
'IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS',
361+
);
362+
}
363+
364+
private async isFirstWorkspaceForUser(userId: string): Promise<boolean> {
365+
const count = await this.userWorkspaceService.countUserWorkspaces(userId);
366+
367+
return count === 0;
368+
}
369+
370+
async checkWorkspaceCreationIsAllowedOrThrow(
371+
currentUser: User,
372+
): Promise<void> {
373+
if (!this.isWorkspaceCreationLimitedToServerAdmins()) return;
374+
375+
if (await this.isFirstWorkspaceForUser(currentUser.id)) return;
376+
377+
if (!currentUser.canAccessFullAdminPanel) {
378+
throw new AuthException(
379+
'Workspace creation is restricted to admins',
380+
AuthExceptionCode.FORBIDDEN_EXCEPTION,
381+
{
382+
userFriendlyMessage: t`Workspace creation is restricted to admins`,
383+
},
384+
);
385+
}
386+
}
387+
358388
async signUpOnNewWorkspace(
359389
userData: ExistingUserOrPartialUserWithPicture['userData'],
360390
) {

packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,15 @@ export class ConfigVariables {
358358
})
359359
EMAIL_SMTP_PASSWORD: string;
360360

361+
@ConfigVariablesMetadata({
362+
group: ConfigVariablesGroup.Other,
363+
description:
364+
'When enabled, only server admins can create new workspaces. Ignored during initial setup when no workspace exists.',
365+
type: ConfigVariableType.BOOLEAN,
366+
})
367+
@IsOptional()
368+
IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS = false;
369+
361370
@ConfigVariablesMetadata({
362371
group: ConfigVariablesGroup.StorageConfig,
363372
description: 'Type of storage to use (local or S3)',

packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
217217
return workspace;
218218
}
219219

220+
async countUserWorkspaces(userId: string): Promise<number> {
221+
return await this.userWorkspaceRepository.count({ where: { userId } });
222+
}
223+
220224
async findAvailableWorkspacesByEmail(email: string) {
221225
const user = await this.userRepository.findOne({
222226
where: {

packages/twenty-server/test/integration/graphql/suites/settings-permissions/roles.integration-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ describe('roles permissions', () => {
9090

9191
expect(resp.status).toBe(200);
9292
expect(resp.body.errors).toBeUndefined();
93-
expect(resp.body.data.getRoles).toHaveLength(5);
93+
expect(resp.body.data.getRoles).toHaveLength(7);
9494

9595
const roles = resp.body.data.getRoles;
9696
const guestRole = roles.find((role: any) => role.label === 'Guest');

0 commit comments

Comments
 (0)