import { Injectable } from '@nestjs/common' ; import { EventBus } from '@nestjs/cqrs' ; import { InjectModel } from '@nestjs/mongoose' ; import { asyncFilter , validateDto } from '@fc/common' ; import { ConfigService , validationOptions } from '@fc/config' ; import { CryptographyService } from '@fc/cryptography' ; import { LoggerLevelNames , LoggerService } from '@fc/logger' ; import { IServiceProviderAdapter , ServiceProviderMetadata } from '@fc/oidc' ; import { ServiceProviderAdapterMongoConfig , ServiceProviderAdapterMongoDTO , } from './dto' ; import { Types } from './enums' ; import { ServiceProviderUpdateEvent } from './events' ; import { ServiceProvider } from './schemas' ; @ Injectable ( ) export class ServiceProviderAdapterMongoService implements IServiceProviderAdapter { private listCache : ServiceProviderMetadata [ ] ; constructor ( @ InjectModel ( 'ServiceProvider' ) private readonly serviceProviderModel , private readonly cryptography , private readonly config , private readonly eventBus , private readonly logger ) { this . logger . setContext ( this . constructor . name ) ; } async onModuleInit ( ) { this . initOperationTypeWatcher ( ) ; this . logger . debug ( 'Initializing service-provider' ) ; } private initOperationTypeWatcher ( ) { const watch = this . serviceProviderModel . watch ( ) ; this . logger . debug ( `Service's database OperationType watcher initialization.` ) ; watch . on ( 'change' , this . operationTypeWatcher . bind ( this ) ) ; } param { object } stream returns { void } private operationTypeWatcher ( stream ) { const operationTypes = [ 'insert' , 'update' , 'delete' , 'rename' , 'replace' ] ; const isOperationListened = operationTypes . includes ( stream . operationType ) ; if ( isOperationListened === true ) { this . eventBus . publish ( new ServiceProviderUpdateEvent ( ) ) ; } } private async findAllServiceProvider ( ) { const rawResult = await this . serviceProviderModel . find ( { active : true , } , { _id : false , active : true , name : true , title : true , key : true , entityId : true , client_secret : true , scopes : true , redirect_uris : true , post_logout_redirect_uris : true , id_token_signed_response_alg : true , id_token_encrypted_response_alg : true , id_token_encrypted_response_enc : true , userinfo_signed_response_alg : true , userinfo_encrypted_response_alg : true , userinfo_encrypted_response_enc : true , jwks_uri : true , idpFilterExclude : true , idpFilterList : true , type : true , identityConsent : true , } ) . lean ( ) ; const serviceProviders = await asyncFilter < ServiceProviderMetadata [ ] > ( rawResult , async ( doc ) => { const { name } = doc ; const errors = await validateDto ( doc , ServiceProviderAdapterMongoDTO , validationOptions ) ; if ( errors . length > 0 ) { this . logger . trace ( { errors } , LoggerLevelNames . WARN ) ; this . logger . warn ( `"${ name }" was excluded from the result at DTO validation :${ JSON . stringify ( errors , null , 2 ) }` ) ; } return errors . length === 0 ; } ) ; return serviceProviders ; } param refreshCache async getList ( refreshCache = false ) { if ( refreshCache || ! this . listCache ) { this . logger . debug ( 'Refresh cache from DB' ) ; const list = await this . findAllServiceProvider ( ) ; this . listCache = list . map ( this . legacyToOpenIdPropertyName . bind ( this ) ) ; this . logger . trace ( { step : 'REFRESH' , list , listCache : this . listCache } ) ; } else { this . logger . trace ( { step : 'CACHE' , listCache : this . listCache } ) ; } return this . listCache ; } async getById ( spId , refreshCache = false ) { const list = await this . getList ( refreshCache ) ; const serviceProvider = list . find ( ( { client_id : dbId } ) => dbId === spId ) ; this . logger . trace ( { spId , refreshCache , serviceProvider } ) ; return serviceProvider ; } param spId param idpId async shouldExcludeIdp ( spId , idpId ) { const { idpFilterExclude , idpFilterList } = await this . getById ( spId ) ; const idpFound = idpFilterList . includes ( idpId ) ; return idpFilterExclude ? idpFound : ! idpFound ; } private legacyToOpenIdPropertyName ( source ) { const client_id = source . key ; const client_secret = this . decryptClientSecret ( source . client_secret ) ; const scope = source . scopes . join ( ' ' ) ; const result = { ... source , client_id , client_secret , scope , } ; delete result . key ; delete result . scopes ; return result as ServiceProviderMetadata ; } private decryptClientSecret ( clientSecret ) { const { clientSecretEncryptKey } = this . config . get < ServiceProviderAdapterMongoConfig > ( 'ServiceProviderAdapterMongo' ) ; return this . cryptography . decrypt ( clientSecretEncryptKey , Buffer . from ( clientSecret , 'base64' ) ) ; } consentRequired ( type , identityConsent ) { return type === Types . PRIVATE && identityConsent ; } }