[permissions] Add object records permissions to role entity (#10255)

Closes https://github.com/twentyhq/core-team-issues/issues/388

- Add object records-related permissions to role entity
- Add it to queriable `currentUserWorkspace` (used in FE)
This commit is contained in:
Marie
2025-02-17 18:32:39 +01:00
committed by GitHub
parent 5b4cb4bd2c
commit cb3bd1353a
22 changed files with 255 additions and 60 deletions

View File

@ -1,5 +1,6 @@
/* eslint-disable */
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
import { PermissionsOnAllObjectRecords } from 'twenty-shared';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -2050,6 +2051,7 @@ export type UserWorkspace = {
deletedAt?: Maybe<Scalars['DateTime']['output']>;
id: Scalars['UUID']['output'];
settingsPermissions?: Maybe<Array<SettingsFeatures>>;
objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>;
updatedAt: Scalars['DateTime']['output'];
user: User;
userId: Scalars['String']['output'];

View File

@ -1,5 +1,5 @@
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -1172,6 +1172,13 @@ export type PageInfo = {
startCursor?: Maybe<Scalars['ConnectionCursor']>;
};
export enum PermissionsOnAllObjectRecords {
DESTROY_ALL_OBJECT_RECORDS = 'DESTROY_ALL_OBJECT_RECORDS',
READ_ALL_OBJECT_RECORDS = 'READ_ALL_OBJECT_RECORDS',
SOFT_DELETE_ALL_OBJECT_RECORDS = 'SOFT_DELETE_ALL_OBJECT_RECORDS',
UPDATE_ALL_OBJECT_RECORDS = 'UPDATE_ALL_OBJECT_RECORDS'
}
export type PostgresCredentials = {
__typename?: 'PostgresCredentials';
id: Scalars['UUID'];
@ -1428,6 +1435,10 @@ export type ResendEmailVerificationTokenOutput = {
export type Role = {
__typename?: 'Role';
canDestroyAllObjectRecords: Scalars['Boolean'];
canReadAllObjectRecords: Scalars['Boolean'];
canSoftDeleteAllObjectRecords: Scalars['Boolean'];
canUpdateAllObjectRecords: Scalars['Boolean'];
canUpdateAllSettings: Scalars['Boolean'];
description?: Maybe<Scalars['String']>;
id: Scalars['String'];
@ -1827,6 +1838,7 @@ export type UserWorkspace = {
createdAt: Scalars['DateTime'];
deletedAt?: Maybe<Scalars['DateTime']>;
id: Scalars['UUID'];
objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>;
settingsPermissions?: Maybe<Array<SettingsFeatures>>;
updatedAt: Scalars['DateTime'];
user: User;
@ -2292,7 +2304,7 @@ export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key:
export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsFeatures> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsFeatures> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@ -2309,7 +2321,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsFeatures> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } };
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, analyticsTinybirdJwts?: { __typename?: 'AnalyticsTinybirdJwtMap', getWebhookAnalytics: string, getPageviewsAnalytics: string, getUsersAnalytics: string, getServerlessFunctionDuration: string, getServerlessFunctionSuccessRate: string, getServerlessFunctionErrorCount: string } | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, currentUserWorkspace?: { __typename?: 'UserWorkspace', settingsPermissions?: Array<SettingsFeatures> | null, objectRecordsPermissions?: Array<PermissionsOnAllObjectRecords> | null } | null, currentWorkspace?: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, isGoogleAuthEnabled: boolean, isMicrosoftAuthEnabled: boolean, isPasswordAuthEnabled: boolean, subdomain: string, hasValidEnterpriseKey: boolean, customDomain?: string | null, metadataVersion: number, workspaceMembersCount?: number | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null }, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: FeatureFlagKey, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null, billingSubscriptions: Array<{ __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus }> } | null, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, customDomain?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } };
export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String'];
@ -2582,6 +2594,7 @@ export const UserQueryFragmentFragmentDoc = gql`
}
currentUserWorkspace {
settingsPermissions
objectRecordsPermissions
}
currentWorkspace {
id

View File

@ -1,5 +1,5 @@
import { gql } from '@apollo/client';
import { FieldMetadataType } from '~/generated/graphql';
import { FieldMetadataType, PermissionsOnAllObjectRecords } from '~/generated/graphql';
export const FIELD_METADATA_ID = '2c43466a-fe9e-4005-8d08-c5836067aa6c';
export const FIELD_RELATION_METADATA_ID =
@ -147,6 +147,7 @@ export const queries = {
}
currentUserWorkspace {
settingsPermissions
objectRecordsPermissions
}
currentWorkspace {
id
@ -310,6 +311,12 @@ export const responseData = {
workspaceMembers: [],
currentUserWorkspace: {
settingsPermissions: ['DATA_MODEL'],
objectRecordsPermissions: [
PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS,
PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS,
PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS,
PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS,
],
},
currentWorkspace: {
id: 'test-workspace-id',

View File

@ -26,6 +26,7 @@ export const USER_QUERY_FRAGMENT = gql`
}
currentUserWorkspace {
settingsPermissions
objectRecordsPermissions
}
currentWorkspace {
id

View File

@ -2,20 +2,6 @@ import { DataSource } from 'typeorm';
const tableName = 'featureFlag';
// export const seedFeatureFlags = async (
// workspaceDataSource: DataSource,
// schemaName: string,
// workspaceId: string,
// ) => {
// await workspaceDataSource
// .createQueryBuilder()
// .insert()
// .into(`${schemaName}.${tableName}`, ['key', 'workspaceId', 'value'])
// .orIgnore()
// .values([])
// .execute();
// };
export const deleteFeatureFlags = async (
workspaceDataSource: DataSource,
schemaName: string,

View File

@ -80,6 +80,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: false,
},
{
key: FeatureFlagKey.IsPermissionsEnabled,
workspaceId: workspaceId,
value: true,
},
])
.execute();
};

View File

@ -13,7 +13,7 @@ export const DEV_SEED_USER_WORKSPACE_IDS = {
TIM: '20202020-9e3b-46d4-a556-88b9ddc2b035',
JONY: '20202020-3957-4908-9c36-2929a23f8353',
PHIL: '20202020-7169-42cf-bc47-1cfef15264b1',
TIM_ACME: '20202020-9e3b-46d4-a556-88b9ddc2b436',
TIM_ACME: '20202020-e10a-4c27-a90b-b08c57b02d44',
};
export const seedUserWorkspaces = async (

View File

@ -0,0 +1,35 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UpdateRoleTable1739795699972 implements MigrationInterface {
name = 'UpdateRoleTable1739795699972';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."role" ADD "canReadAllObjectRecords" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."role" ADD "canUpdateAllObjectRecords" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."role" ADD "canSoftDeleteAllObjectRecords" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."role" ADD "canDestroyAllObjectRecords" boolean NOT NULL DEFAULT false`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "metadata"."role" DROP COLUMN "canDestroyAllObjectRecords"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."role" DROP COLUMN "canSoftDeleteAllObjectRecords"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."role" DROP COLUMN "canUpdateAllObjectRecords"`,
);
await queryRunner.query(
`ALTER TABLE "metadata"."role" DROP COLUMN "canReadAllObjectRecords"`,
);
}
}

View File

@ -199,6 +199,7 @@ export abstract class GraphqlQueryBaseResolverService<
await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId: authContext.userWorkspaceId,
_setting: permissionRequired,
workspaceId: authContext.workspace.id,
});
if (!userHasPermission) {

View File

@ -1,7 +1,7 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { SettingsFeatures } from 'twenty-shared';
import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared';
import {
Column,
CreateDateColumn,
@ -25,6 +25,10 @@ registerEnumType(SettingsFeatures, {
name: 'SettingsFeatures',
});
registerEnumType(PermissionsOnAllObjectRecords, {
name: 'PermissionsOnAllObjectRecords',
});
@Entity({ name: 'userWorkspace', schema: 'core' })
@ObjectType()
@Unique('IndexOnUserIdAndWorkspaceIdUnique', ['userId', 'workspaceId'])
@ -75,4 +79,7 @@ export class UserWorkspace {
@Field(() => [SettingsFeatures], { nullable: true })
settingsPermissions?: SettingsFeatures[];
@Field(() => [PermissionsOnAllObjectRecords], { nullable: true })
objectRecordsPermissions?: PermissionsOnAllObjectRecords[];
}

View File

@ -13,7 +13,7 @@ import crypto from 'crypto';
import { GraphQLJSONObject } from 'graphql-type-json';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { SettingsFeatures } from 'twenty-shared';
import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared';
import { In, Repository } from 'typeorm';
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
@ -113,16 +113,23 @@ export class UserResolver {
if (!currentUserWorkspace) {
throw new Error('Current user workspace not found');
}
const permissions =
await this.permissionsService.getUserWorkspaceSettingsPermissions({
const { settingsPermissions, objectRecordsPermissions } =
await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId: currentUserWorkspace.id,
workspaceId: workspace.id,
});
const permittedFeatures: SettingsFeatures[] = (
Object.keys(permissions) as SettingsFeatures[]
).filter((feature) => permissions[feature] === true);
Object.keys(settingsPermissions) as SettingsFeatures[]
).filter((feature) => settingsPermissions[feature] === true);
const permittedObjectRecordsPermissions = (
Object.keys(objectRecordsPermissions) as PermissionsOnAllObjectRecords[]
).filter((permission) => objectRecordsPermissions[permission] === true);
currentUserWorkspace.settingsPermissions = permittedFeatures;
currentUserWorkspace.objectRecordsPermissions =
permittedObjectRecordsPermissions;
user.currentUserWorkspace = currentUserWorkspace;
}
@ -216,9 +223,12 @@ export class UserResolver {
);
rolesByUserWorkspaces =
await this.userRoleService.getRolesByUserWorkspaces(
userWorkspaces.map((userWorkspace) => userWorkspace.id),
);
await this.userRoleService.getRolesByUserWorkspaces({
userWorkspaceIds: userWorkspaces.map(
(userWorkspace) => userWorkspace.id,
),
workspaceId: workspace.id,
});
}
for (const workspaceMemberEntity of workspaceMemberEntities) {
@ -254,6 +264,11 @@ export class UserResolver {
description: roleEntity.description,
isEditable: roleEntity.isEditable,
userWorkspaceRoles: roleEntity.userWorkspaceRoles,
canReadAllObjectRecords: roleEntity.canReadAllObjectRecords,
canUpdateAllObjectRecords: roleEntity.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords:
roleEntity.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: roleEntity.canDestroyAllObjectRecords,
};
});

View File

@ -152,11 +152,13 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
await this.validateSecurityPermissions({
payload,
userWorkspaceId,
workspaceId: workspace.id,
});
await this.validateWorkspacePermissions({
payload,
userWorkspaceId,
workspaceId: workspace.id,
});
}
@ -378,9 +380,11 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private async validateSecurityPermissions({
payload,
userWorkspaceId,
workspaceId,
}: {
payload: Partial<Workspace>;
userWorkspaceId?: string;
workspaceId: string;
}) {
if (
'isGoogleAuthEnabled' in payload ||
@ -396,6 +400,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId,
_setting: SettingsFeatures.SECURITY,
workspaceId: workspaceId,
});
if (!userHasPermission) {
@ -410,9 +415,11 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private async validateWorkspacePermissions({
payload,
userWorkspaceId,
workspaceId,
}: {
payload: Partial<Workspace>;
userWorkspaceId?: string;
workspaceId: string;
}) {
if (
'displayName' in payload ||
@ -427,6 +434,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
const userHasPermission =
await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId,
workspaceId,
_setting: SettingsFeatures.WORKSPACE,
});

View File

@ -47,6 +47,7 @@ export const SettingsPermissionsGuard = (
await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId,
_setting: requiredPermission,
workspaceId,
});
if (hasPermission === true) {

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { SettingsFeatures } from 'twenty-shared';
import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
@ -12,13 +12,21 @@ export class PermissionsService {
private readonly userRoleService: UserRoleService,
) {}
public async getUserWorkspaceSettingsPermissions({
public async getUserWorkspacePermissions({
userWorkspaceId,
workspaceId,
}: {
userWorkspaceId: string;
}): Promise<Record<SettingsFeatures, boolean>> {
workspaceId: string;
}): Promise<{
settingsPermissions: Record<SettingsFeatures, boolean>;
objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>;
}> {
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces([userWorkspaceId])
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
})
.then((roles) => roles?.get(userWorkspaceId) ?? []);
let hasPermissionOnSettingFeature = false;
@ -27,24 +35,48 @@ export class PermissionsService {
hasPermissionOnSettingFeature = true;
}
return Object.keys(SettingsFeatures).reduce(
const settingsPermissionsMap = Object.keys(SettingsFeatures).reduce(
(acc, feature) => ({
...acc,
[feature]: hasPermissionOnSettingFeature,
}),
{} as Record<SettingsFeatures, boolean>,
);
const objectRecordsPermissionsMap: Record<
PermissionsOnAllObjectRecords,
boolean
> = {
[PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace?.canReadAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace?.canUpdateAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace?.canSoftDeleteAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace?.canDestroyAllObjectRecords ?? false,
};
return {
settingsPermissions: settingsPermissionsMap,
objectRecordsPermissions: objectRecordsPermissionsMap,
};
}
public async userHasWorkspaceSettingPermission({
userWorkspaceId,
workspaceId,
_setting,
}: {
userWorkspaceId: string;
workspaceId: string;
_setting: SettingsFeatures;
}): Promise<boolean> {
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces([userWorkspaceId])
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
})
.then((roles) => roles?.get(userWorkspaceId) ?? []);
if (roleOfUserWorkspace?.canUpdateAllSettings === true) {

View File

@ -13,9 +13,6 @@ export class RoleDTO {
@Field({ nullable: false })
label: string;
@Field({ nullable: false })
canUpdateAllSettings: boolean;
@Field({ nullable: true })
description: string;
@ -27,4 +24,19 @@ export class RoleDTO {
@Field(() => [WorkspaceMember], { nullable: true })
workspaceMembers?: WorkspaceMember[];
@Field({ nullable: false })
canUpdateAllSettings: boolean;
@Field({ nullable: false })
canReadAllObjectRecords: boolean;
@Field({ nullable: false })
canUpdateAllObjectRecords: boolean;
@Field({ nullable: false })
canSoftDeleteAllObjectRecords: boolean;
@Field({ nullable: false })
canDestroyAllObjectRecords: boolean;
}

View File

@ -21,6 +21,18 @@ export class RoleEntity {
@Column({ nullable: false, default: false })
canUpdateAllSettings: boolean;
@Column({ nullable: false, default: false })
canReadAllObjectRecords: boolean;
@Column({ nullable: false, default: false })
canUpdateAllObjectRecords: boolean;
@Column({ nullable: false, default: false })
canSoftDeleteAllObjectRecords: boolean;
@Column({ nullable: false, default: false })
canDestroyAllObjectRecords: boolean;
@Column({ nullable: true, type: 'text' })
description: string;

View File

@ -38,13 +38,17 @@ export class RoleResolver {
return roles.map((role) => ({
id: role.id,
label: role.label,
canUpdateAllSettings: role.canUpdateAllSettings,
description: role.description,
workspaceId: role.workspaceId,
createdAt: role.createdAt,
updatedAt: role.updatedAt,
isEditable: role.isEditable,
userWorkspaceRoles: role.userWorkspaceRoles,
canUpdateAllSettings: role.canUpdateAllSettings,
canReadAllObjectRecords: role.canReadAllObjectRecords,
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords,
}));
}
@ -81,7 +85,10 @@ export class RoleResolver {
}
const roles = await this.userRoleService
.getRolesByUserWorkspaces([userWorkspace.id])
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspace.id],
workspaceId: workspace.id,
})
.then(
(rolesByUserWorkspaces) =>
rolesByUserWorkspaces?.get(userWorkspace.id) ?? [],

View File

@ -30,6 +30,10 @@ export class RoleService {
label: ADMIN_ROLE_LABEL,
description: 'Admin role',
canUpdateAllSettings: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
isEditable: false,
workspaceId,
});
@ -44,6 +48,10 @@ export class RoleService {
label: MEMBER_ROLE_LABEL,
description: 'Member role',
canUpdateAllSettings: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
isEditable: false,
workspaceId,
});

View File

@ -60,7 +60,10 @@ export class UserRoleService {
);
}
const roles = await this.getRolesByUserWorkspaces([userWorkspace.id]);
const roles = await this.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspace.id],
workspaceId,
});
const currentRole = roles.get(userWorkspace.id)?.[0];
@ -88,8 +91,10 @@ export class UserRoleService {
workspaceId: string;
}): Promise<void> {
await this.validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow(
userWorkspaceId,
workspaceId,
{
userWorkspaceId,
workspaceId,
},
);
await this.userWorkspaceRoleRepository.delete({
@ -98,9 +103,13 @@ export class UserRoleService {
});
}
public async getRolesByUserWorkspaces(
userWorkspaceIds: string[],
): Promise<Map<string, RoleDTO[]>> {
public async getRolesByUserWorkspaces({
userWorkspaceIds,
workspaceId,
}: {
userWorkspaceIds: string[];
workspaceId: string;
}): Promise<Map<string, RoleDTO[]>> {
if (!userWorkspaceIds.length) {
return new Map();
}
@ -108,6 +117,7 @@ export class UserRoleService {
const allUserWorkspaceRoles = await this.userWorkspaceRoleRepository.find({
where: {
userWorkspaceId: In(userWorkspaceIds),
workspaceId,
},
relations: {
role: true,
@ -176,11 +186,17 @@ export class UserRoleService {
return workspaceMembers;
}
private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow(
userWorkspaceId: string,
workspaceId: string,
): Promise<void> {
const roles = await this.getRolesByUserWorkspaces([userWorkspaceId]);
private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow({
userWorkspaceId,
workspaceId,
}: {
userWorkspaceId: string;
workspaceId: string;
}): Promise<void> {
const roles = await this.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
});
const currentRoles = roles.get(userWorkspaceId);

View File

@ -4,6 +4,11 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DEV_SEED_USER_WORKSPACE_IDS } from 'src/database/typeorm-seeds/core/user-workspaces';
import {
SEED_ACME_WORKSPACE_ID,
SEED_APPLE_WORKSPACE_ID,
} from 'src/database/typeorm-seeds/core/workspaces';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
@ -42,6 +47,7 @@ export class WorkspaceManagerService {
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
private readonly roleService: RoleService,
private readonly userRoleService: UserRoleService,
private readonly featureFlagService: FeatureFlagService,
) {}
/**
@ -261,20 +267,34 @@ export class WorkspaceManagerService {
workspaceId,
});
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId: DEV_SEED_USER_WORKSPACE_IDS.TIM,
roleId: adminRole.id,
});
let adminUserWorkspaceId: string | undefined;
let memberUserWorkspaceId: string | undefined;
if (workspaceId === SEED_APPLE_WORKSPACE_ID) {
adminUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.TIM;
memberUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.JONY;
} else if (workspaceId === SEED_ACME_WORKSPACE_ID) {
adminUserWorkspaceId = DEV_SEED_USER_WORKSPACE_IDS.TIM_ACME;
}
if (adminUserWorkspaceId) {
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId: adminUserWorkspaceId,
roleId: adminRole.id,
});
}
const memberRole = await this.roleService.createMemberRole({
workspaceId,
});
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId: DEV_SEED_USER_WORKSPACE_IDS.JONY,
roleId: memberRole.id,
});
if (memberUserWorkspaceId) {
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,
userWorkspaceId: memberUserWorkspaceId,
roleId: memberRole.id,
});
}
}
}

View File

@ -0,0 +1,6 @@
export enum PermissionsOnAllObjectRecords {
READ_ALL_OBJECT_RECORDS = 'READ_ALL_OBJECT_RECORDS',
UPDATE_ALL_OBJECT_RECORDS = 'UPDATE_ALL_OBJECT_RECORDS',
SOFT_DELETE_ALL_OBJECT_RECORDS = 'SOFT_DELETE_ALL_OBJECT_RECORDS',
DESTROY_ALL_OBJECT_RECORDS = 'DESTROY_ALL_OBJECT_RECORDS',
}

View File

@ -1,4 +1,5 @@
export * from './FieldForTotalCountAggregateOperation';
export * from './PermissionsOnAllObjectRecords';
export * from './SettingsFeatures';
export * from './TwentyCompaniesBaseUrl';
export * from './TwentyIconsBaseUrl';