feat(sso): allow to use OIDC and SAML (#7246)

## What it does
### Backend
- [x] Add a mutation to create OIDC and SAML configuration
- [x] Add a mutation to delete an SSO config
- [x] Add a feature flag to toggle SSO
- [x] Add a mutation to activate/deactivate an SSO config
- [x] Add a mutation to delete an SSO config
- [x] Add strategy to use OIDC or SAML
- [ ] Improve error management

### Frontend
- [x] Add section "security" in settings
- [x] Add page to list SSO configurations
- [x] Add page and forms to create OIDC or SAML configuration
- [x] Add field to "connect with SSO" in the signin/signup process
- [x] Trigger auth when a user switch to a workspace with SSO enable
- [x] Add an option on the security page to activate/deactivate the
global invitation link
- [ ] Add new Icons for SSO Identity Providers (okta, Auth0, Azure,
Microsoft)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Antoine Moreaux
2024-10-21 20:07:08 +02:00
committed by GitHub
parent 11c3f1c399
commit 0f0a7966b1
132 changed files with 5245 additions and 306 deletions

View File

@ -0,0 +1,12 @@
/* @license Enterprise */
import { Field, InputType } from '@nestjs/graphql';
import { IsUUID } from 'class-validator';
@InputType()
export class DeleteSsoInput {
@Field(() => String)
@IsUUID()
identityProviderId: string;
}

View File

@ -0,0 +1,9 @@
/* @license Enterprise */
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class DeleteSsoOutput {
@Field(() => String)
identityProviderId: string;
}

View File

@ -0,0 +1,19 @@
/* @license Enterprise */
import { Field, InputType } from '@nestjs/graphql';
import { IsString, IsUUID } from 'class-validator';
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
import { SSOIdentityProviderStatus } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
@InputType()
export class EditSsoInput {
@Field(() => String)
@IsUUID()
id: string;
@Field(() => SSOIdentityProviderStatus)
@IsString()
status: SSOConfiguration['status'];
}

View File

@ -0,0 +1,27 @@
/* @license Enterprise */
import { Field, ObjectType } from '@nestjs/graphql';
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
import {
IdentityProviderType,
SSOIdentityProviderStatus,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
@ObjectType()
export class EditSsoOutput {
@Field(() => String)
id: string;
@Field(() => IdentityProviderType)
type: string;
@Field(() => String)
issuer: string;
@Field(() => String)
name: string;
@Field(() => SSOIdentityProviderStatus)
status: SSOConfiguration['status'];
}

View File

@ -0,0 +1,13 @@
/* @license Enterprise */
import { Field, InputType } from '@nestjs/graphql';
import { IsEmail, IsNotEmpty } from 'class-validator';
@InputType()
export class FindAvailableSSOIDPInput {
@Field(() => String)
@IsNotEmpty()
@IsEmail()
email: string;
}

View File

@ -0,0 +1,39 @@
/* @license Enterprise */
import { Field, ObjectType } from '@nestjs/graphql';
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
import {
IdentityProviderType,
SSOIdentityProviderStatus,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
@ObjectType()
class WorkspaceNameAndId {
@Field(() => String, { nullable: true })
displayName?: string | null;
@Field(() => String)
id: string;
}
@ObjectType()
export class FindAvailableSSOIDPOutput {
@Field(() => IdentityProviderType)
type: SSOConfiguration['type'];
@Field(() => String)
id: string;
@Field(() => String)
issuer: string;
@Field(() => String)
name: string;
@Field(() => SSOIdentityProviderStatus)
status: SSOConfiguration['status'];
@Field(() => WorkspaceNameAndId)
workspace: WorkspaceNameAndId;
}

View File

@ -0,0 +1,12 @@
/* @license Enterprise */
import { Field, InputType } from '@nestjs/graphql';
import { IsString } from 'class-validator';
@InputType()
export class GetAuthorizationUrlInput {
@Field(() => String)
@IsString()
identityProviderId: string;
}

View File

@ -0,0 +1,17 @@
/* @license Enterprise */
import { Field, ObjectType } from '@nestjs/graphql';
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
@ObjectType()
export class GetAuthorizationUrlOutput {
@Field(() => String)
authorizationURL: string;
@Field(() => String)
type: SSOConfiguration['type'];
@Field(() => String)
id: string;
}

View File

@ -0,0 +1,50 @@
/* @license Enterprise */
import { Field, InputType } from '@nestjs/graphql';
import { IsOptional, IsString, IsUrl, IsUUID } from 'class-validator';
import { IsX509Certificate } from 'src/engine/core-modules/sso/dtos/validators/x509.validator';
@InputType()
class SetupSsoInputCommon {
@Field(() => String)
@IsString()
name: string;
@Field(() => String)
@IsString()
@IsUrl({ protocols: ['http', 'https'] })
issuer: string;
}
@InputType()
export class SetupOIDCSsoInput extends SetupSsoInputCommon {
@Field(() => String)
@IsString()
clientID: string;
@Field(() => String)
@IsString()
clientSecret: string;
}
@InputType()
export class SetupSAMLSsoInput extends SetupSsoInputCommon {
@Field(() => String)
@IsUUID()
id: string;
@Field(() => String)
@IsUrl({ protocols: ['http', 'https'] })
ssoURL: string;
@Field(() => String)
@IsX509Certificate()
certificate: string;
@Field(() => String, { nullable: true })
@IsOptional()
@IsString()
fingerprint?: string;
}

View File

@ -0,0 +1,27 @@
/* @license Enterprise */
import { Field, ObjectType } from '@nestjs/graphql';
import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
import {
IdentityProviderType,
SSOIdentityProviderStatus,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
@ObjectType()
export class SetupSsoOutput {
@Field(() => String)
id: string;
@Field(() => IdentityProviderType)
type: string;
@Field(() => String)
issuer: string;
@Field(() => String)
name: string;
@Field(() => SSOIdentityProviderStatus)
status: SSOConfiguration['status'];
}

View File

@ -0,0 +1,52 @@
/* @license Enterprise */
import * as crypto from 'crypto';
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
} from 'class-validator';
@ValidatorConstraint({ async: false })
export class IsX509CertificateConstraint
implements ValidatorConstraintInterface
{
validate(value: any) {
if (typeof value !== 'string') {
return false;
}
try {
const cleanCert = value.replace(
/-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----|\n|\r/g,
'',
);
const der = Buffer.from(cleanCert, 'base64');
const cert = new crypto.X509Certificate(der);
return cert instanceof crypto.X509Certificate;
} catch (error) {
return false;
}
}
defaultMessage() {
return 'The string is not a valid X509 certificate';
}
}
export function IsX509Certificate(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsX509CertificateConstraint,
});
};
}

View File

@ -0,0 +1,327 @@
/* @license Enterprise */
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Issuer } from 'openid-client';
import { Repository } from 'typeorm';
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FindAvailableSSOIDPOutput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output';
import {
SSOException,
SSOExceptionCode,
} from 'src/engine/core-modules/sso/sso.exception';
import {
OIDCConfiguration,
SAMLConfiguration,
SSOConfiguration,
} from 'src/engine/core-modules/sso/types/SSOConfigurations.type';
import {
IdentityProviderType,
OIDCResponseType,
WorkspaceSSOIdentityProvider,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
export class SSOService {
constructor(
@InjectRepository(FeatureFlagEntity, 'core')
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
@InjectRepository(WorkspaceSSOIdentityProvider, 'core')
private readonly workspaceSSOIdentityProviderRepository: Repository<WorkspaceSSOIdentityProvider>,
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
private readonly environmentService: EnvironmentService,
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
private readonly cacheStorageService: CacheStorageService,
) {}
private async isSSOEnabled(workspaceId: string) {
const isSSOEnabledFeatureFlag = await this.featureFlagRepository.findOneBy({
workspaceId,
key: FeatureFlagKey.IsSSOEnabled,
value: true,
});
if (!isSSOEnabledFeatureFlag?.value) {
throw new SSOException(
`${FeatureFlagKey.IsSSOEnabled} feature flag is disabled`,
SSOExceptionCode.SSO_DISABLE,
);
}
}
async createOIDCIdentityProvider(
data: Pick<
WorkspaceSSOIdentityProvider,
'issuer' | 'clientID' | 'clientSecret' | 'name'
>,
workspaceId: string,
) {
try {
await this.isSSOEnabled(workspaceId);
if (!data.issuer) {
throw new SSOException(
'Invalid issuer URL',
SSOExceptionCode.INVALID_ISSUER_URL,
);
}
const issuer = await Issuer.discover(data.issuer);
if (!issuer.metadata.issuer) {
throw new SSOException(
'Invalid issuer URL from discovery',
SSOExceptionCode.INVALID_ISSUER_URL,
);
}
const identityProvider =
await this.workspaceSSOIdentityProviderRepository.save({
type: IdentityProviderType.OIDC,
clientID: data.clientID,
clientSecret: data.clientSecret,
issuer: issuer.metadata.issuer,
name: data.name,
workspaceId,
});
return {
id: identityProvider.id,
type: identityProvider.type,
name: identityProvider.name,
status: identityProvider.status,
issuer: identityProvider.issuer,
};
} catch (err) {
if (err instanceof SSOException) {
return err;
}
return new SSOException(
'Unknown SSO configuration error',
SSOExceptionCode.UNKNOWN_SSO_CONFIGURATION_ERROR,
);
}
}
async createSAMLIdentityProvider(
data: Pick<
WorkspaceSSOIdentityProvider,
'ssoURL' | 'certificate' | 'fingerprint' | 'id'
>,
workspaceId: string,
) {
await this.isSSOEnabled(workspaceId);
const identityProvider =
await this.workspaceSSOIdentityProviderRepository.save({
...data,
type: IdentityProviderType.SAML,
workspaceId,
});
return {
id: identityProvider.id,
type: identityProvider.type,
name: identityProvider.name,
issuer: this.buildIssuerURL(identityProvider),
status: identityProvider.status,
};
}
async findAvailableSSOIdentityProviders(email: string) {
const user = await this.userRepository.findOne({
where: { email },
relations: [
'workspaces',
'workspaces.workspace',
'workspaces.workspace.workspaceSSOIdentityProviders',
],
});
if (!user) {
throw new SSOException('User not found', SSOExceptionCode.USER_NOT_FOUND);
}
return user.workspaces.flatMap((userWorkspace) =>
(
userWorkspace.workspace
.workspaceSSOIdentityProviders as Array<SSOConfiguration>
).reduce((acc, identityProvider) => {
if (identityProvider.status === 'Inactive') return acc;
acc.push({
id: identityProvider.id,
name: identityProvider.name ?? 'Unknown',
issuer: identityProvider.issuer,
type: identityProvider.type,
status: identityProvider.status,
workspace: {
id: userWorkspace.workspaceId,
displayName: userWorkspace.workspace.displayName,
},
});
return acc;
}, [] as Array<FindAvailableSSOIDPOutput>),
);
}
async findSSOIdentityProviderById(identityProviderId?: string) {
// if identityProviderId is not provide, typeorm return a random idp instead of undefined
if (!identityProviderId) return undefined;
return (await this.workspaceSSOIdentityProviderRepository.findOne({
where: { id: identityProviderId },
})) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | undefined;
}
buildCallbackUrl(
identityProvider: Pick<WorkspaceSSOIdentityProvider, 'type'>,
) {
const callbackURL = new URL(this.environmentService.get('SERVER_URL'));
callbackURL.pathname = `/auth/${identityProvider.type.toLowerCase()}/callback`;
return callbackURL.toString();
}
buildIssuerURL(
identityProvider: Pick<WorkspaceSSOIdentityProvider, 'id' | 'type'>,
) {
return `${this.environmentService.get('SERVER_URL')}/auth/${identityProvider.type.toLowerCase()}/login/${identityProvider.id}`;
}
private isOIDCIdentityProvider(
identityProvider: WorkspaceSSOIdentityProvider,
): identityProvider is OIDCConfiguration & WorkspaceSSOIdentityProvider {
return identityProvider.type === IdentityProviderType.OIDC;
}
isSAMLIdentityProvider(
identityProvider: WorkspaceSSOIdentityProvider,
): identityProvider is SAMLConfiguration & WorkspaceSSOIdentityProvider {
return identityProvider.type === IdentityProviderType.SAML;
}
getOIDCClient(
identityProvider: WorkspaceSSOIdentityProvider,
issuer: Issuer,
) {
if (!this.isOIDCIdentityProvider(identityProvider)) {
throw new SSOException(
'Invalid Identity Provider type',
SSOExceptionCode.INVALID_IDP_TYPE,
);
}
return new issuer.Client({
client_id: identityProvider.clientID,
client_secret: identityProvider.clientSecret,
redirect_uris: [this.buildCallbackUrl(identityProvider)],
response_types: [OIDCResponseType.CODE],
});
}
async getAuthorizationUrl(identityProviderId: string) {
const identityProvider =
(await this.workspaceSSOIdentityProviderRepository.findOne({
where: {
id: identityProviderId,
},
})) as WorkspaceSSOIdentityProvider & SSOConfiguration;
if (!identityProvider) {
throw new SSOException(
'Identity Provider not found',
SSOExceptionCode.USER_NOT_FOUND,
);
}
return {
id: identityProvider.id,
authorizationURL: this.buildIssuerURL(identityProvider),
type: identityProvider.type,
};
}
async listSSOIdentityProvidersByWorkspaceId(workspaceId: string) {
return (await this.workspaceSSOIdentityProviderRepository.find({
where: { workspaceId },
select: ['id', 'name', 'type', 'issuer', 'status'],
})) as Array<
Pick<
WorkspaceSSOIdentityProvider,
'id' | 'name' | 'type' | 'issuer' | 'status'
>
>;
}
async deleteSSOIdentityProvider(
identityProviderId: string,
workspaceId: string,
) {
const identityProvider =
await this.workspaceSSOIdentityProviderRepository.findOne({
where: {
id: identityProviderId,
workspaceId,
},
});
if (!identityProvider) {
throw new SSOException(
'Identity Provider not found',
SSOExceptionCode.IDENTITY_PROVIDER_NOT_FOUND,
);
}
await this.workspaceSSOIdentityProviderRepository.delete({
id: identityProvider.id,
});
return { identityProviderId: identityProvider.id };
}
async editSSOIdentityProvider(
payload: Partial<WorkspaceSSOIdentityProvider>,
workspaceId: string,
) {
const ssoIdp = await this.workspaceSSOIdentityProviderRepository.findOne({
where: {
id: payload.id,
workspaceId,
},
});
if (!ssoIdp) {
throw new SSOException(
'Identity Provider not found',
SSOExceptionCode.IDENTITY_PROVIDER_NOT_FOUND,
);
}
const result = await this.workspaceSSOIdentityProviderRepository.save({
...ssoIdp,
...payload,
});
return {
id: result.id,
type: result.type,
issuer: result.issuer,
name: result.name,
status: result.status,
};
}
}

View File

@ -0,0 +1,20 @@
/* @license Enterprise */
import { CustomException } from 'src/utils/custom-exception';
export class SSOException extends CustomException {
code: SSOExceptionCode;
constructor(message: string, code: SSOExceptionCode) {
super(message, code);
}
}
export enum SSOExceptionCode {
USER_NOT_FOUND = 'USER_NOT_FOUND',
INVALID_SSO_CONFIGURATION = 'INVALID_SSO_CONFIGURATION',
IDENTITY_PROVIDER_NOT_FOUND = 'IDENTITY_PROVIDER_NOT_FOUND',
INVALID_ISSUER_URL = 'INVALID_ISSUER_URL',
INVALID_IDP_TYPE = 'INVALID_IDP_TYPE',
UNKNOWN_SSO_CONFIGURATION_ERROR = 'UNKNOWN_SSO_CONFIGURATION_ERROR',
SSO_DISABLE = 'SSO_DISABLE',
}

View File

@ -0,0 +1,24 @@
/* @license Enterprise */
import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { SSOResolver } from 'src/engine/core-modules/sso/sso.resolver';
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
@Module({
imports: [
NestjsQueryTypeOrmModule.forFeature(
[WorkspaceSSOIdentityProvider, User, AppToken, FeatureFlagEntity],
'core',
),
],
exports: [SSOService],
providers: [SSOService, SSOResolver],
})
export class WorkspaceSSOModule {}

View File

@ -0,0 +1,97 @@
/* @license Enterprise */
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard';
import { DeleteSsoInput } from 'src/engine/core-modules/sso/dtos/delete-sso.input';
import { DeleteSsoOutput } from 'src/engine/core-modules/sso/dtos/delete-sso.output';
import { EditSsoInput } from 'src/engine/core-modules/sso/dtos/edit-sso.input';
import { EditSsoOutput } from 'src/engine/core-modules/sso/dtos/edit-sso.output';
import { FindAvailableSSOIDPInput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.input';
import { FindAvailableSSOIDPOutput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output';
import { GetAuthorizationUrlInput } from 'src/engine/core-modules/sso/dtos/get-authorization-url.input';
import { GetAuthorizationUrlOutput } from 'src/engine/core-modules/sso/dtos/get-authorization-url.output';
import {
SetupOIDCSsoInput,
SetupSAMLSsoInput,
} from 'src/engine/core-modules/sso/dtos/setup-sso.input';
import { SetupSsoOutput } from 'src/engine/core-modules/sso/dtos/setup-sso.output';
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
import { SSOException } from 'src/engine/core-modules/sso/sso.exception';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Resolver()
export class SSOResolver {
constructor(private readonly sSOService: SSOService) {}
@UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard)
@Mutation(() => SetupSsoOutput)
async createOIDCIdentityProvider(
@Args('input') setupSsoInput: SetupOIDCSsoInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<SetupSsoOutput | SSOException> {
return this.sSOService.createOIDCIdentityProvider(
setupSsoInput,
workspaceId,
);
}
@UseGuards(SSOProviderEnabledGuard)
@Mutation(() => [FindAvailableSSOIDPOutput])
async findAvailableSSOIdentityProviders(
@Args('input') input: FindAvailableSSOIDPInput,
): Promise<Array<FindAvailableSSOIDPOutput>> {
return this.sSOService.findAvailableSSOIdentityProviders(input.email);
}
@UseGuards(SSOProviderEnabledGuard)
@Query(() => [FindAvailableSSOIDPOutput])
async listSSOIdentityProvidersByWorkspaceId(
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.sSOService.listSSOIdentityProvidersByWorkspaceId(workspaceId);
}
@Mutation(() => GetAuthorizationUrlOutput)
async getAuthorizationUrl(
@Args('input') { identityProviderId }: GetAuthorizationUrlInput,
) {
return this.sSOService.getAuthorizationUrl(identityProviderId);
}
@UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard)
@Mutation(() => SetupSsoOutput)
async createSAMLIdentityProvider(
@Args('input') setupSsoInput: SetupSAMLSsoInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<SetupSsoOutput | SSOException> {
return this.sSOService.createSAMLIdentityProvider(
setupSsoInput,
workspaceId,
);
}
@UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard)
@Mutation(() => DeleteSsoOutput)
async deleteSSOIdentityProvider(
@Args('input') { identityProviderId }: DeleteSsoInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.sSOService.deleteSSOIdentityProvider(
identityProviderId,
workspaceId,
);
}
@UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard)
@Mutation(() => EditSsoOutput)
async editSSOIdentityProvider(
@Args('input') input: EditSsoInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.sSOService.editSSOIdentityProvider(input, workspaceId);
}
}

View File

@ -0,0 +1,28 @@
/* @license Enterprise */
import {
IdentityProviderType,
SSOIdentityProviderStatus,
} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
type CommonSSOConfiguration = {
id: string;
issuer: string;
name?: string;
status: SSOIdentityProviderStatus;
};
export type OIDCConfiguration = {
type: IdentityProviderType.OIDC;
clientID: string;
clientSecret: string;
} & CommonSSOConfiguration;
export type SAMLConfiguration = {
type: IdentityProviderType.SAML;
ssoURL: string;
certificate: string;
fingerprint?: string;
} & CommonSSOConfiguration;
export type SSOConfiguration = OIDCConfiguration | SAMLConfiguration;

View File

@ -0,0 +1,110 @@
/* @license Enterprise */
import { ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
export enum IdentityProviderType {
OIDC = 'OIDC',
SAML = 'SAML',
}
export enum OIDCResponseType {
// Only Authorization Code is used for now
CODE = 'code',
ID_TOKEN = 'id_token',
TOKEN = 'token',
NONE = 'none',
}
registerEnumType(IdentityProviderType, {
name: 'IdpType',
});
export enum SSOIdentityProviderStatus {
Active = 'Active',
Inactive = 'Inactive',
Error = 'Error',
}
registerEnumType(SSOIdentityProviderStatus, {
name: 'SSOIdentityProviderStatus',
});
@Entity({ name: 'workspaceSSOIdentityProvider', schema: 'core' })
@ObjectType('WorkspaceSSOIdentityProvider')
export class WorkspaceSSOIdentityProvider {
// COMMON
@IDField(() => UUIDScalarType)
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column({
type: 'enum',
enum: SSOIdentityProviderStatus,
default: SSOIdentityProviderStatus.Active,
})
status: SSOIdentityProviderStatus;
@ManyToOne(
() => Workspace,
(workspace) => workspace.workspaceSSOIdentityProviders,
{
onDelete: 'CASCADE',
},
)
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@Column()
workspaceId: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@Column({
type: 'enum',
enum: IdentityProviderType,
default: IdentityProviderType.OIDC,
})
type: IdentityProviderType;
@Column()
issuer: string;
// OIDC
@Column({ nullable: true })
clientID?: string;
@Column({ nullable: true })
clientSecret?: string;
// SAML
@Column({ nullable: true })
ssoURL?: string;
@Column({ nullable: true })
certificate?: string;
@Column({ nullable: true })
fingerprint?: string;
}