[permissions V2] Upsert object and setting permissions (#11119)

Closes https://github.com/twentyhq/core-team-issues/issues/639
This commit is contained in:
Marie
2025-03-25 11:07:51 +01:00
committed by GitHub
parent 54e346a2aa
commit 4680bc740a
51 changed files with 985 additions and 205 deletions

View File

@ -0,0 +1,25 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType('ObjectPermission')
export class ObjectPermissionDTO {
@Field({ nullable: false })
id: string;
@Field({ nullable: false })
roleId: string;
@Field({ nullable: false })
objectMetadataId: string;
@Field({ nullable: true })
canReadObjectRecords?: boolean;
@Field({ nullable: true })
canUpdateObjectRecords?: boolean;
@Field({ nullable: true })
canSoftDeleteObjectRecords?: boolean;
@Field({ nullable: true })
canDestroyObjectRecords?: boolean;
}

View File

@ -0,0 +1,36 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsBoolean, IsNotEmpty, IsOptional, IsUUID } from 'class-validator';
@InputType()
export class UpsertObjectPermissionInput {
@IsUUID()
@IsNotEmpty()
@Field()
roleId: string;
@IsUUID()
@IsNotEmpty()
@Field()
objectMetadataId: string;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
canReadObjectRecords?: boolean;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
canUpdateObjectRecords?: boolean;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
canSoftDeleteObjectRecords?: boolean;
@IsBoolean()
@IsOptional()
@Field({ nullable: true })
canDestroyObjectRecords?: boolean;
}

View File

@ -0,0 +1,64 @@
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
Unique,
UpdateDateColumn,
} from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@Entity('objectPermission')
@Unique('IndexOnObjectPermissionUnique', ['objectMetadataId', 'roleId'])
export class ObjectPermissionEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ nullable: false, type: 'uuid' })
roleId: string;
@ManyToOne(() => RoleEntity, (role) => role.objectPermissions, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'roleId' })
role: Relation<RoleEntity>;
@Column({ nullable: false, type: 'uuid' })
objectMetadataId: string;
@ManyToOne(
() => ObjectMetadataEntity,
(objectMetadata) => objectMetadata.objectPermissions,
{
onDelete: 'CASCADE',
},
)
@JoinColumn({ name: 'objectMetadataId' })
objectMetadata: Relation<ObjectMetadataEntity>;
@Column({ nullable: true, type: 'boolean' })
canReadObjectRecords?: boolean;
@Column({ nullable: true, type: 'boolean' })
canUpdateObjectRecords?: boolean;
@Column({ nullable: true, type: 'boolean' })
canSoftDeleteObjectRecords?: boolean;
@Column({ nullable: true, type: 'boolean' })
canDestroyObjectRecords?: boolean;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}

View File

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
@Module({
imports: [
TypeOrmModule.forFeature(
[ObjectPermissionEntity, RoleEntity, ObjectMetadataEntity],
'metadata',
),
],
providers: [ObjectPermissionService],
exports: [ObjectPermissionService],
})
export class ObjectPermissionModule {}

View File

@ -0,0 +1,115 @@
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { UpsertObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input';
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
import {
PermissionsException,
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
export class ObjectPermissionService {
constructor(
@InjectRepository(ObjectPermissionEntity, 'metadata')
private readonly objectPermissionRepository: Repository<ObjectPermissionEntity>,
@InjectRepository(RoleEntity, 'metadata')
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(ObjectMetadataEntity, 'metadata')
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
) {}
public async upsertObjectPermission({
workspaceId,
input,
}: {
workspaceId: string;
input: UpsertObjectPermissionInput;
}): Promise<ObjectPermissionEntity | null> {
try {
const result = await this.objectPermissionRepository.upsert(
{
workspaceId,
...input,
},
{
conflictPaths: ['objectMetadataId', 'roleId'],
},
);
const objectPermissionId = result.generatedMaps?.[0]?.id;
if (!isDefined(objectPermissionId)) {
throw new Error('Failed to upsert object permission');
}
return this.objectPermissionRepository.findOne({
where: {
id: objectPermissionId,
},
});
} catch (error) {
await this.handleForeignKeyError({
error,
roleId: input.roleId,
workspaceId,
objectMetadataId: input.objectMetadataId,
});
throw error;
}
}
private async handleForeignKeyError({
error,
roleId,
workspaceId,
objectMetadataId,
}: {
error: Error;
roleId: string;
workspaceId: string;
objectMetadataId: string;
}) {
if (error.message.includes('violates foreign key constraint')) {
const role = await this.getRole(roleId, workspaceId);
if (!isDefined(role)) {
throw new PermissionsException(
PermissionsExceptionMessage.ROLE_NOT_FOUND,
PermissionsExceptionCode.ROLE_NOT_FOUND,
);
}
const objectMetadata = await this.objectMetadataRepository.findOne({
where: {
workspaceId,
id: objectMetadataId,
},
});
if (!isDefined(objectMetadata)) {
throw new PermissionsException(
PermissionsExceptionMessage.OBJECT_METADATA_NOT_FOUND,
PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND,
);
}
}
}
private async getRole(
roleId: string,
workspaceId: string,
): Promise<RoleEntity | null> {
return this.roleRepository.findOne({
where: {
id: roleId,
workspaceId,
},
});
}
}