fix: lists decide every org through the permission system; no system-org shortcut
This commit is contained in:
@@ -9,6 +9,7 @@ export {
|
||||
isAllowed,
|
||||
permittedOrgs,
|
||||
permittedScope,
|
||||
listScope,
|
||||
type OrgTarget,
|
||||
type OrgSelection,
|
||||
} from './permissions.js';
|
||||
|
||||
@@ -75,6 +75,32 @@ export function orgScopeExpr(orgIds: string[], column: string, paramOffset = 0):
|
||||
return { expr: `${column} IN (${parts.join(' UNION ')})`, params: orgIds };
|
||||
}
|
||||
|
||||
/**
|
||||
* The filter a list applies to `column`: the orgs it answers for are those under
|
||||
* the orgs of `scope` (every org when the request named none), kept only where
|
||||
* the caller holds (activity, target) once the permission hooks have run. A row
|
||||
* with no org shows only on an unpinned list, which `permittedScope` let through
|
||||
* on the no-org perimeter.
|
||||
*/
|
||||
export async function listScope(
|
||||
db: Pool,
|
||||
req: FastifyRequest,
|
||||
scope: string[],
|
||||
activity: string,
|
||||
target: string,
|
||||
column: string,
|
||||
paramOffset = 0,
|
||||
): Promise<{ expr: string; params: [string[]] }> {
|
||||
const user = getUser(req);
|
||||
const { rows } = scope.length
|
||||
? await db.query<{ id: string }>(
|
||||
"SELECT DISTINCT s.org_id AS id FROM unnest($1::UUID[]) o, morbac.get_org_scope(o, 'subtree') s", [scope])
|
||||
: await db.query<{ id: string }>('SELECT id FROM morbac.orgs');
|
||||
const orgs = user ? await permittedOrgs(db, user.id, rows.map((r) => r.id), activity, target, req) : [];
|
||||
const own = `${column} = ANY($${paramOffset + 1}::UUID[])`;
|
||||
return { expr: scope.length ? own : `(${own} OR ${column} IS NULL)`, params: [orgs] };
|
||||
}
|
||||
|
||||
export function viewForType(type: 'user' | 'service'): string {
|
||||
return type === 'service' ? 'service_users' : 'users';
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Pool } from 'pg';
|
||||
import { getUser, getOrgScope, orgScopeExpr, hasPermission, permittedScope, canActOnUser, SYSTEM_ORG_ID } from '../permissions.js';
|
||||
import { getUser, getOrgScope, listScope, hasPermission, permittedScope, canActOnUser, SYSTEM_ORG_ID } from '../permissions.js';
|
||||
import { delegationResponse, notFound, forbidden, unauthorized, badRequest } from '../schemas.js';
|
||||
|
||||
const SELECT_COLUMNS = `
|
||||
@@ -33,7 +33,7 @@ export function createDelegationRoutes(db: Pool) {
|
||||
const scope = await permittedScope(db, req, reply, orgIds, 'read', 'delegations');
|
||||
if (!scope) return reply;
|
||||
|
||||
const { expr, params } = orgScopeExpr(scope, 'd.org_id');
|
||||
const { expr, params } = await listScope(db, req, scope, 'read', 'delegations', 'd.org_id');
|
||||
const { rows } = await db.query(
|
||||
`SELECT ${SELECT_COLUMNS}
|
||||
FROM morbac.delegations d
|
||||
|
||||
+4
-5
@@ -3,7 +3,7 @@ import { withMorbacContext } from '../rls-context.js';
|
||||
import type { Pool } from 'pg';
|
||||
import type { CheckLimitFn, OrgEventSink } from '../plugin.js';
|
||||
import {
|
||||
getUser, getOrgScope, orgScopeExpr, hasPermission, permittedScope, permittedOrgs, SYSTEM_ORG_ID,
|
||||
getUser, getOrgScope, listScope, hasPermission, permittedScope, permittedOrgs, SYSTEM_ORG_ID,
|
||||
} from '../permissions.js';
|
||||
import { orgResponse, notFound, forbidden, unauthorized, badRequest } from '../schemas.js';
|
||||
|
||||
@@ -69,12 +69,11 @@ export function createOrgRoutes(db: Pool, checkLimit?: CheckLimitFn, emitOrgEven
|
||||
const scope = await permittedScope(db, req, reply, orgIds, 'read', 'orgs');
|
||||
if (!scope) return reply;
|
||||
|
||||
const isSystemScope = scope.includes(SYSTEM_ORG_ID);
|
||||
const { expr, params } = isSystemScope ? { expr: '', params: [] } : orgScopeExpr(scope, 'id');
|
||||
const { params: [visible] } = await listScope(db, req, scope, 'read', 'orgs', 'id');
|
||||
const { rows } = await withMorbacContext(db, { userId: user.id, orgId: scope[0] }, (client) =>
|
||||
client.query(
|
||||
withOrgPaths(`SELECT id, name, parent_id, metadata FROM morbac.orgs ${expr ? `WHERE ${expr}` : ''}`),
|
||||
params,
|
||||
withOrgPaths('SELECT id, name, parent_id, metadata FROM morbac.orgs WHERE id = ANY($1::UUID[])'),
|
||||
[visible],
|
||||
),
|
||||
);
|
||||
return rows;
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ import type { FastifyInstance } from 'fastify';
|
||||
import type { Pool } from 'pg';
|
||||
import type { CheckLimitFn, RoleEventSink } from '../plugin.js';
|
||||
import {
|
||||
getUser, getOrgScope, orgScopeExpr, hasPermission, permittedScope, SYSTEM_ORG_ID,
|
||||
getUser, getOrgScope, listScope, hasPermission, permittedScope, SYSTEM_ORG_ID,
|
||||
} from '../permissions.js';
|
||||
import { parseListParams, composeWhere, setContentRange, ListQueryError } from '../list-query.js';
|
||||
import { roleResponse, notFound, forbidden, unauthorized, badRequest } from '../schemas.js';
|
||||
@@ -33,7 +33,7 @@ export function createRoleRoutes(db: Pool, checkLimit?: CheckLimitFn, emitRoleEv
|
||||
const scope = await permittedScope(db, req, reply, orgIds, 'read', 'roles');
|
||||
if (!scope) return reply;
|
||||
|
||||
const { expr, params } = orgScopeExpr(scope, 'r.org_id');
|
||||
const { expr, params } = await listScope(db, req, scope, 'read', 'roles', 'r.org_id');
|
||||
let list;
|
||||
try {
|
||||
list = parseListParams(req.query as Record<string, unknown>, {
|
||||
@@ -218,7 +218,7 @@ export function createRoleRoutes(db: Pool, checkLimit?: CheckLimitFn, emitRoleEv
|
||||
const scope = await permittedScope(db, req, reply, orgIds, 'read', 'roles');
|
||||
if (!scope) return reply;
|
||||
|
||||
const { expr, params } = orgScopeExpr(scope, 'rs.org_id');
|
||||
const { expr, params } = await listScope(db, req, scope, 'read', 'roles', 'rs.org_id');
|
||||
const { rows } = await db.query(
|
||||
`SELECT rh.senior_role_id, rs.name AS senior_name,
|
||||
rh.junior_role_id, rj.name AS junior_name,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Pool } from 'pg';
|
||||
import { getUser, getOrgScope, orgScopeExpr, hasPermission, permittedScope, isActivityViewBound, SYSTEM_ORG_ID } from '../permissions.js';
|
||||
import { getUser, getOrgScope, listScope, hasPermission, permittedScope, isActivityViewBound, SYSTEM_ORG_ID } from '../permissions.js';
|
||||
import { crossOrgRuleResponse, notFound, forbidden, unauthorized, badRequest } from '../schemas.js';
|
||||
|
||||
export function createCrossOrgRuleRoutes(db: Pool) {
|
||||
@@ -26,12 +26,8 @@ export function createCrossOrgRuleRoutes(db: Pool) {
|
||||
const scope = await permittedScope(db, req, reply, orgIds, 'read', 'cross_org_rules');
|
||||
if (!scope) return reply;
|
||||
|
||||
const { expr: srcExpr, params: srcParams } = orgScopeExpr(scope, 'cor.source_org_id');
|
||||
const { expr: tgtExpr, params: tgtParams } = orgScopeExpr(scope, 'cor.target_org_id', srcParams.length);
|
||||
const where = srcExpr && tgtExpr ? `WHERE (${srcExpr} OR ${tgtExpr})`
|
||||
: srcExpr ? `WHERE ${srcExpr}`
|
||||
: tgtExpr ? `WHERE ${tgtExpr}`
|
||||
: '';
|
||||
const { params } = await listScope(db, req, scope, 'read', 'cross_org_rules', 'cor.source_org_id');
|
||||
const where = 'WHERE cor.source_org_id = ANY($1::UUID[]) OR cor.target_org_id = ANY($1::UUID[])';
|
||||
|
||||
const { rows } = await db.query(
|
||||
`SELECT cor.id,
|
||||
@@ -46,7 +42,7 @@ export function createCrossOrgRuleRoutes(db: Pool) {
|
||||
JOIN morbac.orgs "to" ON "to".id = cor.target_org_id
|
||||
${where}
|
||||
ORDER BY so.name, sr.name, "to".name, cor.activity, cor.view`,
|
||||
[...srcParams, ...tgtParams],
|
||||
params,
|
||||
);
|
||||
return rows;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Pool } from 'pg';
|
||||
import { getUser, getOrgScope, orgScopeExpr, hasPermission, permittedScope, canGrantRule, isActivityViewBound, SYSTEM_ORG_ID } from '../permissions.js';
|
||||
import { getUser, getOrgScope, listScope, hasPermission, permittedScope, canGrantRule, isActivityViewBound, SYSTEM_ORG_ID } from '../permissions.js';
|
||||
import { ruleResponse, notFound, forbidden, unauthorized, badRequest } from '../schemas.js';
|
||||
|
||||
export function createRoleRuleRoutes(db: Pool) {
|
||||
@@ -26,7 +26,7 @@ export function createRoleRuleRoutes(db: Pool) {
|
||||
const scope = await permittedScope(db, req, reply, orgIds, 'read', 'rules');
|
||||
if (!scope) return reply;
|
||||
|
||||
const { expr, params } = orgScopeExpr(scope, 'r.org_id');
|
||||
const { expr, params } = await listScope(db, req, scope, 'read', 'rules', 'r.org_id');
|
||||
const { rows } = await db.query(
|
||||
`SELECT r.id, r.role_id, r.org_id, o.name AS org_name, ro.name AS role_name,
|
||||
r.activity, r.view, r.modality, r.priority, r.scope,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Pool } from 'pg';
|
||||
import { getUser, getOrgScope, orgScopeExpr, hasPermission, permittedScope, isActivityViewBound } from '../permissions.js';
|
||||
import { getUser, getOrgScope, listScope, hasPermission, permittedScope, isActivityViewBound } from '../permissions.js';
|
||||
import { userRuleResponse, notFound, forbidden, unauthorized, badRequest } from '../schemas.js';
|
||||
|
||||
export function createUserRuleRoutes(db: Pool) {
|
||||
@@ -26,7 +26,7 @@ export function createUserRuleRoutes(db: Pool) {
|
||||
const scope = await permittedScope(db, req, reply, orgIds, 'read', 'user_rules');
|
||||
if (!scope) return reply;
|
||||
|
||||
const { expr, params } = orgScopeExpr(scope, 'ur.org_id');
|
||||
const { expr, params } = await listScope(db, req, scope, 'read', 'user_rules', 'ur.org_id');
|
||||
const { rows } = await db.query(
|
||||
`SELECT ur.id, ur.user_id, ur.org_id,
|
||||
COALESCE(u.name, ur.user_id::TEXT) AS user_name,
|
||||
|
||||
@@ -3,8 +3,8 @@ import type { Pool } from 'pg';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import type { CheckLimitFn } from '../plugin.js';
|
||||
import {
|
||||
getUser, getOrgScope, orgScopeExpr, hasPermission, permittedScope, canActOnUser, checkApiKeyScope,
|
||||
getAccountType, viewForType, isAllowed, SYSTEM_ORG_ID,
|
||||
getUser, getOrgScope, listScope, hasPermission, permittedScope, canActOnUser, checkApiKeyScope,
|
||||
getAccountType, viewForType, isAllowed,
|
||||
} from '../permissions.js';
|
||||
import { userResponse, notFound, forbidden, unauthorized, badRequest } from '../schemas.js';
|
||||
import { generatePassword } from '../password.js';
|
||||
@@ -44,12 +44,9 @@ export function createUserCrudRoutes(db: Pool, checkLimit?: CheckLimitFn) {
|
||||
isAllowed(db, user.id, null, 'read', 'users', req),
|
||||
]);
|
||||
|
||||
const { expr, params } = orgScopeExpr(scope, 'ur2.org_id');
|
||||
const isSystemScope = scope.includes(SYSTEM_ORG_ID);
|
||||
const { params } = await listScope(db, req, scope, 'read', 'users', 'ur2.org_id');
|
||||
const placement = canReadUnattached ? ' OR u.needs_placement' : '';
|
||||
const whereClause = expr && !isSystemScope
|
||||
? `WHERE (u.id IN (SELECT ur2.user_id FROM morbac.user_roles ur2 WHERE ${expr})${placement} OR u.account_type IN ('system', 'service'))`
|
||||
: '';
|
||||
const whereClause = `WHERE (u.id IN (SELECT ur2.user_id FROM morbac.user_roles ur2 WHERE ur2.org_id = ANY($1::UUID[]))${placement} OR u.account_type IN ('system', 'service'))`;
|
||||
const { rows } = await db.query(
|
||||
`SELECT
|
||||
u.id, u.email, u.name, u.account_type, u.is_active, u.needs_placement, u.created_at,
|
||||
@@ -68,7 +65,7 @@ export function createUserCrudRoutes(db: Pool, checkLimit?: CheckLimitFn) {
|
||||
LEFT JOIN morbac.orgs o ON o.id = ur.org_id
|
||||
${whereClause}
|
||||
GROUP BY u.id ORDER BY u.name`,
|
||||
whereClause ? params : [],
|
||||
params,
|
||||
);
|
||||
return (rows as { account_type: string }[]).filter((u) => {
|
||||
if (u.account_type === 'system' && !canSeeSystem) return false;
|
||||
|
||||
Reference in New Issue
Block a user