[permissions] Add settingsPermissions to getCurrentUser (#10054)

For a member with admin role:
<img width="392" alt="Capture d’écran 2025-02-06 à 15 04 07"
src="https://github.com/user-attachments/assets/f47e8611-9577-4d0b-889c-0846acfb0d75"
/>

For a member without admin role:
<img width="394" alt="Capture d’écran 2025-02-06 à 15 04 51"
src="https://github.com/user-attachments/assets/5daacd7a-aa4f-4e06-a886-661bf0830418"
/>

For a member of a workspace that does not have the feature flag enabled:
<img width="390" alt="Capture d’écran 2025-02-06 à 15 05 22"
src="https://github.com/user-attachments/assets/05071bd6-fd2d-4823-b121-8fd11313b833"
/>
This commit is contained in:
Marie
2025-02-07 15:33:17 +01:00
committed by GitHub
parent 08b8a0cc60
commit 859e7c94f9
8 changed files with 83 additions and 7 deletions

View File

@ -1551,6 +1551,16 @@ export enum ServerlessFunctionSyncStatus {
READY = 'READY'
}
export enum SettingsFeatures {
ADMIN_PANEL = 'ADMIN_PANEL',
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
DATA_MODEL = 'DATA_MODEL',
ROLES = 'ROLES',
SECURITY_SETTINGS = 'SECURITY_SETTINGS',
WORKSPACE_SETTINGS = 'WORKSPACE_SETTINGS',
WORKSPACE_USERS = 'WORKSPACE_USERS'
}
export type SetupOidcSsoInput = {
clientID: Scalars['String'];
clientSecret: Scalars['String'];
@ -1773,7 +1783,8 @@ export type User = {
analyticsTinybirdJwts?: Maybe<AnalyticsTinybirdJwtMap>;
canImpersonate: Scalars['Boolean'];
createdAt: Scalars['DateTime'];
currentWorkspace: Workspace;
currentUserWorkspace?: Maybe<UserWorkspace>;
currentWorkspace?: Maybe<Workspace>;
defaultAvatarUrl?: Maybe<Scalars['String']>;
deletedAt?: Maybe<Scalars['DateTime']>;
disabled?: Maybe<Scalars['Boolean']>;
@ -1838,6 +1849,7 @@ export type UserWorkspace = {
createdAt: Scalars['DateTime'];
deletedAt?: Maybe<Scalars['DateTime']>;
id: Scalars['UUID'];
settingsPermissions?: Maybe<Array<SettingsFeatures>>;
updatedAt: Scalars['DateTime'];
user: User;
userId: Scalars['String'];
@ -2289,7 +2301,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, 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, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | 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, hostname?: 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 }> }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, hostname?: 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, 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, 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, hostname?: 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, hostname?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@ -2306,7 +2318,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, 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, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | 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, hostname?: 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 }> }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, subdomain: string, hostname?: 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, 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, 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, hostname?: 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, hostname?: string | null, workspaceUrls: { __typename?: 'workspaceUrls', subdomainUrl: string, customUrl?: string | null } } | null }> } };
export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String'];
@ -2567,6 +2579,9 @@ export const UserQueryFragmentFragmentDoc = gql`
workspaceMembers {
...WorkspaceMemberQueryFragment
}
currentUserWorkspace {
settingsPermissions
}
currentWorkspace {
id
displayName

View File

@ -145,6 +145,9 @@ export const queries = {
workspaceMembers {
...WorkspaceMemberQueryFragment
}
currentUserWorkspace {
settingsPermissions
}
currentWorkspace {
id
displayName
@ -304,6 +307,9 @@ export const responseData = {
timeFormat: '24',
},
workspaceMembers: [],
currentUserWorkspace: {
settingsPermissions: ['DATA_MODEL']
},
currentWorkspace: {
id: 'test-workspace-id',
displayName: 'Test Workspace',

View File

@ -24,6 +24,9 @@ export const USER_QUERY_FRAGMENT = gql`
workspaceMembers {
...WorkspaceMemberQueryFragment
}
currentUserWorkspace {
settingsPermissions
}
currentWorkspace {
id
displayName

View File

@ -1,6 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { SettingsFeatures } from 'twenty-shared';
import {
Column,
CreateDateColumn,
@ -19,6 +20,10 @@ import { TwoFactorMethod } from 'src/engine/core-modules/two-factor-method/two-f
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
registerEnumType(SettingsFeatures, {
name: 'SettingsFeatures',
});
@Entity({ name: 'userWorkspace', schema: 'core' })
@ObjectType()
@Unique('IndexOnUserIdAndWorkspaceIdUnique', ['userId', 'workspaceId'])
@ -66,4 +71,7 @@ export class UserWorkspace {
(twoFactorMethod) => twoFactorMethod.userWorkspace,
)
twoFactorMethods: Relation<TwoFactorMethod[]>;
@Field(() => [SettingsFeatures], { nullable: true })
settingsPermissions?: SettingsFeatures[];
}

View File

@ -19,6 +19,7 @@ import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-p
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
registerEnumType(OnboardingStatus, {
name: 'OnboardingStatus',
@ -99,4 +100,10 @@ export class User {
@Field(() => OnboardingStatus, { nullable: true })
onboardingStatus: OnboardingStatus;
@Field(() => Workspace, { nullable: true })
currentWorkspace: Relation<Workspace>;
@Field(() => UserWorkspace, { nullable: true })
currentUserWorkspace?: Relation<UserWorkspace>;
}

View File

@ -21,6 +21,7 @@ import { UserResolver } from 'src/engine/core-modules/user/user.resolver';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
@ -48,6 +49,7 @@ import { UserService } from './services/user.service';
DomainManagerModule,
UserRoleModule,
FeatureFlagModule,
PermissionsModule,
],
exports: [UserService],
providers: [UserService, UserResolver, TypeORMService],

View File

@ -13,6 +13,7 @@ import crypto from 'crypto';
import { GraphQLJSONObject } from 'graphql-type-json';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { SettingsFeatures } from 'twenty-shared';
import { In, Repository } from 'typeorm';
import { SupportDriver } from 'src/engine/core-modules/environment/interfaces/support.interface';
@ -47,6 +48,7 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { OriginHeader } from 'src/engine/decorators/auth/origin-header.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
@ -77,11 +79,15 @@ export class UserResolver {
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
private readonly userRoleService: UserRoleService,
private readonly permissionsService: PermissionsService,
private readonly featureFlagService: FeatureFlagService,
) {}
@Query(() => User)
async currentUser(@AuthUser() { id: userId }: User): Promise<User> {
async currentUser(
@AuthUser() { id: userId }: User,
@AuthWorkspace() workspace: Workspace,
): Promise<User> {
const user = await this.userRepository.findOne({
where: {
id: userId,
@ -94,7 +100,36 @@ export class UserResolver {
new AuthException('User not found', AuthExceptionCode.USER_NOT_FOUND),
);
return user;
const permissionsEnabled = await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsPermissionsEnabled,
workspace.id,
);
if (permissionsEnabled === true) {
const currentUserWorkspace = user.workspaces.find(
(userWorkspace) => userWorkspace.workspace.id === workspace.id,
);
if (!currentUserWorkspace) {
throw new Error('Current user workspace not found');
}
const permissions =
await this.permissionsService.getUserWorkspaceSettingsPermissions({
userWorkspaceId: currentUserWorkspace.id,
});
const permittedFeatures: SettingsFeatures[] = (
Object.keys(permissions) as SettingsFeatures[]
).filter((feature) => permissions[feature] === true);
currentUserWorkspace.settingsPermissions = permittedFeatures;
user.currentUserWorkspace = currentUserWorkspace;
}
return {
...user,
currentWorkspace: workspace,
};
}
@ResolveField(() => GraphQLJSONObject)

View File

@ -2,7 +2,7 @@ export enum SettingsFeatures {
API_KEYS_AND_WEBHOOKS = 'API_KEYS_AND_WEBHOOKS',
WORKSPACE_SETTINGS = 'WORKSPACE_SETTINGS',
WORKSPACE_USERS = 'WORKSPACE_USERS',
ROLES = 'WORKSPACE_ROLES',
ROLES = 'ROLES',
DATA_MODEL = 'DATA_MODEL',
ADMIN_PANEL = 'ADMIN_PANEL',
SECURITY_SETTINGS = 'SECURITY_SETTINGS',