[permissions V2] Upsert object and setting permissions (#11119)
Closes https://github.com/twentyhq/core-team-issues/issues/639
This commit is contained in:
@ -36,7 +36,7 @@ import {
|
||||
import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service';
|
||||
import { BeforeUpdateOneField } from 'src/engine/metadata-modules/field-metadata/hooks/before-update-one-field.hook';
|
||||
import { fieldMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/field-metadata/utils/field-metadata-graphql-api-exception-handler.util';
|
||||
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||
|
||||
@ -74,6 +74,7 @@ export class FieldMetadataResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
|
||||
@ResolveField(() => String, { nullable: true })
|
||||
async icon(
|
||||
@Parent() fieldMetadata: FieldMetadataDTO,
|
||||
@ -86,7 +87,7 @@ export class FieldMetadataResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL))
|
||||
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
|
||||
@Mutation(() => FieldMetadataDTO)
|
||||
async createOneField(
|
||||
@Args('input') input: CreateOneFieldMetadataInput,
|
||||
@ -102,7 +103,7 @@ export class FieldMetadataResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL))
|
||||
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
|
||||
@Mutation(() => FieldMetadataDTO)
|
||||
async updateOneField(
|
||||
@Args('input') input: UpdateOneFieldMetadataInput,
|
||||
@ -123,7 +124,7 @@ export class FieldMetadataResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL))
|
||||
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
|
||||
@Mutation(() => FieldMetadataDTO)
|
||||
async deleteOneField(
|
||||
@Args('input') input: DeleteOneFieldInput,
|
||||
|
||||
@ -17,7 +17,7 @@ import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-s
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity';
|
||||
import { ObjectStandardOverridesDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-standard-overrides.dto';
|
||||
import { ObjectPermissionsEntity } from 'src/engine/metadata-modules/object-permissions/object-permissions.entity';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
|
||||
@Entity('objectMetadata')
|
||||
@ -142,9 +142,12 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface {
|
||||
updatedAt: Date;
|
||||
|
||||
@OneToMany(
|
||||
() => ObjectPermissionsEntity,
|
||||
(objectPermissions: ObjectPermissionsEntity) =>
|
||||
objectPermissions.objectMetadata,
|
||||
() => ObjectPermissionEntity,
|
||||
(objectPermission: ObjectPermissionEntity) =>
|
||||
objectPermission.objectMetadata,
|
||||
{
|
||||
cascade: true,
|
||||
},
|
||||
)
|
||||
objectPermissions: Relation<ObjectPermissionsEntity[]>;
|
||||
objectPermissions: Relation<ObjectPermissionEntity[]>;
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metad
|
||||
import { ObjectMetadataMigrationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-migration.service';
|
||||
import { ObjectMetadataRelatedRecordsService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-related-records.service';
|
||||
import { ObjectMetadataRelationService } from 'src/engine/metadata-modules/object-metadata/services/object-metadata-relation.service';
|
||||
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||
@ -78,7 +78,9 @@ import { UpdateObjectPayload } from './dtos/update-object.input';
|
||||
},
|
||||
create: {
|
||||
many: { disabled: true },
|
||||
guards: [SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)],
|
||||
guards: [
|
||||
SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL),
|
||||
],
|
||||
},
|
||||
update: { disabled: true },
|
||||
delete: { disabled: true },
|
||||
|
||||
@ -24,7 +24,7 @@ import {
|
||||
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
|
||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
||||
import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util';
|
||||
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@ -72,6 +72,7 @@ export class ObjectMetadataResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
|
||||
@ResolveField(() => String, { nullable: true })
|
||||
async icon(
|
||||
@Parent() objectMetadata: ObjectMetadataDTO,
|
||||
@ -84,7 +85,7 @@ export class ObjectMetadataResolver {
|
||||
);
|
||||
}
|
||||
|
||||
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL))
|
||||
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
|
||||
@Mutation(() => ObjectMetadataDTO)
|
||||
async deleteOneObject(
|
||||
@Args('input') input: DeleteOneObjectInput,
|
||||
@ -100,7 +101,7 @@ export class ObjectMetadataResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL))
|
||||
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
|
||||
@Mutation(() => ObjectMetadataDTO)
|
||||
async updateOneObject(
|
||||
@Args('input') input: UpdateOneObjectInput,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -13,9 +13,9 @@ import {
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
|
||||
@Entity('objectPermissions')
|
||||
@Unique('IndexOnObjectPermissionsUnique', ['objectMetadataId', 'roleId'])
|
||||
export class ObjectPermissionsEntity {
|
||||
@Entity('objectPermission')
|
||||
@Unique('IndexOnObjectPermissionUnique', ['objectMetadataId', 'roleId'])
|
||||
export class ObjectPermissionEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ -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 {}
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
export enum SettingsPermissions {
|
||||
export enum SettingPermissionType {
|
||||
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
|
||||
WORKSPACE = 'WORKSPACE',
|
||||
WORKSPACE_MEMBERS = 'WORKSPACE_MEMBERS',
|
||||
@ -25,6 +25,9 @@ export enum PermissionsExceptionCode {
|
||||
PERMISSIONS_V2_NOT_ENABLED = 'PERMISSIONS_V2_NOT_ENABLED',
|
||||
ROLE_LABEL_ALREADY_EXISTS = 'ROLE_LABEL_ALREADY_EXISTS',
|
||||
DEFAULT_ROLE_NOT_FOUND = 'DEFAULT_ROLE_NOT_FOUND',
|
||||
OBJECT_METADATA_NOT_FOUND = 'OBJECT_METADATA_NOT_FOUND',
|
||||
INVALID_SETTING = 'INVALID_SETTING',
|
||||
ROLE_NOT_EDITABLE = 'ROLE_NOT_EDITABLE',
|
||||
}
|
||||
|
||||
export enum PermissionsExceptionMessage {
|
||||
@ -45,4 +48,7 @@ export enum PermissionsExceptionMessage {
|
||||
PERMISSIONS_V2_NOT_ENABLED = 'Permissions V2 is not enabled',
|
||||
ROLE_LABEL_ALREADY_EXISTS = 'A role with this label already exists',
|
||||
DEFAULT_ROLE_NOT_FOUND = 'Default role not found',
|
||||
OBJECT_METADATA_NOT_FOUND = 'Object metadata not found',
|
||||
INVALID_SETTING = 'Invalid permission setting (unknown value)',
|
||||
ROLE_NOT_EDITABLE = 'Role is not editable',
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
@ -31,7 +31,7 @@ export class PermissionsService {
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<{
|
||||
settingsPermissions: Record<SettingsPermissions, boolean>;
|
||||
settingsPermissions: Record<SettingPermissionType, boolean>;
|
||||
objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>;
|
||||
}> {
|
||||
const [roleOfUserWorkspace] = await this.userRoleService
|
||||
@ -47,12 +47,12 @@ export class PermissionsService {
|
||||
hasPermissionOnSettingFeature = true;
|
||||
}
|
||||
|
||||
const settingsPermissionsMap = Object.keys(SettingsPermissions).reduce(
|
||||
const settingsPermissionsMap = Object.keys(SettingPermissionType).reduce(
|
||||
(acc, feature) => ({
|
||||
...acc,
|
||||
[feature]: hasPermissionOnSettingFeature,
|
||||
}),
|
||||
{} as Record<SettingsPermissions, boolean>,
|
||||
{} as Record<SettingPermissionType, boolean>,
|
||||
);
|
||||
|
||||
const objectRecordsPermissionsMap: Record<
|
||||
@ -83,7 +83,7 @@ export class PermissionsService {
|
||||
}: {
|
||||
userWorkspaceId?: string;
|
||||
workspaceId: string;
|
||||
_setting: SettingsPermissions;
|
||||
_setting: SettingPermissionType;
|
||||
isExecutedByApiKey: boolean;
|
||||
}): Promise<boolean> {
|
||||
if (isExecutedByApiKey) {
|
||||
|
||||
@ -19,11 +19,14 @@ export const permissionGraphqlApiExceptionHandler = (
|
||||
case PermissionsExceptionCode.CANNOT_DELETE_LAST_ADMIN_USER:
|
||||
case PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED:
|
||||
case PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS:
|
||||
case PermissionsExceptionCode.ROLE_NOT_EDITABLE:
|
||||
throw new ForbiddenError(error.message);
|
||||
case PermissionsExceptionCode.INVALID_ARG:
|
||||
case PermissionsExceptionCode.INVALID_SETTING:
|
||||
throw new UserInputError(error.message);
|
||||
case PermissionsExceptionCode.ROLE_NOT_FOUND:
|
||||
case PermissionsExceptionCode.USER_WORKSPACE_NOT_FOUND:
|
||||
case PermissionsExceptionCode.OBJECT_METADATA_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
case PermissionsExceptionCode.DEFAULT_ROLE_NOT_FOUND:
|
||||
default:
|
||||
|
||||
@ -13,7 +13,7 @@ import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { RelationMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/relation-metadata/interceptors/relation-metadata-graphql-api-exception.interceptor';
|
||||
@ -57,7 +57,9 @@ import { RelationMetadataDTO } from './dtos/relation-metadata.dto';
|
||||
pagingStrategy: PagingStrategies.CURSOR,
|
||||
create: {
|
||||
many: { disabled: true },
|
||||
guards: [SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL)],
|
||||
guards: [
|
||||
SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL),
|
||||
],
|
||||
},
|
||||
update: { disabled: true },
|
||||
delete: { disabled: true },
|
||||
|
||||
@ -5,7 +5,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { DeleteOneRelationInput } from 'src/engine/metadata-modules/relation-metadata/dtos/delete-relation.input';
|
||||
import { RelationMetadataDTO } from 'src/engine/metadata-modules/relation-metadata/dtos/relation-metadata.dto';
|
||||
@ -20,7 +20,7 @@ export class RelationMetadataResolver {
|
||||
private readonly relationMetadataService: RelationMetadataService,
|
||||
) {}
|
||||
|
||||
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.DATA_MODEL))
|
||||
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.DATA_MODEL))
|
||||
@Mutation(() => RelationMetadataDTO)
|
||||
async deleteOneRelation(
|
||||
@Args('input') input: DeleteOneRelationInput,
|
||||
|
||||
@ -9,9 +9,9 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { ObjectPermissionsEntity } from 'src/engine/metadata-modules/object-permissions/object-permissions.entity';
|
||||
import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permission/object-permission.entity';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { SettingsPermissionsEntity } from 'src/engine/metadata-modules/settings-permissions/settings-permissions.entity';
|
||||
import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity';
|
||||
|
||||
@Entity('role')
|
||||
@Unique('IndexOnRoleUnique', ['label', 'workspaceId'])
|
||||
@ -62,15 +62,14 @@ export class RoleEntity {
|
||||
userWorkspaceRoles: Relation<UserWorkspaceRoleEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => ObjectPermissionsEntity,
|
||||
(objectPermissions: ObjectPermissionsEntity) => objectPermissions.role,
|
||||
() => ObjectPermissionEntity,
|
||||
(objectPermission: ObjectPermissionEntity) => objectPermission.role,
|
||||
)
|
||||
objectPermissions: Relation<ObjectPermissionsEntity[]>;
|
||||
objectPermissions: Relation<ObjectPermissionEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => SettingsPermissionsEntity,
|
||||
(settingsPermissions: SettingsPermissionsEntity) =>
|
||||
settingsPermissions.role,
|
||||
() => SettingPermissionEntity,
|
||||
(settingPermission: SettingPermissionEntity) => settingPermission.role,
|
||||
)
|
||||
settingsPermissions: Relation<SettingsPermissionsEntity[]>;
|
||||
settingPermissions: Relation<SettingPermissionEntity[]>;
|
||||
}
|
||||
|
||||
@ -4,11 +4,13 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { RoleResolver } from 'src/engine/metadata-modules/role/role.resolver';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
|
||||
import { SettingPermissionModule } from 'src/engine/metadata-modules/setting-permission/setting-permission.module';
|
||||
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
|
||||
|
||||
@Module({
|
||||
@ -19,6 +21,8 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.
|
||||
PermissionsModule,
|
||||
UserWorkspaceModule,
|
||||
FeatureFlagModule,
|
||||
ObjectPermissionModule,
|
||||
SettingPermissionModule,
|
||||
],
|
||||
providers: [RoleService, RoleResolver],
|
||||
exports: [RoleService],
|
||||
|
||||
@ -16,22 +16,28 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
||||
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
|
||||
import { UpsertObjectPermissionInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-object-permission-input';
|
||||
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/createRoleInput.dto';
|
||||
import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import { UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/updateRoleInput.dto';
|
||||
import { UpdateRoleInput } from 'src/engine/metadata-modules/role/dtos/update-role-input.dto';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { SettingPermissionDTO } from 'src/engine/metadata-modules/setting-permission/dtos/setting-permission.dto';
|
||||
import { UpsertSettingPermissionInput } from 'src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input';
|
||||
import { SettingPermissionService } from 'src/engine/metadata-modules/setting-permission/setting-permission.service';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Resolver(() => RoleDTO)
|
||||
@UseGuards(SettingsPermissionsGuard(SettingsPermissions.ROLES))
|
||||
@UseGuards(SettingsPermissionsGuard(SettingPermissionType.ROLES))
|
||||
@UseFilters(PermissionsGraphqlApiExceptionFilter)
|
||||
export class RoleResolver {
|
||||
constructor(
|
||||
@ -39,6 +45,8 @@ export class RoleResolver {
|
||||
private readonly roleService: RoleService,
|
||||
private readonly userWorkspaceService: UserWorkspaceService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly objectPermissionService: ObjectPermissionService,
|
||||
private readonly settingPermissionService: SettingPermissionService,
|
||||
) {}
|
||||
|
||||
@Query(() => [RoleDTO])
|
||||
@ -101,18 +109,7 @@ export class RoleResolver {
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Args('createRoleInput') createRoleInput: CreateRoleInput,
|
||||
): Promise<RoleDTO> {
|
||||
const isPermissionsV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsV2Enabled,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!isPermissionsV2Enabled) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSIONS_V2_NOT_ENABLED,
|
||||
PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED,
|
||||
);
|
||||
}
|
||||
await this.validatePermissionsV2EnabledOrThrow(workspace);
|
||||
|
||||
return this.roleService.createRole({
|
||||
workspaceId: workspace.id,
|
||||
@ -125,18 +122,11 @@ export class RoleResolver {
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Args('updateRoleInput') updateRoleInput: UpdateRoleInput,
|
||||
): Promise<RoleDTO> {
|
||||
const isPermissionsV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsV2Enabled,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!isPermissionsV2Enabled) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSIONS_V2_NOT_ENABLED,
|
||||
PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED,
|
||||
);
|
||||
}
|
||||
await this.validatePermissionsV2EnabledOrThrow(workspace);
|
||||
await this.validateRoleIsEditableOrThrow({
|
||||
roleId: updateRoleInput.id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return this.roleService.updateRole({
|
||||
input: updateRoleInput,
|
||||
@ -144,6 +134,42 @@ export class RoleResolver {
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => ObjectPermissionDTO)
|
||||
async upsertOneObjectPermission(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Args('upsertObjectPermissionInput')
|
||||
upsertObjectPermissionInput: UpsertObjectPermissionInput,
|
||||
) {
|
||||
await this.validatePermissionsV2EnabledOrThrow(workspace);
|
||||
await this.validateRoleIsEditableOrThrow({
|
||||
roleId: upsertObjectPermissionInput.roleId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return this.objectPermissionService.upsertObjectPermission({
|
||||
workspaceId: workspace.id,
|
||||
input: upsertObjectPermissionInput,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => SettingPermissionDTO)
|
||||
async upsertOneSettingPermission(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Args('upsertSettingPermissionInput')
|
||||
upsertSettingPermissionInput: UpsertSettingPermissionInput,
|
||||
) {
|
||||
await this.validatePermissionsV2EnabledOrThrow(workspace);
|
||||
await this.validateRoleIsEditableOrThrow({
|
||||
roleId: upsertSettingPermissionInput.roleId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return this.settingPermissionService.upsertSettingPermission({
|
||||
workspaceId: workspace.id,
|
||||
input: upsertSettingPermissionInput,
|
||||
});
|
||||
}
|
||||
|
||||
@ResolveField('workspaceMembers', () => [WorkspaceMember])
|
||||
async getWorkspaceMembersAssignedToRole(
|
||||
@Parent() role: RoleDTO,
|
||||
@ -154,4 +180,36 @@ export class RoleResolver {
|
||||
workspace.id,
|
||||
);
|
||||
}
|
||||
|
||||
private async validatePermissionsV2EnabledOrThrow(workspace: Workspace) {
|
||||
const isPermissionsV2Enabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsPermissionsV2Enabled,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!isPermissionsV2Enabled) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.PERMISSIONS_V2_NOT_ENABLED,
|
||||
PermissionsExceptionCode.PERMISSIONS_V2_NOT_ENABLED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async validateRoleIsEditableOrThrow({
|
||||
roleId,
|
||||
workspaceId,
|
||||
}: {
|
||||
roleId: string;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const role = await this.roleService.getRoleById(roleId, workspaceId);
|
||||
|
||||
if (!role?.isEditable) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
|
||||
PermissionsExceptionCode.ROLE_NOT_EDITABLE,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ADMIN_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/admin-role-label.constants';
|
||||
import { MEMBER_ROLE_LABEL } from 'src/engine/metadata-modules/permissions/constants/member-role-label.constants';
|
||||
@ -10,11 +10,11 @@ import {
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/createRoleInput.dto';
|
||||
import { CreateRoleInput } from 'src/engine/metadata-modules/role/dtos/create-role-input.dto';
|
||||
import {
|
||||
UpdateRoleInput,
|
||||
UpdateRolePayload,
|
||||
} from 'src/engine/metadata-modules/role/dtos/updateRoleInput.dto';
|
||||
} from 'src/engine/metadata-modules/role/dtos/update-role-input.dto';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { isArgDefinedIfProvidedOrThrow } from 'src/engine/metadata-modules/utils/is-arg-defined-if-provided-or-throw.util';
|
||||
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
|
||||
@ObjectType('SettingPermission')
|
||||
export class SettingPermissionDTO {
|
||||
@Field({ nullable: false })
|
||||
id: string;
|
||||
|
||||
@Field({ nullable: false })
|
||||
roleId: string;
|
||||
|
||||
@Field({ nullable: false })
|
||||
setting: SettingPermissionType;
|
||||
|
||||
@Field({ nullable: true })
|
||||
canUpdateSetting?: boolean;
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsBoolean,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
|
||||
@InputType()
|
||||
export class UpsertSettingPermissionInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
roleId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field({ nullable: false })
|
||||
setting: SettingPermissionType;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
canUpdateSetting?: boolean;
|
||||
}
|
||||
@ -10,26 +10,26 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { SettingsPermissions } from 'src/engine/metadata-modules/permissions/constants/settings-permissions.constants';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
|
||||
@Entity('settingsPermissions')
|
||||
@Unique('IndexOnSettingsPermissionsUnique', ['setting', 'roleId'])
|
||||
export class SettingsPermissionsEntity {
|
||||
@Entity('settingPermission')
|
||||
@Unique('IndexOnSettingPermissionUnique', ['setting', 'roleId'])
|
||||
export class SettingPermissionEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: false, type: 'uuid' })
|
||||
roleId: string;
|
||||
|
||||
@ManyToOne(() => RoleEntity, (role) => role.settingsPermissions, {
|
||||
@ManyToOne(() => RoleEntity, (role) => role.settingPermissions, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'roleId' })
|
||||
role: Relation<RoleEntity>;
|
||||
|
||||
@Column({ nullable: false, type: 'varchar' })
|
||||
setting: SettingsPermissions;
|
||||
setting: SettingPermissionType;
|
||||
|
||||
@Column({ nullable: true, type: 'boolean' })
|
||||
canUpdateSetting?: boolean;
|
||||
@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity';
|
||||
import { SettingPermissionService } from 'src/engine/metadata-modules/setting-permission/setting-permission.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([SettingPermissionEntity, RoleEntity], 'metadata'),
|
||||
],
|
||||
providers: [SettingPermissionService],
|
||||
exports: [SettingPermissionService],
|
||||
})
|
||||
export class SettingPermissionModule {}
|
||||
@ -0,0 +1,79 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import {
|
||||
PermissionsException,
|
||||
PermissionsExceptionCode,
|
||||
PermissionsExceptionMessage,
|
||||
} from 'src/engine/metadata-modules/permissions/permissions.exception';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UpsertSettingPermissionInput } from 'src/engine/metadata-modules/setting-permission/dtos/upsert-setting-permission-input';
|
||||
import { SettingPermissionEntity } from 'src/engine/metadata-modules/setting-permission/setting-permission.entity';
|
||||
|
||||
export class SettingPermissionService {
|
||||
constructor(
|
||||
@InjectRepository(SettingPermissionEntity, 'metadata')
|
||||
private readonly settingPermissionRepository: Repository<SettingPermissionEntity>,
|
||||
@InjectRepository(RoleEntity, 'metadata')
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
) {}
|
||||
|
||||
public async upsertSettingPermission({
|
||||
workspaceId,
|
||||
input,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
input: UpsertSettingPermissionInput;
|
||||
}): Promise<SettingPermissionEntity | null | undefined> {
|
||||
if (!Object.values(SettingPermissionType).includes(input.setting)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.INVALID_SETTING,
|
||||
PermissionsExceptionCode.INVALID_SETTING,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.settingPermissionRepository.upsert(
|
||||
{
|
||||
workspaceId,
|
||||
...input,
|
||||
},
|
||||
{
|
||||
conflictPaths: ['setting', 'roleId'],
|
||||
},
|
||||
);
|
||||
|
||||
const settingPermissionId = result.generatedMaps?.[0]?.id;
|
||||
|
||||
if (!isDefined(settingPermissionId)) {
|
||||
throw new Error('Failed to upsert setting permission');
|
||||
}
|
||||
|
||||
return this.settingPermissionRepository.findOne({
|
||||
where: {
|
||||
id: settingPermissionId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.message.includes('violates foreign key constraint')) {
|
||||
const role = await this.roleRepository.findOne({
|
||||
where: {
|
||||
id: input.roleId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(role)) {
|
||||
throw new PermissionsException(
|
||||
PermissionsExceptionMessage.ROLE_NOT_FOUND,
|
||||
PermissionsExceptionCode.ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user