From 229991021c68f9ca917bae7087413f45d6006f3f Mon Sep 17 00:00:00 2001 From: Evert Pot Date: Sun, 24 Sep 2023 23:22:14 -0400 Subject: [PATCH] Refactored everything into privileged-service --- src/app/controller/item.ts | 3 +- src/group/controller/item.ts | 13 ++- src/group/service.ts | 73 -------------- src/oauth2-client/service.ts | 4 +- src/oauth2/service.ts | 5 +- src/one-time-token/service.ts | 3 +- src/principal/privileged-service.ts | 147 ++++++++++++++++++++++++++++ src/principal/service.ts | 122 ----------------------- src/privilege/service.ts | 7 +- src/user-app-permissions/service.ts | 7 +- src/user/controller/item.ts | 9 +- 11 files changed, 177 insertions(+), 216 deletions(-) delete mode 100644 src/group/service.ts delete mode 100644 src/principal/service.ts diff --git a/src/app/controller/item.ts b/src/app/controller/item.ts index d5d017a4..1fb111c4 100644 --- a/src/app/controller/item.ts +++ b/src/app/controller/item.ts @@ -3,7 +3,6 @@ import { Context } from '@curveball/core'; import * as privilegeService from '../../privilege/service'; import * as hal from '../formats/hal'; import { PrincipalService } from '../../principal/privileged-service'; -import * as groupService from '../../group/service'; type EditPrincipalBody = { nickname: string; @@ -35,7 +34,7 @@ class AppController extends Controller { app, principalPrivileges.getAll(), isAdmin, - await groupService.findGroupsForPrincipal(app), + await principalService.findGroupsForPrincipal(app), ); } diff --git a/src/group/controller/item.ts b/src/group/controller/item.ts index d5bff81f..e198b524 100644 --- a/src/group/controller/item.ts +++ b/src/group/controller/item.ts @@ -3,7 +3,6 @@ import { Context } from '@curveball/core'; import * as privilegeService from '../../privilege/service'; import * as hal from '../formats/hal'; import { PrincipalService } from '../../principal/privileged-service'; -import * as groupService from '../../group/service'; import { NotFound, Conflict } from '@curveball/http-errors'; type EditPrincipalBody = { @@ -34,7 +33,7 @@ class GroupController extends Controller { const principalService = new PrincipalService(ctx.privileges); const group = await principalService.findByExternalId(ctx.params.id, 'group'); const isAdmin = ctx.privileges.has('admin'); - const members = await groupService.findMembers(group); + const members = await principalService.findMembers(group); const principalPrivileges = await privilegeService.get(group); @@ -49,7 +48,7 @@ class GroupController extends Controller { group, principalPrivileges.getAll(), isAdmin, - await groupService.findGroupsForPrincipal(group), + await principalService.findGroupsForPrincipal(group), members, ); } @@ -97,23 +96,23 @@ class GroupController extends Controller { switch (ctx.request.body.operation) { case 'add-member': - await groupService.addMember(group, member); + await principalService.addMember(group, member); break; case 'remove-member': - await groupService.removeMember(group, member); + await principalService.removeMember(group, member); break; } if (ctx.request.accepts('text/html')) { const isAdmin = ctx.privileges.has('admin'); - const members = await groupService.findMembers(group); + const members = await principalService.findMembers(group); const principalPrivileges = await privilegeService.get(group); ctx.response.body = hal.item( group, principalPrivileges.getAll(), isAdmin, - await groupService.findGroupsForPrincipal(group), + await principalService.findGroupsForPrincipal(group), members, ); ctx.redirect(200, group.href); diff --git a/src/group/service.ts b/src/group/service.ts deleted file mode 100644 index 6e537e46..00000000 --- a/src/group/service.ts +++ /dev/null @@ -1,73 +0,0 @@ -import db, { query } from '../database'; -import * as principalService from '../principal/service'; -import { Principal, Group } from '../types'; - -/** - * Finding group members - */ - -export async function findMembers(group: Group): Promise { - - const result = await query( - `SELECT ${principalService.fieldNames.join(', ')} FROM principals INNER JOIN group_members ON principals.id = group_members.user_id WHERE group_id = ? ORDER BY nickname`, - [group.id] - ); - - const models = []; - - for (const record of result) { - const model = principalService.recordToModel(record); - models.push(model); - } - - return models; - -} - -export async function addMember(group: Group, user: Principal): Promise { - - const query = 'INSERT INTO group_members (group_id, user_id) VALUES (?, ?)'; - await db.raw(query, [group.id, user.id]); - -} - -export async function replaceMembers(group: Group, users: Principal[]): Promise { - - await db.transaction(async trx => { - await trx.raw('DELETE FROM group_members WHERE group_id = ?', [group.id]); - for(const user of users) { - await trx.raw('INSERT INTO group_members (group_id, user_id) VALUES (?, ?)', [group.id, user.id]); - } - await trx.commit(); - }); - -} - -export async function removeMember(group: Group, user: Principal): Promise { - - const query = 'DELETE FROM group_members WHERE group_id = ? AND user_id = ?'; - await db.raw(query, [group.id, user.id]); - -} - -/** - * Finding group members - */ - -export async function findGroupsForPrincipal(principal: Principal): Promise { - - const result = await query( - `SELECT ${principalService.fieldNames.join(', ')} FROM principals INNER JOIN group_members ON principals.id = group_members.group_id WHERE user_id = ?`, - [principal.id] - ); - - const models: Group[] = []; - - for (const record of result) { - const model = principalService.recordToModel(record); - models.push(model as Group); - } - - return models; - -} diff --git a/src/oauth2-client/service.ts b/src/oauth2-client/service.ts index 293c8c98..cffd7ba4 100644 --- a/src/oauth2-client/service.ts +++ b/src/oauth2-client/service.ts @@ -5,7 +5,7 @@ import { Oauth2ClientsRecord } from 'knex/types/tables'; import { wrapError, UniqueViolationError } from 'db-errors'; import { OAuth2Client } from './types'; -import * as principalService from '../principal/service'; +import { PrincipalService } from '../principal/privileged-service'; import db, { insertAndGetId } from '../database'; import { InvalidRequest } from '../oauth2/errors'; import parseBasicAuth from './parse-basic-auth'; @@ -23,6 +23,7 @@ export async function findByClientId(clientId: string): Promise { const record: Oauth2ClientsRecord = result[0]; + const principalService = new PrincipalService('insecure'); const app = await principalService.findById(record.user_id, 'app'); if (!app.active) { throw new Error(`App ${app.nickname} is not active`); @@ -43,6 +44,7 @@ export async function findById(id: number): Promise { const record: Oauth2ClientsRecord = result[0]; + const principalService = new PrincipalService('insecure'); const app = await principalService.findById(record.user_id, 'app'); if (!app.active) { throw new Error(`App ${app.nickname} is not active`); diff --git a/src/oauth2/service.ts b/src/oauth2/service.ts index b3c59002..e4b7f362 100644 --- a/src/oauth2/service.ts +++ b/src/oauth2/service.ts @@ -3,7 +3,7 @@ import * as crypto from 'crypto'; import db, { query } from '../database'; import { getSetting } from '../server-settings'; -import * as principalService from '../principal/service'; +import { PrincipalService } from '../principal/privileged-service'; import { InvalidGrant, InvalidRequest, UnauthorizedClient } from './errors'; import { CodeChallengeMethod, OAuth2Code, OAuth2Token } from './types'; import { OAuth2Client } from '../oauth2-client/types'; @@ -182,6 +182,7 @@ export async function generateTokenAuthorizationCode(options: GenerateTokenAutho throw new UnauthorizedClient('The client_id associated with the token did not match with the authenticated client credentials'); } + const principalService = new PrincipalService('insecure'); const user = await principalService.findById(codeRecord.principal_id, 'user'); if (!user.active) { throw new Error(`User ${user.href} is not active`); @@ -511,6 +512,7 @@ export async function getTokenByAccessToken(accessToken: string): Promise { throw new BadRequest ('Failed to validate token'); } else { await db.raw('DELETE FROM reset_password_token WHERE token = ?', [token]); + const principalService = new PrincipalService('insecure'); return principalService.findById(result[0][0].user_id) as Promise; } diff --git a/src/principal/privileged-service.ts b/src/principal/privileged-service.ts index ba68ac53..effa6928 100644 --- a/src/principal/privileged-service.ts +++ b/src/principal/privileged-service.ts @@ -216,6 +216,153 @@ export class PrincipalService { return recordToModel(result); } + /** + * Find multiple principals. + * + * This function returns the principals as a map, index by their id. + * This is a helper function typically used by other services to find large numbers + * of joined principals from other tables fast. + * + * If any of the ids in the list is duplicated, they are de-duplicated here. + * if any of the provided ids are not found, this function will error. + */ + async findMany(ids: number[]): Promise> { + + this.privileges.require('a12n:principals:list'); + const records = await db('principals') + .select() + .whereIn('id', ids); + + const result = new Map(records.map( + record => [record.id, recordToModel(record)] + )); + + for (const id of ids) { + if (!result.has(id)) { + throw new NotFound(`Principal with ${id} not found`); + } + } + + return result; + + } + + async findById(id: number, type: 'user'): Promise; + async findById(id: number, type: 'group'): Promise; + async findById(id: number, type: 'app'): Promise; + async findById(id: number): Promise; + async findById(id: number, type?: PrincipalType): Promise { + + this.privileges.require('a12n:principals:list'); + const result = await db('principals') + .select() + .where({id}); + + if (result.length !== 1) { + throw new NotFound(`Principal with id: ${id} not found`); + } + + const principal = recordToModel(result[0]); + + if (type && principal.type !== type) { + throw new NotFound(`Principal with id ${id} does not have type ${type}`); + } + return principal; + + } + + /** + * Returns the list of members of a group + */ + async findMembers(group: Group): Promise { + + this.privileges.require('a12n:principals:list'); + + const result = await db('principals') + .select('principals.*') + .innerJoin('group_members', { 'principals.id': 'group_members.user_id'}) + .where({group_id: group.id}) + .orderBy('nickname'); + + const models = []; + + for (const record of result) { + const model = recordToModel(record); + models.push(model); + } + + return models; + + } + + async addMember(group: Group, user: Principal): Promise { + + this.privileges.require('admin'); + + await db('group_members').insert({ + group_id: group.id, + user_id: user.id + }); + + } + + async replaceMembers(group: Group, users: Principal[]): Promise { + + this.privileges.require('admin'); + await db.transaction(async trx => { + await trx('group_members') + .delete() + .where({ + group_id: group.id + }); + + for(const user of users) { + await trx('groupmembers') + .insert({ + group_id: group.id, + user_id: user.id + }); + } + await trx.commit(); + }); + + } + + async removeMember(group: Group, user: Principal): Promise { + + this.privileges.require('admin'); + await db('group_members') + .delete() + .where({ + group_id: group.id, + user_id: user.id, + }); + + } + + /** + * Returns a list of groups for which the principal is a member + */ + async findGroupsForPrincipal(principal: Principal): Promise { + + this.privileges.require('admin'); + const result = await db('principals') + .select('principals.*') + .innerJoin('group_members', { 'principals.id': 'group_members.group_id'}) + .where({user_id: principal.id}) + .orderBy('nickname'); + + const models: Group[] = []; + + for (const record of result) { + const model = recordToModel(record); + models.push(model as Group); + } + + return models; + + } + } /** * Returns true if more than 1 principal exists in the system. diff --git a/src/principal/service.ts b/src/principal/service.ts deleted file mode 100644 index ddb5c17c..00000000 --- a/src/principal/service.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { NotFound } from '@curveball/http-errors'; -import db from '../database'; -import { Principal, PrincipalType, User, Group, App } from '../types'; -import { PrincipalsRecord } from 'knex/types/tables'; - -export const fieldNames: Array = [ - 'id', - 'identity', - 'external_id', - 'nickname', - 'created_at', - 'modified_at', - 'type', - 'active', - 'system', -]; - - -export async function findById(id: number, type: 'user'): Promise; -export async function findById(id: number, type: 'group'): Promise; -export async function findById(id: number, type: 'app'): Promise; -export async function findById(id: number): Promise; -export async function findById(id: number, type?: PrincipalType): Promise { - - const result = await db('principals') - .select(fieldNames) - .where({id}); - - if (result.length !== 1) { - throw new NotFound(`Principal with id: ${id} not found`); - } - - const principal = recordToModel(result[0]); - - if (type && principal.type !== type) { - throw new NotFound(`Principal with id ${id} does not have type ${type}`); - } - return principal; - -} - - - -/** - * Find multiple principals. - * - * This function returns the principals as a map, index by their id. - * This is a helper function typically used by other services to find large numbers - * of joined principals from other tables fast. - * - * If any of the ids in the list is duplicated, they are de-duplicated here. - * if any of the provided ids are not found, this function will error. - */ -export async function findMany(ids: number[]): Promise> { - - const records = await db('principals') - .select() - .whereIn('id', ids); - - const result = new Map(records.map( - record => [record.id, recordToModel(record)] - )); - - for (const id of ids) { - if (!result.has(id)) { - throw new NotFound(`Principal with ${id} not found`); - } - } - - return result; - -} - -function userTypeIntToUserType(input: number): PrincipalType { - - switch (input) { - case 1: return 'user'; - case 2: return 'app'; - case 3: return 'group'; - default: - throw new Error('Unknown user type id: ' + input); - } - -} - -export function recordToModel(user: PrincipalsRecord): Principal { - - return { - id: user.id, - href: `/${userTypeIntToUserType(user.type)}/${user.external_id}`, - identity: user.identity, - externalId: user.external_id, - nickname: user.nickname!, - createdAt: new Date(user.created_at), - modifiedAt: new Date(user.modified_at), - type: userTypeIntToUserType(user.type), - active: !!user.active, - system: !!user.system, - }; - -} - -export function isIdentityValid(identity: string): boolean { - - const regex = /^(?:[A-Za-z]+:\S*$)?/; - return regex.test(identity); - -} - - -export function getPathName(href: string): string { - - let url; - - try { - url = new URL(href); - } catch { - return href; - } - return url.pathname; - -} diff --git a/src/privilege/service.ts b/src/privilege/service.ts index 2e499414..33442ca7 100644 --- a/src/privilege/service.ts +++ b/src/privilege/service.ts @@ -3,7 +3,7 @@ import db, { query } from '../database'; import { Principal } from '../types'; import { Privilege, PrivilegeMap, PrivilegeEntry } from './types'; import { UserPrivilegesRecord } from 'knex/types/tables'; -import * as principalService from '../principal/service'; +import { PrincipalService } from '../principal/privileged-service'; import { Forbidden } from '@curveball/http-errors'; @@ -23,11 +23,14 @@ export async function get(who: Context | Principal | 'insecure'): Promise { + const principalService = new PrincipalService('insecure'); const records = await db('user_privileges') .select('*') .where({resource}).orWhere({resource: '*'}); - const principals = await principalService.findMany(records.map(record => record.user_id)); + const principals = await principalService.findMany( + records.map(record => record.user_id) + ); return records.map(record => ({ privilege: record.privilege, diff --git a/src/user-app-permissions/service.ts b/src/user-app-permissions/service.ts index 07cd8482..ccc3a2e8 100644 --- a/src/user-app-permissions/service.ts +++ b/src/user-app-permissions/service.ts @@ -1,7 +1,7 @@ import db from '../database'; import { App, User } from '../types'; import { UserAppPermission } from './types'; -import * as principalService from '../principal/service'; +import { PrincipalService } from '../principal/privileged-service'; import { UserAppPermissionsRecord } from 'knex/types/tables'; import { NotFound } from '@curveball/http-errors'; @@ -56,11 +56,14 @@ export async function setPermissions(app: App, user: User, scope: string[]): Pro */ export async function findByUser(user: User): Promise { + const principalService = new PrincipalService('insecure'); const records = await db('user_app_permissions') .select() .where({user_id: user.id}); - const apps = await principalService.findMany(records.map(record => record.app_id)) as Map; + const apps = await principalService.findMany( + records.map(record => record.app_id) + ) as Map; return records.map( record => recordToModel( record, diff --git a/src/user/controller/item.ts b/src/user/controller/item.ts index fba6946d..b0290e1d 100644 --- a/src/user/controller/item.ts +++ b/src/user/controller/item.ts @@ -5,7 +5,6 @@ import * as userHal from '../formats/hal'; import * as appHal from '../../app/formats/hal'; import * as groupHal from '../../group/formats/hal'; import * as userService from '../service'; -import * as groupService from '../../group/service'; import { PrincipalService } from '../../principal/privileged-service'; type EditPrincipalBody = { @@ -56,16 +55,16 @@ class UserController extends Controller { hasControl, hasPassword, isAdmin, - await groupService.findGroupsForPrincipal(principal), + await principalService.findGroupsForPrincipal(principal), ); break; case 'group' : { - const members = await groupService.findMembers(principal); + const members = await principalService.findMembers(principal); ctx.response.body = groupHal.item( principal, principalPrivileges.getAll(), isAdmin, - await groupService.findGroupsForPrincipal(principal), + await principalService.findGroupsForPrincipal(principal), members, ); break; @@ -75,7 +74,7 @@ class UserController extends Controller { principal, principalPrivileges.getAll(), isAdmin, - await groupService.findGroupsForPrincipal(principal), + await principalService.findGroupsForPrincipal(principal), ); break; }