Skip to content

Commit

Permalink
refactor(service): users/auth: add some functionality to user ingress…
Browse files Browse the repository at this point in the history
… bindings entity and repository
  • Loading branch information
restjohn committed Nov 4, 2024
1 parent 2eb9527 commit 500ff6d
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 35 deletions.
78 changes: 62 additions & 16 deletions service/src/ingress/identity-providers.adapters.db.mongoose.ts
Original file line number Diff line number Diff line change
@@ -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<IdentityProvider, 'id'> & {
_id: ObjectId
}
Expand Down Expand Up @@ -127,22 +120,75 @@ export class IdentityProviderMongooseRepository extends BaseMongooseRepository<I
}

async findIdpByName(name: string): Promise<IdentityProvider | null> {
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<IdentityProvider> & Pick<IdentityProvider, 'id'>): Promise<IdentityProvider | null> {
throw new Error('Method not implemented.')
updateIdp(update: Partial<IdentityProviderMutableAttrs> & Pick<IdentityProvider, 'id'>): Promise<IdentityProvider | null> {
return super.update(update)
}

deleteIdp(id: string): Promise<IdentityProvider | null> {
deleteIdp(id: IdentityProviderId): Promise<IdentityProvider | null> {
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<UserIngressBindingsDocument>

export const UserIngressBindingsSchema = new Schema<UserIngressBindingsDocument>(
{
bindings: { type: Schema.Types.Mixed, required: true }
}
)

export class UserIngressBindingsMongooseRepository implements UserIngressBindingsRepository {

constructor(readonly model: UserIngressBindingsModel) {}

async readBindingsForUser(userId: UserId): Promise<UserIngressBindings> {
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<PageOf<UserIngressBindings>> {
throw new Error('Method not implemented.')
}

async saveUserIngressBinding(userId: UserId, binding: UserIngressBinding): Promise<Error | UserIngressBindings> {
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<UserIngressBinding | null> {
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<UserIngressBindings | null> {
throw new Error('Method not implemented.')
}

async deleteAllBindingsForIdp(idpId: IdentityProviderId): Promise<number> {
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'];
Expand Down Expand Up @@ -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')) {
Expand Down
10 changes: 5 additions & 5 deletions service/src/ingress/ingress.app.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnrollMyselfOperation> {
const localAccountCreate = await createLocalIdpAccount(req)
if (localAccountCreate.error) {
Expand Down Expand Up @@ -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<AdmitFromIdentityProviderOperation> {
const idp = await idpRepo.findIdpByName(req.identityProviderName)
if (!idp) {
Expand All @@ -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}`)
Expand Down
36 changes: 30 additions & 6 deletions service/src/ingress/ingress.entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,14 +89,27 @@ export type IdentityProviderUser = Pick<User, 'username' | 'displayName' | 'emai
* identity provider for Mage to map the identity provider account to the Mage user account.
*/
export interface UserIngressBinding {
userId: UserId
idpId: IdentityProviderId
verified: boolean
enabled: boolean
// TODO: evaluate for utility of disabling a single ingress/idp path for a user as opposed to the entire account
// verified: boolean
// enabled: boolean
/**
* The identity provider account ID is the identifier of the account native to the owning identity provider. This is
* only necessary if the identifier differs from the Mage account's username, especially if a Mage account has
* multiple ingress bindings for different identity providers with different account identifiers.
*/
idpAccountId?: string
/**
* Any attributes the identity provider or protocol needs to persist about the account mapping
*/
idpAccountAttrs?: Record<string, any>
}

export type UserIngressBindings = {
userId: UserId
bindingsByIdp: Map<IdentityProviderId, UserIngressBinding>
}

export type IdentityProviderMutableAttrs = Omit<IdentityProvider, 'id' | 'name' | 'protocol'>

export interface IdentityProviderRepository {
Expand All @@ -109,12 +123,22 @@ export interface IdentityProviderRepository {
deleteIdp(id: IdentityProviderId): Promise<IdentityProvider | null>
}

export type UserIngressBindings = Map<IdentityProviderId, UserIngressBinding>

export interface UserIngressBindingRepository {
export interface UserIngressBindingsRepository {
readBindingsForUser(userId: UserId): Promise<UserIngressBindings>
readAllBindingsForIdp(idpId: IdentityProviderId, paging?: PagingParameters): Promise<PageOf<UserIngressBindings>>
saveUserIngressBinding(userId: UserId, binding: UserIngressBinding): Promise<UserIngressBindings | Error>
/**
* 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<UserIngressBinding | null>
/**
* Return the bindings that were deleted for the given user, or null if the user had no ingress bindings.
*/
deleteBindingsForUser(userId: UserId): Promise<UserIngressBindings | null>
/**
* Return the number of deleted bindings.
*/
deleteAllBindingsForIdp(idpId: IdentityProviderId): Promise<number>
}

/**
Expand Down
2 changes: 1 addition & 1 deletion service/src/ingress/ingress.services.api.ts
Original file line number Diff line number Diff line change
@@ -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 }>
}
10 changes: 3 additions & 7 deletions service/src/ingress/ingress.services.impl.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>
Expand All @@ -12,7 +12,7 @@ export interface FindEventTeam {
(mageEventId: MageEventId): Promise<Team | null>
}

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)
Expand All @@ -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) {
Expand Down

0 comments on commit 500ff6d

Please sign in to comment.