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

View File

@ -1,5 +1,5 @@
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null; export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>; export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }; 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']>; 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 = { export type PostgresCredentials = {
__typename?: 'PostgresCredentials'; __typename?: 'PostgresCredentials';
id: Scalars['UUID']; id: Scalars['UUID'];
@ -1428,6 +1435,10 @@ export type ResendEmailVerificationTokenOutput = {
export type Role = { export type Role = {
__typename?: 'Role'; __typename?: 'Role';
canDestroyAllObjectRecords: Scalars['Boolean'];
canReadAllObjectRecords: Scalars['Boolean'];
canSoftDeleteAllObjectRecords: Scalars['Boolean'];
canUpdateAllObjectRecords: Scalars['Boolean'];
canUpdateAllSettings: Scalars['Boolean']; canUpdateAllSettings: Scalars['Boolean'];
description?: Maybe<Scalars['String']>; description?: Maybe<Scalars['String']>;
id: Scalars['String']; id: Scalars['String'];
@ -1827,6 +1838,7 @@ export type UserWorkspace = {
createdAt: Scalars['DateTime']; createdAt: Scalars['DateTime'];
deletedAt?: Maybe<Scalars['DateTime']>; deletedAt?: Maybe<Scalars['DateTime']>;
id: Scalars['UUID']; id: Scalars['UUID'];
objectRecordsPermissions?: Maybe<Array<PermissionsOnAllObjectRecords>>;
settingsPermissions?: Maybe<Array<SettingsFeatures>>; settingsPermissions?: Maybe<Array<SettingsFeatures>>;
updatedAt: Scalars['DateTime']; updatedAt: Scalars['DateTime'];
user: User; 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 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; }>; 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 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<{ export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String']; workflowVersionId: Scalars['String'];
@ -2582,6 +2594,7 @@ export const UserQueryFragmentFragmentDoc = gql`
} }
currentUserWorkspace { currentUserWorkspace {
settingsPermissions settingsPermissions
objectRecordsPermissions
} }
currentWorkspace { currentWorkspace {
id id

View File

@ -1,5 +1,5 @@
import { gql } from '@apollo/client'; 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_METADATA_ID = '2c43466a-fe9e-4005-8d08-c5836067aa6c';
export const FIELD_RELATION_METADATA_ID = export const FIELD_RELATION_METADATA_ID =
@ -147,6 +147,7 @@ export const queries = {
} }
currentUserWorkspace { currentUserWorkspace {
settingsPermissions settingsPermissions
objectRecordsPermissions
} }
currentWorkspace { currentWorkspace {
id id
@ -310,6 +311,12 @@ export const responseData = {
workspaceMembers: [], workspaceMembers: [],
currentUserWorkspace: { currentUserWorkspace: {
settingsPermissions: ['DATA_MODEL'], 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: { currentWorkspace: {
id: 'test-workspace-id', id: 'test-workspace-id',

View File

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

View File

@ -2,20 +2,6 @@ import { DataSource } from 'typeorm';
const tableName = 'featureFlag'; 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 ( export const deleteFeatureFlags = async (
workspaceDataSource: DataSource, workspaceDataSource: DataSource,
schemaName: string, schemaName: string,

View File

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

View File

@ -13,7 +13,7 @@ export const DEV_SEED_USER_WORKSPACE_IDS = {
TIM: '20202020-9e3b-46d4-a556-88b9ddc2b035', TIM: '20202020-9e3b-46d4-a556-88b9ddc2b035',
JONY: '20202020-3957-4908-9c36-2929a23f8353', JONY: '20202020-3957-4908-9c36-2929a23f8353',
PHIL: '20202020-7169-42cf-bc47-1cfef15264b1', PHIL: '20202020-7169-42cf-bc47-1cfef15264b1',
TIM_ACME: '20202020-9e3b-46d4-a556-88b9ddc2b436', TIM_ACME: '20202020-e10a-4c27-a90b-b08c57b02d44',
}; };
export const seedUserWorkspaces = async ( 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({ await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId: authContext.userWorkspaceId, userWorkspaceId: authContext.userWorkspaceId,
_setting: permissionRequired, _setting: permissionRequired,
workspaceId: authContext.workspace.id,
}); });
if (!userHasPermission) { if (!userHasPermission) {

View File

@ -1,7 +1,7 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql';
import { SettingsFeatures } from 'twenty-shared'; import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared';
import { import {
Column, Column,
CreateDateColumn, CreateDateColumn,
@ -25,6 +25,10 @@ registerEnumType(SettingsFeatures, {
name: 'SettingsFeatures', name: 'SettingsFeatures',
}); });
registerEnumType(PermissionsOnAllObjectRecords, {
name: 'PermissionsOnAllObjectRecords',
});
@Entity({ name: 'userWorkspace', schema: 'core' }) @Entity({ name: 'userWorkspace', schema: 'core' })
@ObjectType() @ObjectType()
@Unique('IndexOnUserIdAndWorkspaceIdUnique', ['userId', 'workspaceId']) @Unique('IndexOnUserIdAndWorkspaceIdUnique', ['userId', 'workspaceId'])
@ -75,4 +79,7 @@ export class UserWorkspace {
@Field(() => [SettingsFeatures], { nullable: true }) @Field(() => [SettingsFeatures], { nullable: true })
settingsPermissions?: SettingsFeatures[]; 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 { GraphQLJSONObject } from 'graphql-type-json';
import { FileUpload, GraphQLUpload } from 'graphql-upload'; import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { SettingsFeatures } from 'twenty-shared'; import { PermissionsOnAllObjectRecords, SettingsFeatures } from 'twenty-shared';
import { In, Repository } from 'typeorm'; import { In, Repository } from 'typeorm';
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface'; import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
@ -113,16 +113,23 @@ export class UserResolver {
if (!currentUserWorkspace) { if (!currentUserWorkspace) {
throw new Error('Current user workspace not found'); throw new Error('Current user workspace not found');
} }
const permissions = const { settingsPermissions, objectRecordsPermissions } =
await this.permissionsService.getUserWorkspaceSettingsPermissions({ await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId: currentUserWorkspace.id, userWorkspaceId: currentUserWorkspace.id,
workspaceId: workspace.id,
}); });
const permittedFeatures: SettingsFeatures[] = ( const permittedFeatures: SettingsFeatures[] = (
Object.keys(permissions) as SettingsFeatures[] Object.keys(settingsPermissions) as SettingsFeatures[]
).filter((feature) => permissions[feature] === true); ).filter((feature) => settingsPermissions[feature] === true);
const permittedObjectRecordsPermissions = (
Object.keys(objectRecordsPermissions) as PermissionsOnAllObjectRecords[]
).filter((permission) => objectRecordsPermissions[permission] === true);
currentUserWorkspace.settingsPermissions = permittedFeatures; currentUserWorkspace.settingsPermissions = permittedFeatures;
currentUserWorkspace.objectRecordsPermissions =
permittedObjectRecordsPermissions;
user.currentUserWorkspace = currentUserWorkspace; user.currentUserWorkspace = currentUserWorkspace;
} }
@ -216,9 +223,12 @@ export class UserResolver {
); );
rolesByUserWorkspaces = rolesByUserWorkspaces =
await this.userRoleService.getRolesByUserWorkspaces( await this.userRoleService.getRolesByUserWorkspaces({
userWorkspaces.map((userWorkspace) => userWorkspace.id), userWorkspaceIds: userWorkspaces.map(
); (userWorkspace) => userWorkspace.id,
),
workspaceId: workspace.id,
});
} }
for (const workspaceMemberEntity of workspaceMemberEntities) { for (const workspaceMemberEntity of workspaceMemberEntities) {
@ -254,6 +264,11 @@ export class UserResolver {
description: roleEntity.description, description: roleEntity.description,
isEditable: roleEntity.isEditable, isEditable: roleEntity.isEditable,
userWorkspaceRoles: roleEntity.userWorkspaceRoles, 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({ await this.validateSecurityPermissions({
payload, payload,
userWorkspaceId, userWorkspaceId,
workspaceId: workspace.id,
}); });
await this.validateWorkspacePermissions({ await this.validateWorkspacePermissions({
payload, payload,
userWorkspaceId, userWorkspaceId,
workspaceId: workspace.id,
}); });
} }
@ -378,9 +380,11 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private async validateSecurityPermissions({ private async validateSecurityPermissions({
payload, payload,
userWorkspaceId, userWorkspaceId,
workspaceId,
}: { }: {
payload: Partial<Workspace>; payload: Partial<Workspace>;
userWorkspaceId?: string; userWorkspaceId?: string;
workspaceId: string;
}) { }) {
if ( if (
'isGoogleAuthEnabled' in payload || 'isGoogleAuthEnabled' in payload ||
@ -396,6 +400,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
await this.permissionsService.userHasWorkspaceSettingPermission({ await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId, userWorkspaceId,
_setting: SettingsFeatures.SECURITY, _setting: SettingsFeatures.SECURITY,
workspaceId: workspaceId,
}); });
if (!userHasPermission) { if (!userHasPermission) {
@ -410,9 +415,11 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private async validateWorkspacePermissions({ private async validateWorkspacePermissions({
payload, payload,
userWorkspaceId, userWorkspaceId,
workspaceId,
}: { }: {
payload: Partial<Workspace>; payload: Partial<Workspace>;
userWorkspaceId?: string; userWorkspaceId?: string;
workspaceId: string;
}) { }) {
if ( if (
'displayName' in payload || 'displayName' in payload ||
@ -427,6 +434,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
const userHasPermission = const userHasPermission =
await this.permissionsService.userHasWorkspaceSettingPermission({ await this.permissionsService.userHasWorkspaceSettingPermission({
userWorkspaceId, userWorkspaceId,
workspaceId,
_setting: SettingsFeatures.WORKSPACE, _setting: SettingsFeatures.WORKSPACE,
}); });

View File

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

View File

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

View File

@ -13,9 +13,6 @@ export class RoleDTO {
@Field({ nullable: false }) @Field({ nullable: false })
label: string; label: string;
@Field({ nullable: false })
canUpdateAllSettings: boolean;
@Field({ nullable: true }) @Field({ nullable: true })
description: string; description: string;
@ -27,4 +24,19 @@ export class RoleDTO {
@Field(() => [WorkspaceMember], { nullable: true }) @Field(() => [WorkspaceMember], { nullable: true })
workspaceMembers?: WorkspaceMember[]; 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 }) @Column({ nullable: false, default: false })
canUpdateAllSettings: boolean; 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' }) @Column({ nullable: true, type: 'text' })
description: string; description: string;

View File

@ -38,13 +38,17 @@ export class RoleResolver {
return roles.map((role) => ({ return roles.map((role) => ({
id: role.id, id: role.id,
label: role.label, label: role.label,
canUpdateAllSettings: role.canUpdateAllSettings,
description: role.description, description: role.description,
workspaceId: role.workspaceId, workspaceId: role.workspaceId,
createdAt: role.createdAt, createdAt: role.createdAt,
updatedAt: role.updatedAt, updatedAt: role.updatedAt,
isEditable: role.isEditable, isEditable: role.isEditable,
userWorkspaceRoles: role.userWorkspaceRoles, 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 const roles = await this.userRoleService
.getRolesByUserWorkspaces([userWorkspace.id]) .getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspace.id],
workspaceId: workspace.id,
})
.then( .then(
(rolesByUserWorkspaces) => (rolesByUserWorkspaces) =>
rolesByUserWorkspaces?.get(userWorkspace.id) ?? [], rolesByUserWorkspaces?.get(userWorkspace.id) ?? [],

View File

@ -30,6 +30,10 @@ export class RoleService {
label: ADMIN_ROLE_LABEL, label: ADMIN_ROLE_LABEL,
description: 'Admin role', description: 'Admin role',
canUpdateAllSettings: true, canUpdateAllSettings: true,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
isEditable: false, isEditable: false,
workspaceId, workspaceId,
}); });
@ -44,6 +48,10 @@ export class RoleService {
label: MEMBER_ROLE_LABEL, label: MEMBER_ROLE_LABEL,
description: 'Member role', description: 'Member role',
canUpdateAllSettings: false, canUpdateAllSettings: false,
canReadAllObjectRecords: true,
canUpdateAllObjectRecords: true,
canSoftDeleteAllObjectRecords: true,
canDestroyAllObjectRecords: true,
isEditable: false, isEditable: false,
workspaceId, 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]; const currentRole = roles.get(userWorkspace.id)?.[0];
@ -88,8 +91,10 @@ export class UserRoleService {
workspaceId: string; workspaceId: string;
}): Promise<void> { }): Promise<void> {
await this.validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow( await this.validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow(
userWorkspaceId, {
workspaceId, userWorkspaceId,
workspaceId,
},
); );
await this.userWorkspaceRoleRepository.delete({ await this.userWorkspaceRoleRepository.delete({
@ -98,9 +103,13 @@ export class UserRoleService {
}); });
} }
public async getRolesByUserWorkspaces( public async getRolesByUserWorkspaces({
userWorkspaceIds: string[], userWorkspaceIds,
): Promise<Map<string, RoleDTO[]>> { workspaceId,
}: {
userWorkspaceIds: string[];
workspaceId: string;
}): Promise<Map<string, RoleDTO[]>> {
if (!userWorkspaceIds.length) { if (!userWorkspaceIds.length) {
return new Map(); return new Map();
} }
@ -108,6 +117,7 @@ export class UserRoleService {
const allUserWorkspaceRoles = await this.userWorkspaceRoleRepository.find({ const allUserWorkspaceRoles = await this.userWorkspaceRoleRepository.find({
where: { where: {
userWorkspaceId: In(userWorkspaceIds), userWorkspaceId: In(userWorkspaceIds),
workspaceId,
}, },
relations: { relations: {
role: true, role: true,
@ -176,11 +186,17 @@ export class UserRoleService {
return workspaceMembers; return workspaceMembers;
} }
private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow( private async validatesUserWorkspaceIsNotLastAdminIfUnassigningAdminRoleOrThrow({
userWorkspaceId: string, userWorkspaceId,
workspaceId: string, workspaceId,
): Promise<void> { }: {
const roles = await this.getRolesByUserWorkspaces([userWorkspaceId]); userWorkspaceId: string;
workspaceId: string;
}): Promise<void> {
const roles = await this.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
workspaceId,
});
const currentRoles = roles.get(userWorkspaceId); const currentRoles = roles.get(userWorkspaceId);

View File

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