[CONNECTED_ACCOUNT_BREAKING_CHANGE] Unify connected account permissions (#20732)
# Introduction This PR is a followup of https://github.com/twentyhq/twenty/pull/20673 It aims to unify the authentication/permissions layer with all the connectedAccount interactions across the application ## Deprecate - findAll - findById ## Email sync An user can only sync the message of his own connected account ## Workflow email - Related https://github.com/twentyhq/private-issues/issues/478 - Only reauthorize owned account
This commit is contained in:
+8
-25
@@ -2,20 +2,19 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { AppOAuthRevokeService } from 'src/engine/core-modules/application/connection-provider/refresh/services/app-oauth-revoke.service';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import {
|
||||
ConnectedAccountException,
|
||||
ConnectedAccountExceptionCode,
|
||||
} from 'src/engine/metadata-modules/connected-account/connected-account.exception';
|
||||
import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByUniversalIdentifierOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-universal-identifier-or-throw.util';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
@@ -37,17 +36,13 @@ export class ConnectedAccountMetadataService {
|
||||
private readonly appOAuthRevokeService: AppOAuthRevokeService,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<ConnectedAccountDTO[]> {
|
||||
return this.repository.find({ where: { workspaceId } });
|
||||
}
|
||||
|
||||
async findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<ConnectedAccountDTO[]> {
|
||||
}): Promise<ConnectedAccountEntity[]> {
|
||||
return this.repository.find({
|
||||
where: { userWorkspaceId, workspaceId },
|
||||
});
|
||||
@@ -59,7 +54,7 @@ export class ConnectedAccountMetadataService {
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<ConnectedAccountDTO | null> {
|
||||
}): Promise<ConnectedAccountEntity | null> {
|
||||
return this.repository.findOne({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
@@ -77,18 +72,6 @@ export class ConnectedAccountMetadataService {
|
||||
});
|
||||
}
|
||||
|
||||
async findByIds({
|
||||
ids,
|
||||
workspaceId,
|
||||
}: {
|
||||
ids: string[];
|
||||
workspaceId: string;
|
||||
}): Promise<ConnectedAccountDTO[]> {
|
||||
return this.repository.find({
|
||||
where: { id: In(ids), workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async verifyOwnership({
|
||||
id,
|
||||
userWorkspaceId,
|
||||
@@ -141,7 +124,7 @@ export class ConnectedAccountMetadataService {
|
||||
provider: string;
|
||||
userWorkspaceId: string;
|
||||
},
|
||||
): Promise<ConnectedAccountDTO> {
|
||||
): Promise<ConnectedAccountEntity> {
|
||||
const entity = this.repository.create(data);
|
||||
|
||||
return this.repository.save(entity);
|
||||
@@ -155,7 +138,7 @@ export class ConnectedAccountMetadataService {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
data: Partial<ConnectedAccountEntity>;
|
||||
}): Promise<ConnectedAccountDTO> {
|
||||
}): Promise<ConnectedAccountEntity> {
|
||||
await this.repository.update(
|
||||
{ id, workspaceId },
|
||||
data as Record<string, unknown>,
|
||||
@@ -170,7 +153,7 @@ export class ConnectedAccountMetadataService {
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<ConnectedAccountDTO> {
|
||||
}): Promise<ConnectedAccountEntity> {
|
||||
const connectedAccount = await this.repository.findOneOrFail({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ObjectType('ConnectedAccountHandleDTO')
|
||||
export class ConnectedAccountHandleDTO {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
handle: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
provider: string;
|
||||
}
|
||||
+12
-29
@@ -1,20 +1,18 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
|
||||
import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
||||
import { ConnectedAccountPublicDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account-public.dto';
|
||||
import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
||||
import { ConnectedAccountGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor';
|
||||
import { buildPublicConnectedAccount } from 'src/engine/metadata-modules/connected-account/utils/build-public-connected-account.util';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(ConnectedAccountGraphqlApiExceptionInterceptor)
|
||||
@@ -30,30 +28,13 @@ export class ConnectedAccountResolver {
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<ConnectedAccountPublicDTO[]> {
|
||||
return this.connectedAccountMetadataService.findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
const accounts =
|
||||
await this.connectedAccountMetadataService.findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
@Query(() => ConnectedAccountPublicDTO, { nullable: true })
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async connectedAccountById(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountPublicDTO | null> {
|
||||
return this.connectedAccountMetadataService.findById({
|
||||
id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Query(() => [ConnectedAccountPublicDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
async connectedAccounts(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountPublicDTO[]> {
|
||||
return this.connectedAccountMetadataService.findAll(workspace.id);
|
||||
return accounts.map((account) => buildPublicConnectedAccount(account));
|
||||
}
|
||||
|
||||
@Mutation(() => ConnectedAccountPublicDTO)
|
||||
@@ -69,9 +50,11 @@ export class ConnectedAccountResolver {
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return this.connectedAccountMetadataService.delete({
|
||||
const deleted = await this.connectedAccountMetadataService.delete({
|
||||
id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return buildPublicConnectedAccount(deleted);
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { buildPublicConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/utils/build-public-connection-parameters.util';
|
||||
import { type ConnectedAccountPublicDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account-public.dto';
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
|
||||
export function buildPublicConnectedAccount(
|
||||
account: ConnectedAccountEntity,
|
||||
): ConnectedAccountPublicDTO;
|
||||
export function buildPublicConnectedAccount(
|
||||
account: ConnectedAccountEntity | null,
|
||||
): ConnectedAccountPublicDTO | null;
|
||||
export function buildPublicConnectedAccount(
|
||||
account: ConnectedAccountEntity | null,
|
||||
): ConnectedAccountPublicDTO | null {
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...account,
|
||||
connectionParameters: buildPublicConnectionParameters(
|
||||
account.connectionParameters,
|
||||
),
|
||||
};
|
||||
}
|
||||
+33
-33
@@ -6,34 +6,35 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { Not, Repository } from 'typeorm';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { buildPublicConnectedAccount } from 'src/engine/metadata-modules/connected-account/utils/build-public-connected-account.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
|
||||
import { ConnectedAccountPublicDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account-public.dto';
|
||||
import { CreateEmailGroupChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/create-email-group-channel.input';
|
||||
import { CreateEmailGroupChannelOutput } from 'src/engine/metadata-modules/message-channel/dtos/create-email-group-channel.output';
|
||||
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
|
||||
import { UpdateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/update-message-channel.input';
|
||||
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
|
||||
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
|
||||
import {
|
||||
MessageChannelException,
|
||||
MessageChannelExceptionCode,
|
||||
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
|
||||
import {
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelType,
|
||||
MessageFolderPendingSyncAction,
|
||||
} from 'twenty-shared/types';
|
||||
import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateEmailGroupChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/create-email-group-channel.input';
|
||||
import { CreateEmailGroupChannelOutput } from 'src/engine/metadata-modules/message-channel/dtos/create-email-group-channel.output';
|
||||
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
|
||||
import { ConnectedAccountPublicDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account-public.dto';
|
||||
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
|
||||
import { UpdateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/update-message-channel.input';
|
||||
import {
|
||||
MessageChannelException,
|
||||
MessageChannelExceptionCode,
|
||||
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
|
||||
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
|
||||
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UseInterceptors(MessageChannelGraphqlApiExceptionInterceptor)
|
||||
@@ -55,24 +56,23 @@ export class MessageChannelResolver {
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<ConnectedAccountPublicDTO | null> {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountMetadataService.findById({
|
||||
if (messageChannel.type === MessageChannelType.EMAIL_GROUP) {
|
||||
const account = await this.connectedAccountMetadataService.findById({
|
||||
id: messageChannel.connectedAccountId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (!isDefined(connectedAccount)) {
|
||||
return null;
|
||||
return buildPublicConnectedAccount(account);
|
||||
}
|
||||
|
||||
if (
|
||||
messageChannel.type !== MessageChannelType.EMAIL_GROUP &&
|
||||
connectedAccount.userWorkspaceId !== userWorkspaceId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const account =
|
||||
await this.connectedAccountMetadataService.findByIdAndUserWorkspaceId({
|
||||
id: messageChannel.connectedAccountId,
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return connectedAccount;
|
||||
return buildPublicConnectedAccount(account);
|
||||
}
|
||||
|
||||
@Query(() => [MessageChannelDTO])
|
||||
|
||||
Reference in New Issue
Block a user