[permissions - seeds] Give tim@apple.dev restricted rights (#12768)

Let's introduce an object-limited role for Tim, to test and/or spot
incompatibilities with restricted permissions in the future.
Our main user tim@apple.dev is now assigned a role that has all settings
permissions, and all object permissions except for update on Pets (to
test read-only view) and read on Rockets.
Since we still need an admin user for each workspace we are introducing
a new member, Jane, who has the admin role

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Marie
2025-06-23 15:46:53 +02:00
committed by GitHub
parent 8f0c9facf2
commit 2cb2f528df
12 changed files with 246 additions and 108 deletions

View File

@ -92,71 +92,6 @@ export class UserService extends TypeOrmQueryService<User> {
});
}
private async deleteUserFromWorkspace({
userId,
workspaceId,
}: {
userId: string;
workspaceId: string;
}) {
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
const workspaceMembers = await workspaceMemberRepository.find();
if (workspaceMembers.length > 1) {
const userWorkspace =
await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({
userId,
workspaceId,
});
await this.userRoleService.validateUserWorkspaceIsNotUniqueAdminOrThrow({
workspaceId,
userWorkspaceId: userWorkspace.id,
});
}
const workspaceMember = workspaceMembers.filter(
(member: WorkspaceMemberWorkspaceEntity) => member.userId === userId,
)?.[0];
assert(workspaceMember, 'WorkspaceMember not found');
await workspaceMemberRepository.delete({ userId });
const objectMetadata = await this.objectMetadataRepository.findOneOrFail({
where: {
nameSingular: 'workspaceMember',
workspaceId,
},
});
if (workspaceMembers.length === 1) {
await this.workspaceService.deleteWorkspace(workspaceId);
return;
}
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'workspaceMember',
action: DatabaseEventAction.DELETED,
events: [
{
recordId: workspaceMember.id,
objectMetadata,
properties: {
before: workspaceMember,
},
},
],
workspaceId,
});
}
async deleteUser(userId: string): Promise<User> {
const user = await this.userRepository.findOne({
where: {
@ -167,29 +102,97 @@ export class UserService extends TypeOrmQueryService<User> {
userValidator.assertIsDefinedOrThrow(user);
await Promise.all(
const prepareForUserDeletionInWorkspaces = await Promise.all(
user.workspaces.map(async (userWorkspace) => {
try {
await this.deleteUserFromWorkspace({
userId,
workspaceId: userWorkspace.workspaceId,
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
if (
error instanceof PermissionsException &&
error.code === PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN
) {
throw new PermissionsException(
PermissionsExceptionMessage.CANNOT_DELETE_LAST_ADMIN_USER,
PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER,
const { workspaceId } = userWorkspace;
const workspaceMemberRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
workspaceId,
'workspaceMember',
);
const workspaceMembers = await workspaceMemberRepository.find();
if (workspaceMembers.length > 1) {
try {
await this.userRoleService.validateUserWorkspaceIsNotUniqueAdminOrThrow(
{
workspaceId,
userWorkspaceId: userWorkspace.id,
},
);
} catch (error) {
if (
error instanceof PermissionsException &&
error.code === PermissionsExceptionCode.CANNOT_UNASSIGN_LAST_ADMIN
) {
throw new PermissionsException(
PermissionsExceptionMessage.CANNOT_DELETE_LAST_ADMIN_USER,
PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER,
);
}
throw error;
}
throw error;
}
const workspaceMember = workspaceMembers.find(
(member: WorkspaceMemberWorkspaceEntity) => member.userId === userId,
);
assert(workspaceMember, 'WorkspaceMember not found');
return {
workspaceId,
workspaceMemberRepository,
workspaceMembers,
workspaceMember,
};
}),
);
await Promise.all(
prepareForUserDeletionInWorkspaces.map(
async ({
workspaceId,
workspaceMemberRepository,
workspaceMembers,
workspaceMember,
}) => {
await workspaceMemberRepository.delete({ userId });
const objectMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: {
nameSingular: 'workspaceMember',
workspaceId,
},
});
if (workspaceMembers.length === 1) {
await this.workspaceService.deleteWorkspace(workspaceId);
return;
}
this.workspaceEventEmitter.emitDatabaseBatchEvent({
objectMetadataNameSingular: 'workspaceMember',
action: DatabaseEventAction.DELETED,
events: [
{
recordId: workspaceMember.id,
objectMetadata,
properties: {
before: workspaceMember,
},
},
],
workspaceId,
});
},
),
);
return user;
}

View File

@ -5,6 +5,8 @@ import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { Repository } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
@ -22,6 +24,9 @@ export class DevSeederPermissionsService {
private readonly userRoleService: UserRoleService,
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
private readonly objectPermissionService: ObjectPermissionService,
@InjectRepository(ObjectMetadataEntity, 'core')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
public async initPermissions(workspaceId: string) {
@ -30,11 +35,15 @@ export class DevSeederPermissionsService {
});
let adminUserWorkspaceId: string | undefined;
let memberUserWorkspaceId: string | undefined;
let memberUserWorkspaceIds: string[] = [];
let limitedUserWorkspaceId: string | undefined;
let guestUserWorkspaceId: string | undefined;
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM;
memberUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.JONY;
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.JANE;
limitedUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM;
memberUserWorkspaceIds = [USER_WORKSPACE_DATA_SEED_IDS.JONY];
guestUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.PHIL;
// Create guest role only in this workspace
const guestRole = await this.roleService.createGuestRole({
@ -43,11 +52,25 @@ export class DevSeederPermissionsService {
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.PHIL,
userWorkspaceId: guestUserWorkspaceId,
roleId: guestRole.id,
});
const limitedRole =
await this.createLimitedRoleForSeedWorkspace(workspaceId);
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId: limitedUserWorkspaceId,
roleId: limitedRole.id,
});
} else if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
adminUserWorkspaceId = USER_WORKSPACE_DATA_SEED_IDS.TIM_ACME;
memberUserWorkspaceIds = [
USER_WORKSPACE_DATA_SEED_IDS.JONY_ACME,
USER_WORKSPACE_DATA_SEED_IDS.JANE_ACME,
USER_WORKSPACE_DATA_SEED_IDS.PHIL_ACME,
];
}
if (adminUserWorkspaceId) {
@ -67,12 +90,73 @@ export class DevSeederPermissionsService {
activationStatus: WorkspaceActivationStatus.ACTIVE,
});
if (memberUserWorkspaceId) {
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId: memberUserWorkspaceId,
roleId: memberRole.id,
});
if (memberUserWorkspaceIds) {
for (const memberUserWorkspaceId of memberUserWorkspaceIds) {
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId: memberUserWorkspaceId,
roleId: memberRole.id,
});
}
}
}
private async createLimitedRoleForSeedWorkspace(workspaceId: string) {
const customRole = await this.roleService.createRole({
workspaceId,
input: {
label: 'Object-restricted',
description:
'All permissions except read on Rockets and update on Pets',
icon: 'custom',
canUpdateAllSettings: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
},
});
const petObjectMetadata = await this.objectMetadataRepository.findOneOrFail(
{
where: {
nameSingular: 'pet',
workspaceId,
},
},
);
const rocketObjectMetadata =
await this.objectMetadataRepository.findOneOrFail({
where: {
nameSingular: 'rocket',
workspaceId,
},
});
await this.objectPermissionService.upsertObjectPermissions({
workspaceId,
input: {
roleId: customRole.id,
objectPermissions: [
{
objectMetadataId: petObjectMetadata.id,
canReadObjectRecords: true,
canUpdateObjectRecords: false,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
{
objectMetadataId: rocketObjectMetadata.id,
canReadObjectRecords: false,
canUpdateObjectRecords: false,
canSoftDeleteObjectRecords: false,
canDestroyObjectRecords: false,
},
],
},
});
return customRole;
}
}

View File

@ -10,9 +10,11 @@ import {
const tableName = 'userWorkspace';
export const USER_WORKSPACE_DATA_SEED_IDS = {
JANE: '20202020-1e7c-43d9-a5db-685b5069d816',
TIM: '20202020-9e3b-46d4-a556-88b9ddc2b035',
JONY: '20202020-3957-4908-9c36-2929a23f8353',
PHIL: '20202020-7169-42cf-bc47-1cfef15264b1',
JANE_ACME: '20202020-ae8d-41ea-9469-f74f5d4b002e',
TIM_ACME: '20202020-e10a-4c27-a90b-b08c57b02d44',
JONY_ACME: '20202020-e10a-4c27-a90b-b08c57b02d45',
PHIL_ACME: '20202020-e10a-4c27-a90b-b08c57b02d46',
@ -33,6 +35,11 @@ export const seedUserWorkspaces = async (
userId: USER_DATA_SEED_IDS.TIM,
workspaceId,
},
{
id: USER_WORKSPACE_DATA_SEED_IDS.JANE,
userId: USER_DATA_SEED_IDS.JANE,
workspaceId,
},
{
id: USER_WORKSPACE_DATA_SEED_IDS.JONY,
userId: USER_DATA_SEED_IDS.JONY,
@ -63,6 +70,11 @@ export const seedUserWorkspaces = async (
userId: USER_DATA_SEED_IDS.PHIL,
workspaceId,
},
{
id: USER_WORKSPACE_DATA_SEED_IDS.JANE_ACME,
userId: USER_DATA_SEED_IDS.JANE,
workspaceId,
},
];
}
await dataSource

View File

@ -3,6 +3,7 @@ import { DataSource } from 'typeorm';
const tableName = 'user';
export const USER_DATA_SEED_IDS = {
JANE: '20202020-e6b5-4680-8a32-b8209737156b',
TIM: '20202020-9e3b-46d4-a556-88b9ddc2b034',
JONY: '20202020-3957-4908-9c36-2929a23f8357',
PHIL: '20202020-7169-42cf-bc47-1cfef15264b8',
@ -57,6 +58,17 @@ export const seedUsers = async (dataSource: DataSource, schemaName: string) => {
canAccessFullAdminPanel: true,
isEmailVerified: true,
},
{
id: USER_DATA_SEED_IDS.JANE,
firstName: 'Jane',
lastName: 'Austen',
email: 'jane.austen@apple.dev',
passwordHash:
'$2b$10$3LwXjJRtLsfx4hLuuXhxt.3mWgismTiZFCZSG3z9kDrSfsrBl0fT6', // tim@apple.dev
canImpersonate: true,
canAccessFullAdminPanel: true,
isEmailVerified: true,
},
])
.execute();
};

View File

@ -25,6 +25,7 @@ export const WORKSPACE_MEMBER_DATA_SEED_IDS = {
TIM: '20202020-0687-4c41-b707-ed1bfca972a7',
JONY: '20202020-77d5-4cb6-b60a-f4a835a85d61',
PHIL: '20202020-1553-45c6-a028-5a9064cce07f',
JANE: '20202020-463f-435b-828c-107e007a2711',
};
export const WORKSPACE_MEMBER_DATA_SEEDS: WorkspaceMemberDataSeed[] = [
@ -55,4 +56,13 @@ export const WORKSPACE_MEMBER_DATA_SEEDS: WorkspaceMemberDataSeed[] = [
userEmail: 'phil.schiler@apple.dev',
userId: USER_DATA_SEED_IDS.PHIL,
},
{
id: WORKSPACE_MEMBER_DATA_SEED_IDS.JANE,
nameFirstName: 'Jane',
nameLastName: 'Austen',
locale: 'en',
colorScheme: 'Light',
userEmail: 'jane.austen@apple.dev',
userId: USER_DATA_SEED_IDS.JANE,
},
];

View File

@ -6,7 +6,9 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@ -30,7 +32,8 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
UserRoleModule,
FeatureFlagModule,
WorkspaceSyncMetadataModule,
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([Workspace, ObjectMetadataEntity], 'core'),
ObjectPermissionModule,
],
exports: [DevSeederService],
providers: [