From 500ff6dd8a34ce765b62cb14eb4a0ad852f63e31 Mon Sep 17 00:00:00 2001 From: "Robert St. John" Date: Mon, 4 Nov 2024 12:14:20 -0700 Subject: [PATCH] refactor(service): users/auth: add some functionality to user ingress bindings entity and repository --- ...identity-providers.adapters.db.mongoose.ts | 78 +++++++++++++++---- service/src/ingress/ingress.app.impl.ts | 10 +-- service/src/ingress/ingress.entities.ts | 36 +++++++-- service/src/ingress/ingress.services.api.ts | 2 +- service/src/ingress/ingress.services.impl.ts | 10 +-- 5 files changed, 101 insertions(+), 35 deletions(-) diff --git a/service/src/ingress/identity-providers.adapters.db.mongoose.ts b/service/src/ingress/identity-providers.adapters.db.mongoose.ts index 1be0fae37..2eae1bcec 100644 --- a/service/src/ingress/identity-providers.adapters.db.mongoose.ts +++ b/service/src/ingress/identity-providers.adapters.db.mongoose.ts @@ -1,20 +1,13 @@ import mongoose from 'mongoose' import { BaseMongooseRepository } from '../adapters/base/adapters.base.db.mongoose' -import { MageEventId } from '../entities/events/entities.events' -import { TeamId } from '../entities/teams/entities.teams' -import { DeviceEnrollmentPolicy, IdentityProvider, IdentityProviderRepository, UserEnrollmentPolicy } from './ingress.entities' +import { PagingParameters, PageOf } from '../entities/entities.global' +import { UserId } from '../entities/users/entities.users' +import { DeviceEnrollmentPolicy, IdentityProvider, IdentityProviderId, IdentityProviderMutableAttrs, IdentityProviderRepository, UserEnrollmentPolicy, UserIngressBinding, UserIngressBindings, UserIngressBindingsRepository } from './ingress.entities' type ObjectId = mongoose.Types.ObjectId - +const ObjectId = mongoose.Types.ObjectId const Schema = mongoose.Schema -export type CommonIdpSettings = { - usersReqAdmin?: { enabled: boolean } - devicesReqAdmin?: { enabled: boolean } - newUserTeams?: TeamId[] - newUserEvents?: MageEventId[] -} - export type IdentityProviderDocument = Omit & { _id: ObjectId } @@ -127,22 +120,75 @@ export class IdentityProviderMongooseRepository extends BaseMongooseRepository { - const doc = await this.model.findOne({ name }) + const doc = await this.model.findOne({ name }, null, { lean: true }) if (doc) { return this.entityForDocument(doc) } return null } - updateIdp(update: Partial & Pick): Promise { - throw new Error('Method not implemented.') + updateIdp(update: Partial & Pick): Promise { + return super.update(update) } - deleteIdp(id: string): Promise { + deleteIdp(id: IdentityProviderId): Promise { return super.removeById(id) } } + +export type UserIngressBindingsDocument = { + /** + * The ingress bindings `_id` is actually the `_id` of the related user document. + */ + _id: ObjectId + bindings: { [idpId: IdentityProviderId]: UserIngressBinding } +} + +export type UserIngressBindingsModel = mongoose.Model + +export const UserIngressBindingsSchema = new Schema( + { + bindings: { type: Schema.Types.Mixed, required: true } + } +) + +export class UserIngressBindingsMongooseRepository implements UserIngressBindingsRepository { + + constructor(readonly model: UserIngressBindingsModel) {} + + async readBindingsForUser(userId: UserId): Promise { + const doc = await this.model.findById(userId, null, { lean: true }) + return { userId, bindings: new Map(Object.entries(doc?.bindings || {})) } + } + + async readAllBindingsForIdp(idpId: IdentityProviderId, paging?: PagingParameters | undefined): Promise> { + throw new Error('Method not implemented.') + } + + async saveUserIngressBinding(userId: UserId, binding: UserIngressBinding): Promise { + const _id = new ObjectId(userId) + const bindingsUpdate = { $set: { [`bindings.${binding.idpId}`]: binding } } + const doc = await this.model.findOneAndUpdate({ _id }, bindingsUpdate, { upsert: true, new: true }) + return { userId, bindings: new Map(Object.entries(doc.bindings)) } + } + + async deleteBinding(userId: UserId, idpId: IdentityProviderId): Promise { + const _id = new ObjectId(userId) + const bindingsUpdate = { $unset: [`bindings.${idpId}`] } + const doc = await this.model.findOneAndUpdate({ _id }, bindingsUpdate) + return doc?.bindings[idpId] || null + } + + async deleteBindingsForUser(userId: UserId): Promise { + throw new Error('Method not implemented.') + } + + async deleteAllBindingsForIdp(idpId: IdentityProviderId): Promise { + throw new Error('Method not implemented.') + } +} + // TODO: should be per protocol and identity provider and in entity layer const whitelist = ['name', 'type', 'title', 'textColor', 'buttonColor', 'icon']; const blacklist = ['clientsecret', 'bindcredentials', 'privatecert', 'decryptionpvk']; @@ -174,7 +220,7 @@ function DbAuthenticationConfigurationToObject(config, ret, options) { ret.icon = ret.icon ? ret.icon.toString('base64') : null; } -// TODO: move to api layer +// TODO: move to api/web layer function manageIcon(config) { if (config.icon) { if (config.icon.startsWith('data')) { diff --git a/service/src/ingress/ingress.app.impl.ts b/service/src/ingress/ingress.app.impl.ts index b462f67e5..ae46cd297 100644 --- a/service/src/ingress/ingress.app.impl.ts +++ b/service/src/ingress/ingress.app.impl.ts @@ -2,13 +2,13 @@ import { entityNotFound, infrastructureError } from '../app.api/app.api.errors' import { AppResponse } from '../app.api/app.api.global' import { UserRepository } from '../entities/users/entities.users' import { AdmitFromIdentityProviderOperation, AdmitFromIdentityProviderRequest, authenticationFailedError, EnrollMyselfOperation, EnrollMyselfRequest } from './ingress.app.api' -import { IdentityProviderRepository, IdentityProviderUser, UserIngressBindingRepository } from './ingress.entities' -import { ProcessNewUserEnrollment } from './ingress.services.api' +import { IdentityProviderRepository, IdentityProviderUser, UserIngressBindingsRepository } from './ingress.entities' +import { EnrollNewUser } from './ingress.services.api' import { LocalIdpCreateAccountOperation } from './local-idp.app.api' import { JWTService, TokenAssertion } from './verification' -export function CreateEnrollMyselfOperation(createLocalIdpAccount: LocalIdpCreateAccountOperation, idpRepo: IdentityProviderRepository, enrollNewUser: ProcessNewUserEnrollment): EnrollMyselfOperation { +export function CreateEnrollMyselfOperation(createLocalIdpAccount: LocalIdpCreateAccountOperation, idpRepo: IdentityProviderRepository, enrollNewUser: EnrollNewUser): EnrollMyselfOperation { return async function enrollMyself(req: EnrollMyselfRequest): ReturnType { const localAccountCreate = await createLocalIdpAccount(req) if (localAccountCreate.error) { @@ -37,7 +37,7 @@ export function CreateEnrollMyselfOperation(createLocalIdpAccount: LocalIdpCreat } } -export function CreateAdmitFromIdentityProviderOperation(idpRepo: IdentityProviderRepository, ingressBindingRepo: UserIngressBindingRepository, userRepo: UserRepository, enrollNewUser: ProcessNewUserEnrollment, tokenService: JWTService): AdmitFromIdentityProviderOperation { +export function CreateAdmitFromIdentityProviderOperation(idpRepo: IdentityProviderRepository, ingressBindingRepo: UserIngressBindingsRepository, userRepo: UserRepository, enrollNewUser: EnrollNewUser, tokenService: JWTService): AdmitFromIdentityProviderOperation { return async function admitFromIdentityProvider(req: AdmitFromIdentityProviderRequest): ReturnType { const idp = await idpRepo.findIdpByName(req.identityProviderName) if (!idp) { @@ -56,7 +56,7 @@ export function CreateAdmitFromIdentityProviderOperation(idpRepo: IdentityProvid }) .then(enrolled => { const { mageAccount, ingressBindings } = enrolled - if (ingressBindings.has(idp.id)) { + if (ingressBindings.bindingsByIdp.has(idp.id)) { return mageAccount } console.error(`user ${mageAccount.username} has no ingress binding to identity provider ${idp.name}`) diff --git a/service/src/ingress/ingress.entities.ts b/service/src/ingress/ingress.entities.ts index 9c6f27f4d..dadf3b6c7 100644 --- a/service/src/ingress/ingress.entities.ts +++ b/service/src/ingress/ingress.entities.ts @@ -4,6 +4,7 @@ import { MageEventId } from '../entities/events/entities.events' import { TeamId } from '../entities/teams/entities.teams' import { UserExpanded, UserId } from '../entities/users/entities.users' import { RoleId } from '../entities/authorization/entities.authorization' +import { PageOf, PagingParameters } from '../entities/entities.global' export interface Session { token: string @@ -88,14 +89,27 @@ export type IdentityProviderUser = Pick } +export type UserIngressBindings = { + userId: UserId + bindingsByIdp: Map +} + export type IdentityProviderMutableAttrs = Omit export interface IdentityProviderRepository { @@ -109,12 +123,22 @@ export interface IdentityProviderRepository { deleteIdp(id: IdentityProviderId): Promise } -export type UserIngressBindings = Map - -export interface UserIngressBindingRepository { +export interface UserIngressBindingsRepository { readBindingsForUser(userId: UserId): Promise + readAllBindingsForIdp(idpId: IdentityProviderId, paging?: PagingParameters): Promise> saveUserIngressBinding(userId: UserId, binding: UserIngressBinding): Promise + /** + * Return the binding that was deleted, or null if the user did not have a binding to the given IDP. + */ deleteBinding(userId: UserId, idpId: IdentityProviderId): Promise + /** + * Return the bindings that were deleted for the given user, or null if the user had no ingress bindings. + */ + deleteBindingsForUser(userId: UserId): Promise + /** + * Return the number of deleted bindings. + */ + deleteAllBindingsForIdp(idpId: IdentityProviderId): Promise } /** diff --git a/service/src/ingress/ingress.services.api.ts b/service/src/ingress/ingress.services.api.ts index 7b80f98d3..63315f1c0 100644 --- a/service/src/ingress/ingress.services.api.ts +++ b/service/src/ingress/ingress.services.api.ts @@ -1,6 +1,6 @@ import { User } from '../entities/users/entities.users' import { IdentityProvider, IdentityProviderUser, UserIngressBindings } from './ingress.entities' -export interface ProcessNewUserEnrollment { +export interface EnrollNewUser { (idpAccount: IdentityProviderUser, idp: IdentityProvider): Promise<{ mageAccount: User, ingressBindings: UserIngressBindings }> } \ No newline at end of file diff --git a/service/src/ingress/ingress.services.impl.ts b/service/src/ingress/ingress.services.impl.ts index 434cb413d..540aec846 100644 --- a/service/src/ingress/ingress.services.impl.ts +++ b/service/src/ingress/ingress.services.impl.ts @@ -1,8 +1,8 @@ import { MageEventId } from '../entities/events/entities.events' import { Team, TeamId } from '../entities/teams/entities.teams' import { User, UserId, UserRepository, UserRepositoryError } from '../entities/users/entities.users' -import { createEnrollmentCandidateUser, IdentityProvider, IdentityProviderUser, UserIngressBindingRepository, UserIngressBindings } from './ingress.entities' -import { ProcessNewUserEnrollment } from './ingress.services.api' +import { createEnrollmentCandidateUser, IdentityProvider, IdentityProviderUser, UserIngressBindingsRepository, UserIngressBindings } from './ingress.entities' +import { EnrollNewUser } from './ingress.services.api' export interface AssignTeamMember { (member: UserId, team: TeamId): Promise @@ -12,7 +12,7 @@ export interface FindEventTeam { (mageEventId: MageEventId): Promise } -export function CreateProcessNewUserEnrollmentService(userRepo: UserRepository, ingressBindingRepo: UserIngressBindingRepository, findEventTeam: FindEventTeam, assignTeamMember: AssignTeamMember): ProcessNewUserEnrollment { +export function CreateProcessNewUserEnrollmentService(userRepo: UserRepository, ingressBindingRepo: UserIngressBindingsRepository, findEventTeam: FindEventTeam, assignTeamMember: AssignTeamMember): EnrollNewUser { return async function processNewUserEnrollment(idpAccount: IdentityProviderUser, idp: IdentityProvider): Promise<{ mageAccount: User, ingressBindings: UserIngressBindings }> { console.info(`enrolling new user account ${idpAccount.username} from identity provider ${idp.name}`) const candidate = createEnrollmentCandidateUser(idpAccount, idp) @@ -23,13 +23,9 @@ export function CreateProcessNewUserEnrollmentService(userRepo: UserRepository, const ingressBindings = await ingressBindingRepo.saveUserIngressBinding( mageAccount.id, { - userId: mageAccount.id, idpId: idp.id, idpAccountId: idpAccount.username, idpAccountAttrs: {}, - // TODO: these do not have functionality yet - verified: true, - enabled: true, } ) if (ingressBindings instanceof Error) {