diff --git a/src/auth/index.ts b/src/auth/index.ts index 77570a47..61e55647 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -2,15 +2,15 @@ * This file is a new (type-safe) interface for common authentication functions * ahead of the auth refactor outlined in HPC-6999 */ +import Knex = require('knex'); import * as crypto from 'node:crypto'; import { promisify } from 'node:util'; -import type v4Models from '../db'; import { type AuthTargetId } from '../db/models/authTarget'; import { type ParticipantId } from '../db/models/participant'; import type { Database } from '../db/type'; import { Op } from '../db/util/conditions'; import { createDeferredFetcher } from '../db/util/deferred'; -import { type InstanceOfModel } from '../db/util/types'; +import type { InstanceOfModel, UserDataOfModel } from '../db/util/types'; import { type Context } from '../lib/context'; import { type SharedLogContext } from '../lib/logging'; import { organizeObjectsByUniqueProperty } from '../util'; @@ -31,8 +31,6 @@ import { const randomBytes = promisify(crypto.randomBytes); -type Models = ReturnType; - type Participant = InstanceOfModel; type AuthGrantee = InstanceOfModel; type AuthTarget = InstanceOfModel; @@ -471,11 +469,11 @@ export const getParticipantFromToken = async ( export const createToken = async ({ participant, expires, - models, + database, }: { participant: ParticipantId; expires?: Date; - models: Models; + database: Database; }): Promise<{ instance: AuthToken; token: string; @@ -487,7 +485,7 @@ export const createToken = async ({ .digest() .toString('hex'); return { - instance: await models.authToken.create({ + instance: await database.authToken.create({ tokenHash, participant, expires, @@ -495,3 +493,51 @@ export const createToken = async ({ token, }; }; + +export const deleteAuthTarget = async ( + database: Database, + where: UserDataOfModel, + actor: ParticipantId, + trx: Knex.Transaction +): Promise => { + const authTargets = await database.authTarget.find({ where, trx }); + const authGrants = await database.authGrant.find({ + where: { + target: { [Op.IN]: authTargets.map((at) => at.id) }, + }, + trx, + }); + + await Promise.all( + authGrants.map(async (grant) => { + await database.authGrant.update( + { + grantee: grant.grantee, + target: grant.target, + roles: [], // Delete `authGrant` entry + }, + actor, + trx + ); + }) + ); + + await Promise.all( + authTargets.map(async ({ id }) => { + await database.authGrantLog.destroy({ + where: { target: id }, + trx, + }); + + await database.authInvite.destroy({ + where: { target: id }, + trx, + }); + + await database.authTarget.destroy({ + where: { id }, + trx, + }); + }) + ); +}; diff --git a/src/auth/permissions.ts b/src/auth/permissions.ts index 347db3c6..69ebb1b7 100644 --- a/src/auth/permissions.ts +++ b/src/auth/permissions.ts @@ -85,6 +85,7 @@ export const AUTH_PERMISSIONS = { */ CLONE_ANY_PROJECT: 'cloneAnyProject', DELETE_ANY_PROJECT: 'canDeleteAnyProject', + DELETE_ANY_PLAN: 'canDeleteAnyPlan', /** * Can change visibility of any plan in Projects Module */ diff --git a/src/auth/roles.ts b/src/auth/roles.ts index 247eed7e..9fda6321 100644 --- a/src/auth/roles.ts +++ b/src/auth/roles.ts @@ -181,6 +181,7 @@ export const calculatePermissionsFromRolesGrant = async < global.add(P.global.EDIT_ANY_PLAN_DATA); global.add(P.global.EDIT_ANY_MEASUREMENT); global.add(P.global.CHANGE_ANY_PLAN_VISIBILITY_IN_PROJECTS); + global.add(P.global.DELETE_ANY_PLAN); } else if (role === 'ftsAdmin') { // New Permissions global.add(P.global.VIEW_ANY_FLOW); diff --git a/src/db/models/authGrant.ts b/src/db/models/authGrant.ts index 7a94a6a1..5ee41218 100644 --- a/src/db/models/authGrant.ts +++ b/src/db/models/authGrant.ts @@ -42,9 +42,10 @@ const authGrantModel = (conn: Knex) => { const create = ( data: UserData, actor: ParticipantId, - date = new Date() + date = new Date(), + trx?: Knex.Transaction ): Promise => { - return conn.transaction(async (trx) => { + const createCallback = async (trx: Knex.Transaction) => { await authGrantLog.create( { grantee: data.grantee, @@ -58,11 +59,17 @@ const authGrantModel = (conn: Knex) => { } ); return await model.create(data, { trx }); - }); + }; + + return trx ? createCallback(trx) : conn.transaction(createCallback); }; - const update = (data: UserData, actor: ParticipantId): Promise => { - return conn.transaction(async (trx) => { + const update = ( + data: UserData, + actor: ParticipantId, + trx?: Knex.Transaction + ): Promise => { + const updateCallback = async (trx: Knex.Transaction) => { await authGrantLog.create( { grantee: data.grantee, @@ -95,7 +102,9 @@ const authGrantModel = (conn: Knex) => { }, }); } - }); + }; + + return trx ? updateCallback(trx) : conn.transaction(updateCallback); }; const createOrUpdate = async ( diff --git a/src/db/models/authTarget.ts b/src/db/models/authTarget.ts index 4d7356dd..72d622f8 100644 --- a/src/db/models/authTarget.ts +++ b/src/db/models/authTarget.ts @@ -1,5 +1,6 @@ import * as t from 'io-ts'; import { defineIDModel } from '../util/id-model'; +import Knex = require('knex'); import { brandedType } from '../../util/io-ts'; import type { Brand } from '../../util/types'; @@ -21,28 +22,38 @@ const AUTH_TARGET_TYPE = { governingEntity: null, }; -export default defineIDModel({ - tableName: 'authTarget', - fields: { - generated: { - id: { - kind: 'branded-integer', - brand: AUTH_TARGET_ID, +export default (conn: Knex) => { + const model = defineIDModel({ + tableName: 'authTarget', + fields: { + generated: { + id: { + kind: 'branded-integer', + brand: AUTH_TARGET_ID, + }, }, - }, - optional: { - targetId: { - kind: 'checked', - type: t.number, + optional: { + targetId: { + kind: 'checked', + type: t.number, + }, }, - }, - required: { - type: { - kind: 'enum', - values: AUTH_TARGET_TYPE, + required: { + type: { + kind: 'enum', + values: AUTH_TARGET_TYPE, + }, }, }, - }, - idField: 'id', - softDeletionEnabled: false, -}); + idField: 'id', + softDeletionEnabled: false, + })(conn); + + return { + ...model, + /** + * @deprecated Use `deleteAuthTarget` utility method instead + */ + destroy: model.destroy, + }; +};