diff --git a/backend/src/common/data-injection.tokens.ts b/backend/src/common/data-injection.tokens.ts index f3752a4c4..e65e687a5 100644 --- a/backend/src/common/data-injection.tokens.ts +++ b/backend/src/common/data-injection.tokens.ts @@ -108,6 +108,7 @@ export enum UseCaseType { SAAS_GET_USERS_COUNT_IN_COMPANY = 'SAAS_GET_USERS_COUNT_IN_COMPANY', FREEZE_CONNECTIONS_IN_COMPANY = 'FREEZE_CONNECTIONS_IN_COMPANY', UNFREEZE_CONNECTIONS_IN_COMPANY = 'UNFREEZE_CONNECTIONS_IN_COMPANY', + SAAS_REGISTER_USER_WITH_SAML = 'SAAS_REGISTER_USER_WITH_SAML', INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP', VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP = 'VERIFY_INVITE_USER_IN_COMPANY_AND_CONNECTION_GROUP', diff --git a/backend/src/entities/user/application/data-structures/register-user-ds.ts b/backend/src/entities/user/application/data-structures/register-user-ds.ts index e0215c4c3..178b4e65a 100644 --- a/backend/src/entities/user/application/data-structures/register-user-ds.ts +++ b/backend/src/entities/user/application/data-structures/register-user-ds.ts @@ -7,4 +7,5 @@ export class RegisterUserDs { isActive: boolean; name: string; role?: UserRoleEnum; + samlNameId?: string; } diff --git a/backend/src/entities/user/enums/external-registration-provider.enum.ts b/backend/src/entities/user/enums/external-registration-provider.enum.ts index 69d5c29ae..aceb02fa2 100644 --- a/backend/src/entities/user/enums/external-registration-provider.enum.ts +++ b/backend/src/entities/user/enums/external-registration-provider.enum.ts @@ -1,4 +1,5 @@ export enum ExternalRegistrationProviderEnum { GOOGLE = 'GOOGLE', GITHUB = 'GITHUB', + SAML = 'SAML', } diff --git a/backend/src/entities/user/repository/user-custom-repository-extension.ts b/backend/src/entities/user/repository/user-custom-repository-extension.ts index 58e055081..cbf9149e8 100644 --- a/backend/src/entities/user/repository/user-custom-repository-extension.ts +++ b/backend/src/entities/user/repository/user-custom-repository-extension.ts @@ -47,6 +47,7 @@ export const userCustomRepositoryExtension: IUserRepository = { async findOneUserByEmail( email: string, externalRegistrationProvider: ExternalRegistrationProviderEnum = null, + samlNameId: string = null, ): Promise { const userQb = this.createQueryBuilder('user').where('user.email = :userEmail', { userEmail: email?.toLowerCase(), @@ -56,6 +57,9 @@ export const userCustomRepositoryExtension: IUserRepository = { externalRegistrationProvider: externalRegistrationProvider, }); } + if (samlNameId && externalRegistrationProvider === ExternalRegistrationProviderEnum.SAML) { + userQb.andWhere('user.samlNameId = :samlNameId', { samlNameId: samlNameId }); + } return userQb.getOne(); }, diff --git a/backend/src/entities/user/repository/user.repository.interface.ts b/backend/src/entities/user/repository/user.repository.interface.ts index 8b0e2a4f1..d0b06f0bd 100644 --- a/backend/src/entities/user/repository/user.repository.interface.ts +++ b/backend/src/entities/user/repository/user.repository.interface.ts @@ -13,6 +13,7 @@ export interface IUserRepository { findOneUserByEmail( email: string, externalRegistrationProvider?: ExternalRegistrationProviderEnum, + samlNameId?: string, ): Promise; findUserWithConnections(userId: string): Promise; diff --git a/backend/src/entities/user/user.entity.ts b/backend/src/entities/user/user.entity.ts index f3084c58c..bb44d1e1b 100644 --- a/backend/src/entities/user/user.entity.ts +++ b/backend/src/entities/user/user.entity.ts @@ -136,6 +136,9 @@ export class UserEntity { }) externalRegistrationProvider: ExternalRegistrationProviderEnum; + @Column({ default: null }) + samlNameId: string; + @Column({ default: true }) showTestConnections: boolean; diff --git a/backend/src/microservices/saas-microservice/data-structures/saas-saml-user-register.ds.ts b/backend/src/microservices/saas-microservice/data-structures/saas-saml-user-register.ds.ts new file mode 100644 index 000000000..9cf6e6652 --- /dev/null +++ b/backend/src/microservices/saas-microservice/data-structures/saas-saml-user-register.ds.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SaasSAMLUserRegisterDS { + @ApiProperty() + email: string; + + @ApiProperty() + name: string; + + @ApiProperty() + companyId: string; + + @ApiProperty() + samlConfigId: string; + + @ApiProperty() + samlNameId: string; + + @ApiProperty({ required: false }) + samlAttributes?: Record; +} diff --git a/backend/src/microservices/saas-microservice/saas.controller.ts b/backend/src/microservices/saas-microservice/saas.controller.ts index ca8e5e7d3..f6fe4b4f6 100644 --- a/backend/src/microservices/saas-microservice/saas.controller.ts +++ b/backend/src/microservices/saas-microservice/saas.controller.ts @@ -6,11 +6,13 @@ import { SaasUsualUserRegisterDS } from '../../entities/user/application/data-st import { FoundUserDto } from '../../entities/user/dto/found-user.dto.js'; import { ExternalRegistrationProviderEnum } from '../../entities/user/enums/external-registration-provider.enum.js'; import { UserEntity } from '../../entities/user/user.entity.js'; +import { InTransactionEnum } from '../../enums/in-transaction.enum.js'; import { SentryInterceptor } from '../../interceptors/sentry.interceptor.js'; import { SuccessResponse } from './data-structures/common-responce.ds.js'; import { RegisterCompanyWebhookDS } from './data-structures/register-company.ds.js'; import { RegisteredCompanyDS } from './data-structures/registered-company.ds.js'; import { SaasRegisterUserWithGithub } from './data-structures/saas-register-user-with-github.js'; +import { SaasSAMLUserRegisterDS } from './data-structures/saas-saml-user-register.ds.js'; import { SaasRegisterUserWithGoogleDS } from './data-structures/sass-register-user-with-google.js'; import { ICompanyRegistration, @@ -23,9 +25,9 @@ import { ISaaSGetUsersCountInCompany, ISaasGetUsersInfosByEmail, ISaasRegisterUser, + ISaasSAMLRegisterUser, ISuspendUsers, } from './use-cases/saas-use-cases.interface.js'; -import { InTransactionEnum } from '../../enums/in-transaction.enum.js'; @UseInterceptors(SentryInterceptor) @Controller('saas') @@ -48,6 +50,8 @@ export class SaasController { private readonly loginUserWithGoogleUseCase: ILoginUserWithGoogle, @Inject(UseCaseType.SAAS_LOGIN_USER_WITH_GITHUB) private readonly loginUserWithGithubUseCase: ILoginUserWithGitHub, + @Inject(UseCaseType.SAAS_REGISTER_USER_WITH_SAML) + private readonly registerUserWithSamlUseCase: ISaasSAMLRegisterUser, @Inject(UseCaseType.SAAS_SUSPEND_USERS) private readonly suspendUsersUseCase: ISuspendUsers, @Inject(UseCaseType.SAAS_GET_COMPANY_INFO_BY_USER_ID) @@ -203,4 +207,28 @@ export class SaasController { async unfreezeConnectionsInCompany(@Body('companyIds') companyIds: Array) { return await this.unfreezeConnectionsInCompanyUseCase.execute({ companyIds }); } + + @ApiOperation({ summary: 'Register user with SAML' }) + @ApiBody({ type: SaasSAMLUserRegisterDS }) + @ApiResponse({ + status: 201, + }) + @Post('user/saml/login') + async registerUserWithSaml( + @Body('email') email: string, + @Body('name') name: string, + @Body('companyId') companyId: string, + @Body('samlConfigId') samlConfigId: string, + @Body('samlNameId') samlNameId: string, + @Body('samlAttributes') samlAttributes: Record, + ): Promise { + return await this.registerUserWithSamlUseCase.execute({ + email, + name, + companyId, + samlConfigId, + samlNameId, + samlAttributes + }); + } } diff --git a/backend/src/microservices/saas-microservice/saas.module.ts b/backend/src/microservices/saas-microservice/saas.module.ts index d19b7a3ab..754b4e928 100644 --- a/backend/src/microservices/saas-microservice/saas.module.ts +++ b/backend/src/microservices/saas-microservice/saas.module.ts @@ -15,6 +15,7 @@ import { SaasUsualRegisterUseCase } from './use-cases/saas-usual-register-user.u import { SuspendUsersUseCase } from './use-cases/suspend-users.use.case.js'; import { UnFreezeConnectionsInCompanyUseCase } from './use-cases/unfreeze-connections-in-company-use.case.js'; import { SaasRegisterDemoUserAccountUseCase } from './use-cases/register-demo-user-account.use.case.js'; +import { SaaSRegisterUserWIthSamlUseCase } from './use-cases/register-user-with-saml-use.case.js'; @Module({ imports: [], @@ -71,6 +72,10 @@ import { SaasRegisterDemoUserAccountUseCase } from './use-cases/register-demo-us provide: UseCaseType.SAAS_DEMO_USER_REGISTRATION, useClass: SaasRegisterDemoUserAccountUseCase, }, + { + provide: UseCaseType.SAAS_REGISTER_USER_WITH_SAML, + useClass: SaaSRegisterUserWIthSamlUseCase, + }, ], controllers: [SaasController], exports: [], @@ -91,6 +96,7 @@ export class SaasModule { { path: 'saas/company/:companyId/users/count', method: RequestMethod.GET }, { path: 'saas/company/freeze-connections', method: RequestMethod.PUT }, { path: 'saas/company/unfreeze-connections', method: RequestMethod.PUT }, + { path: 'saas/user/saml/login', method: RequestMethod.POST }, ); } } diff --git a/backend/src/microservices/saas-microservice/use-cases/register-user-with-saml-use.case.ts b/backend/src/microservices/saas-microservice/use-cases/register-user-with-saml-use.case.ts new file mode 100644 index 000000000..bbe8b6fc3 --- /dev/null +++ b/backend/src/microservices/saas-microservice/use-cases/register-user-with-saml-use.case.ts @@ -0,0 +1,56 @@ +import { Inject, Injectable, NotFoundException } from '@nestjs/common'; +import AbstractUseCase from '../../../common/abstract-use.case.js'; +import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js'; +import { BaseType } from '../../../common/data-injection.tokens.js'; +import { RegisterUserDs } from '../../../entities/user/application/data-structures/register-user-ds.js'; +import { ExternalRegistrationProviderEnum } from '../../../entities/user/enums/external-registration-provider.enum.js'; +import { UserRoleEnum } from '../../../entities/user/enums/user-role.enum.js'; +import { UserEntity } from '../../../entities/user/user.entity.js'; +import { Messages } from '../../../exceptions/text/messages.js'; +import { SaasSAMLUserRegisterDS } from '../data-structures/saas-saml-user-register.ds.js'; + +@Injectable() +export class SaaSRegisterUserWIthSamlUseCase extends AbstractUseCase { + constructor( + @Inject(BaseType.GLOBAL_DB_CONTEXT) + protected _dbContext: IGlobalDatabaseContext, + ) { + super(); + } + + public async implementation(inputData: SaasSAMLUserRegisterDS): Promise { + const { email, name, samlNameId, companyId } = inputData; + const foundUser = await this._dbContext.userRepository.findOneUserByEmail( + email, + ExternalRegistrationProviderEnum.SAML, + samlNameId, + ); + if (foundUser) { + return foundUser; + } + + const userData: RegisterUserDs = { + email: email, + password: null, + isActive: true, + name: name ? name : null, + gclidValue: null, + }; + + const savedUser = await this._dbContext.userRepository.saveRegisteringUser( + userData, + ExternalRegistrationProviderEnum.SAML, + ); + + const foundCompanyInfo = await this._dbContext.companyInfoRepository.findOne({ where: { id: companyId } }); + if (!foundCompanyInfo) { + throw new NotFoundException(Messages.COMPANY_NOT_FOUND); + } + + savedUser.company = foundCompanyInfo; + savedUser.samlNameId = samlNameId; + savedUser.role = UserRoleEnum.USER; + + return await this._dbContext.userRepository.saveUserEntity(savedUser); + } +} diff --git a/backend/src/microservices/saas-microservice/use-cases/saas-use-cases.interface.ts b/backend/src/microservices/saas-microservice/use-cases/saas-use-cases.interface.ts index a4b801868..fbbe002df 100644 --- a/backend/src/microservices/saas-microservice/use-cases/saas-use-cases.interface.ts +++ b/backend/src/microservices/saas-microservice/use-cases/saas-use-cases.interface.ts @@ -11,6 +11,7 @@ import { GetUsersInfosByEmailDS } from '../data-structures/get-users-infos-by-em import { RegisterCompanyWebhookDS } from '../data-structures/register-company.ds.js'; import { RegisteredCompanyDS } from '../data-structures/registered-company.ds.js'; import { SaasRegisterUserWithGithub } from '../data-structures/saas-register-user-with-github.js'; +import { SaasSAMLUserRegisterDS } from '../data-structures/saas-saml-user-register.ds.js'; import { SaasRegisterUserWithGoogleDS } from '../data-structures/sass-register-user-with-google.js'; import { SuspendUsersDS } from '../data-structures/suspend-users.ds.js'; @@ -57,3 +58,7 @@ export interface ISaaSGetUsersCountInCompany { export interface IFreezeConnectionsInCompany { execute(inputData: FreezeConnectionsInCompanyDS): Promise; } + +export interface ISaasSAMLRegisterUser { + execute(userData: SaasSAMLUserRegisterDS): Promise; +} diff --git a/backend/src/migrations/1748002305012-AddSamlPropertiesToUserEntity.ts b/backend/src/migrations/1748002305012-AddSamlPropertiesToUserEntity.ts new file mode 100644 index 000000000..ab058662d --- /dev/null +++ b/backend/src/migrations/1748002305012-AddSamlPropertiesToUserEntity.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddSamlPropertiesToUserEntity1748002305012 implements MigrationInterface { + name = 'AddSamlPropertiesToUserEntity1748002305012'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" ADD "samlNameId" character varying`); + await queryRunner.query( + `ALTER TYPE "public"."user_externalregistrationprovider_enum" RENAME TO "user_externalregistrationprovider_enum_old"`, + ); + await queryRunner.query( + `CREATE TYPE "public"."user_externalregistrationprovider_enum" AS ENUM('GOOGLE', 'GITHUB', 'SAML')`, + ); + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "externalRegistrationProvider" TYPE "public"."user_externalregistrationprovider_enum" USING "externalRegistrationProvider"::"text"::"public"."user_externalregistrationprovider_enum"`, + ); + await queryRunner.query(`DROP TYPE "public"."user_externalregistrationprovider_enum_old"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "public"."user_externalregistrationprovider_enum_old" AS ENUM('GOOGLE', 'GITHUB')`, + ); + await queryRunner.query( + `ALTER TABLE "user" ALTER COLUMN "externalRegistrationProvider" TYPE "public"."user_externalregistrationprovider_enum_old" USING "externalRegistrationProvider"::"text"::"public"."user_externalregistrationprovider_enum_old"`, + ); + await queryRunner.query(`DROP TYPE "public"."user_externalregistrationprovider_enum"`); + await queryRunner.query( + `ALTER TYPE "public"."user_externalregistrationprovider_enum_old" RENAME TO "user_externalregistrationprovider_enum"`, + ); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "samlNameId"`); + } +}