feat(permissions): granular permission gating + role editor & presets (#747, phase 1 of #743) (#1045)
* feat(permissions): granular permission gating + role editor & presets Make every admin feature permission-gateable so multi-user studios can split capability across roles (#747, and phase 1 of #743). - Split the catch-all settings.edit into dedicated dangerous-config perms (banking / domains / security / integrations / features): a team member can no longer change IBAN, domains, SSO, webhooks, API tokens or feature flags. Reads keep an OR with settings.view so existing roles keep visibility. The site-URL write inside /general is change-gated on settings.domains. - Add dedicated perms for admin surfaces miscategorised under settings.* (whatsapp, event_types, image_security, notifications, system) plus roles.manage and vat_codes.view; gate the previously-ungated VAT read. - Boot self-heal (_permissionsBoot.js): super_admin always holds every permission (tracks-all) so new perms never need a compensation migration; all other roles stay frozen (no silent escalation on upgrade). - Seed two presets: Solo Photographer (full operator) and Team Photographer (contributor — view events + manage photos + read-only CRM; no settings/users/billing edits, no events.edit). - Role editor: adminRoles CRUD (create/edit/clone/delete + permission matrix; system roles protected, super_admin immutable) and a Roles tab with a category-grouped matrix and preset cloning. - Settings page tabs are permission-gated with snap-back; i18n en/de. Migration 174. Backward-compatible: admin/editor/viewer unchanged. * feat(permissions): hide in-page action buttons a role can't use Wrap mutating controls on the surfaces restricted roles actually reach (Events list, Archives, gallery photo grid, event detail) in PermissionGate so they are HIDDEN when the user lacks the permission, rather than shown-then-403: - Events list: create / bulk archive / bulk delete / row archive / row delete / download-archive. - Archives: restore / download / delete. - Photo grid: single + bulk delete (photos.delete), per-photo download (photos.download), bulk move/hide/show (photos.edit). - Event detail: edit / rename / publish (events.edit), duplicate (events.create), archive (events.archive), create-invoice (bills.manage); the Actions card is hidden entirely for view-only roles. - Photos tab: upload / external import (photos.upload), export menu (photos.download). Backend already enforces these with 403; this is the matching UX so a Team Photographer never sees delete/settings controls. * fix(permissions): close settings-split bypass via generic settings writers Security review found the settings.edit split was bypassable: the generic settings writers (/general, /analytics, /seo, /security) upsert arbitrary setting_keys, so a role holding only settings.edit (or settings.security) could write keys owned by a narrower permission — repointing the public site URL (settings.domains), security policy (settings.security) or VAT/accounting config (settings.banking) via the wrong endpoint. Add stripUnauthorizedProtectedKeys(): before every generic upsert, drop any protected key the caller isn't permitted to write (general_site_url → settings.domains, security_* → settings.security, accounting_* → settings.banking). Dedicated routes still work because their caller holds the matching perm. Replaces the narrower in-handler site-URL guard. Also fix two tests affected by the RBAC changes: - authzPermissionGaps: API-token management moved to settings.integrations, so grant that (not settings.edit) to exercise the ownership 404. - AdminPhotoGrid.viewToggle: stub PermissionGate (its buttons are now gated and the test renders without a PermissionsProvider). * fix(permissions): address upstream review (#1045) - Renumber migration 174 -> 175 (174 now taken by 174_sqlite_nullable_event_dates from #1035; the collision made picpeakImportService's forward-only restore guard treat both as order 174 and accept a newer .picpeak onto an older schema). - Contain the roles.manage blast radius (delegation, not root escalation): a non-super_admin can no longer edit their own role, nor grant any permission their own role doesn't already hold (createRole + updateRole). - Protected-key denial now 403s (naming the keys + required perms) instead of silently stripping and reporting "saved" (adminSettings generic writers). - Reserve team_photographer so a custom role can't squat the preset name. - Boot self-heal: per-step try/catch so a role_permissions insert race on one replica doesn't skip preset seeding. - Forward-project the feature .manage perms that also replaced settings.edit gates (whatsapp/event_types/image_security/notifications/system), matching the settings.* split projection so the pattern is symmetric for phase-2. - Guard exports.down's roles/admin_users queries with hasTable. * fix(permissions): change-detection on protected-key 403 + commit guard tests (#1045) Round-2 review: - The protected-key 403 fired on key PRESENCE. The General tab re-posts general_site_url on every save, so a settings.edit-only role (the office manager this PR enables) got 403'd on every General save even when the URL was unchanged. Restore change-detection: compare the incoming value against the stored one and 403 only on an actual change; unchanged protected keys are dropped so the rest of the save proceeds. Only /general is affected. - Commit the self-amplification guard test (was run locally, never staged): adminRolesGuards.test.js — non-super can't grant perms it lacks, can't edit its own role, can't escalate another role; super_admin bypasses; team_photographer name reserved. - Add adminSettingsProtectedKeys.test.js pinning the change-detection: an unchanged general_site_url saves, an actual change 403s, super_admin changes it.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Boot-time self-heal for the RBAC permission catalog.
|
||||
*
|
||||
* The single durable guarantee here: the `super_admin` role holds EVERY
|
||||
* permission currently defined. This is the "Admin tracks all" mechanism —
|
||||
* whenever a future release adds a permission (via its migration), this boot
|
||||
* pass grants it to super_admin automatically, so a new perm never needs a
|
||||
* compensation migration and the owner is never locked out of a new feature.
|
||||
* See feedback_self_heal_pattern + project_permission_gating.
|
||||
*
|
||||
* Deliberately NARROW: it only ever backfills `super_admin`. All other roles —
|
||||
* admin, editor, viewer, the solo_photographer preset, and any custom roles an
|
||||
* org creates — are FROZEN: new perms default OFF for them so nobody silently
|
||||
* gains a capability (e.g. the ability to change IBAN/domains) on upgrade. The
|
||||
* owner grants those explicitly via the role editor.
|
||||
*
|
||||
* The solo_photographer preset itself is seeded (with all-perms-at-seed-time) by
|
||||
* migration 174; this pass only ensures it exists so a partially-migrated or
|
||||
* hand-restored DB still shows the preset. Its grants are never re-synced.
|
||||
*
|
||||
* Idempotent and best-effort: any failure is logged and swallowed so a boot is
|
||||
* never blocked by permission housekeeping.
|
||||
*/
|
||||
|
||||
// Preset roles shipped with the app. `permissions: 'ALL'` = every current perm.
|
||||
// Kept in sync with migration 174 (PRESET_ROLES). This boot pass only ensures a
|
||||
// preset EXISTS (create + grant if missing) so a partially-migrated or restored
|
||||
// DB still shows it; it never re-syncs an existing preset's grants (frozen).
|
||||
const PRESETS = [
|
||||
{
|
||||
name: 'solo_photographer',
|
||||
display_name: 'Solo Photographer',
|
||||
description: 'Full operator for a one-person studio — everything needed to run the business. A preset starting point; new-release permissions are not auto-added (only Super Admin tracks all).',
|
||||
is_system: true,
|
||||
priority: 90,
|
||||
permissions: 'ALL',
|
||||
},
|
||||
{
|
||||
name: 'team_photographer',
|
||||
display_name: 'Team Photographer',
|
||||
description: 'Contributing photographer (second/festival shooter) — view events, upload and manage photos, and see read-only client context. Not the customer contact: no settings, user management, billing edits or event configuration. A preset starting point.',
|
||||
is_system: true,
|
||||
priority: 40,
|
||||
permissions: [
|
||||
'events.view',
|
||||
'photos.view', 'photos.upload', 'photos.edit', 'photos.download',
|
||||
'customers.view', 'quotes.view', 'bills.view',
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
async function ensureSuperAdminHasAllPermissions(db, logger) {
|
||||
const superAdmin = await db('roles').where({ name: 'super_admin' }).first();
|
||||
if (!superAdmin) return 0;
|
||||
|
||||
const allPerms = await db('permissions').select('id');
|
||||
if (allPerms.length === 0) return 0;
|
||||
|
||||
const held = await db('role_permissions')
|
||||
.where({ role_id: superAdmin.id })
|
||||
.select('permission_id');
|
||||
const heldSet = new Set(held.map((r) => r.permission_id));
|
||||
|
||||
const inserts = allPerms
|
||||
.filter((p) => !heldSet.has(p.id))
|
||||
.map((p) => ({ role_id: superAdmin.id, permission_id: p.id }));
|
||||
|
||||
if (inserts.length > 0) {
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < inserts.length; i += batchSize) {
|
||||
await db('role_permissions').insert(inserts.slice(i, i + batchSize));
|
||||
}
|
||||
logger?.info?.(`Permissions self-heal: granted ${inserts.length} missing permission(s) to super_admin`);
|
||||
}
|
||||
return inserts.length;
|
||||
}
|
||||
|
||||
async function ensurePreset(db, logger, preset) {
|
||||
const existing = await db('roles').where({ name: preset.name }).first();
|
||||
if (existing) return; // frozen — never re-sync its grants
|
||||
|
||||
await db('roles').insert({
|
||||
name: preset.name,
|
||||
display_name: preset.display_name,
|
||||
description: preset.description,
|
||||
is_system: preset.is_system,
|
||||
priority: preset.priority,
|
||||
created_at: db.fn.now(),
|
||||
updated_at: db.fn.now(),
|
||||
});
|
||||
const role = await db('roles').where({ name: preset.name }).first();
|
||||
if (!role) return;
|
||||
|
||||
let permIds;
|
||||
if (preset.permissions === 'ALL') {
|
||||
permIds = (await db('permissions').select('id')).map((p) => p.id);
|
||||
} else {
|
||||
const rows = await db('permissions').whereIn('name', preset.permissions).select('id');
|
||||
permIds = rows.map((p) => p.id);
|
||||
}
|
||||
if (permIds.length > 0) {
|
||||
const inserts = permIds.map((id) => ({ role_id: role.id, permission_id: id }));
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < inserts.length; i += batchSize) {
|
||||
await db('role_permissions').insert(inserts.slice(i, i + batchSize));
|
||||
}
|
||||
}
|
||||
logger?.info?.(`Permissions self-heal: seeded missing ${preset.name} preset (${permIds.length} permissions)`);
|
||||
}
|
||||
|
||||
async function seedPermissionsAtBoot(db, logger) {
|
||||
try {
|
||||
const hasRoles = await db.schema.hasTable('roles');
|
||||
const hasPerms = await db.schema.hasTable('permissions');
|
||||
const hasRolePerms = await db.schema.hasTable('role_permissions');
|
||||
if (!hasRoles || !hasPerms || !hasRolePerms) return;
|
||||
|
||||
// Per-step try/catch: on a multi-replica start the loser of a
|
||||
// role_permissions insert race can throw a PK violation in one step; that
|
||||
// must not skip the remaining steps (e.g. preset seeding) on that replica.
|
||||
let granted = 0;
|
||||
try {
|
||||
granted = await ensureSuperAdminHasAllPermissions(db, logger);
|
||||
} catch (err) {
|
||||
logger?.warn?.('Permissions self-heal: super_admin backfill failed:', err.message);
|
||||
}
|
||||
for (const preset of PRESETS) {
|
||||
try {
|
||||
await ensurePreset(db, logger, preset);
|
||||
} catch (err) {
|
||||
logger?.warn?.(`Permissions self-heal: preset ${preset.name} failed:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (granted > 0) {
|
||||
// Drop the in-memory permission cache so the new grants take effect
|
||||
// without waiting out the 60s TTL.
|
||||
try {
|
||||
const { clearPermissionCache } = require('../middleware/permissions');
|
||||
clearPermissionCache?.();
|
||||
} catch (_) { /* cache module optional at boot */ }
|
||||
}
|
||||
} catch (err) {
|
||||
logger?.warn?.('Permissions self-heal failed at boot:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { seedPermissionsAtBoot };
|
||||
@@ -11,7 +11,7 @@ const { generateReadablePassword } = require('../utils/passwordGenerator');
|
||||
const { getBcryptRounds } = require('../utils/passwordValidation');
|
||||
const { queueEmail } = require('./emailProcessor');
|
||||
const logger = require('../utils/logger');
|
||||
const { ConflictError, NotFoundError, ValidationError } = require('../utils/errors');
|
||||
const { ConflictError, NotFoundError, ValidationError, ForbiddenError } = require('../utils/errors');
|
||||
|
||||
/**
|
||||
* Create a new admin user invitation
|
||||
@@ -567,6 +567,263 @@ async function validateInvitationToken(token) {
|
||||
return invitation || null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Role management (the role editor). Highly privileged — every mutation here is
|
||||
// gated by `roles.manage` at the route layer. See project_permission_gating.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// System roles that ship with the app. Their `name` (the semantic key routes
|
||||
// check) is immutable and they cannot be deleted; their display/description and
|
||||
// (except super_admin) their permission set may be tweaked.
|
||||
const RESERVED_ROLE_NAMES = ['super_admin', 'admin', 'editor', 'viewer', 'solo_photographer', 'team_photographer'];
|
||||
|
||||
function clearPermCache() {
|
||||
// Bust the RBAC middleware cache so grant changes take effect immediately
|
||||
// rather than waiting out its 60s TTL. Lazy-required to avoid a load cycle.
|
||||
try { require('../middleware/permissions').clearPermissionCache(); } catch (_) { /* optional */ }
|
||||
}
|
||||
|
||||
function normalizeRoleName(name) {
|
||||
return String(name || '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
async function resolvePermissionIds(permissionNames) {
|
||||
const names = Array.from(new Set((permissionNames || []).filter(Boolean)));
|
||||
if (names.length === 0) return [];
|
||||
const rows = await db('permissions').whereIn('name', names).select('id', 'name');
|
||||
const found = new Set(rows.map((r) => r.name));
|
||||
const missing = names.filter((n) => !found.has(n));
|
||||
if (missing.length > 0) {
|
||||
throw new ValidationError(`Unknown permission(s): ${missing.join(', ')}`);
|
||||
}
|
||||
return rows.map((r) => r.id);
|
||||
}
|
||||
|
||||
// Contain the roles.manage blast radius (delegation, not root escalation): a
|
||||
// non-super_admin managing roles may only grant permissions their OWN role
|
||||
// already holds, so `roles.manage` can't be turned into "grant myself
|
||||
// everything". super_admin bypasses (it holds the full catalog anyway).
|
||||
async function assertActorMayGrant(actorId, permissionNames) {
|
||||
const names = Array.from(new Set((permissionNames || []).filter(Boolean)));
|
||||
if (names.length === 0) return;
|
||||
const actor = await db('admin_users')
|
||||
.leftJoin('roles', 'roles.id', 'admin_users.role_id')
|
||||
.where('admin_users.id', actorId)
|
||||
.select('roles.name as role_name')
|
||||
.first();
|
||||
if (actor && actor.role_name === 'super_admin') return;
|
||||
const { userHasAllPermissions } = require('../middleware/permissions');
|
||||
if (!(await userHasAllPermissions(actorId, names))) {
|
||||
throw new ForbiddenError('You can only grant permissions your own role already holds.');
|
||||
}
|
||||
}
|
||||
|
||||
async function getRoleWithPermissionsById(id) {
|
||||
const role = await db('roles')
|
||||
.where('id', id)
|
||||
.select('id', 'name', 'display_name', 'description', 'is_system', 'priority')
|
||||
.first();
|
||||
if (!role) return null;
|
||||
const permissions = await db('role_permissions')
|
||||
.join('permissions', 'permissions.id', 'role_permissions.permission_id')
|
||||
.where('role_permissions.role_id', id)
|
||||
.pluck('permissions.name');
|
||||
return { ...role, permissions: permissions.sort() };
|
||||
}
|
||||
|
||||
/**
|
||||
* All roles with their permission-name arrays and assigned-user counts. Backs
|
||||
* the role-editor list.
|
||||
*/
|
||||
async function getRolesWithPermissions() {
|
||||
const roles = await db('roles')
|
||||
.select('id', 'name', 'display_name', 'description', 'is_system', 'priority')
|
||||
.orderBy('priority', 'desc');
|
||||
const grants = await db('role_permissions')
|
||||
.join('permissions', 'permissions.id', 'role_permissions.permission_id')
|
||||
.select('role_permissions.role_id as role_id', 'permissions.name as name');
|
||||
const userCounts = await db('admin_users')
|
||||
.whereNotNull('role_id')
|
||||
.select('role_id')
|
||||
.count('* as count')
|
||||
.groupBy('role_id');
|
||||
|
||||
const permsByRole = new Map();
|
||||
for (const g of grants) {
|
||||
if (!permsByRole.has(g.role_id)) permsByRole.set(g.role_id, []);
|
||||
permsByRole.get(g.role_id).push(g.name);
|
||||
}
|
||||
const countByRole = new Map(userCounts.map((r) => [r.role_id, Number(r.count)]));
|
||||
|
||||
return roles.map((r) => ({
|
||||
...r,
|
||||
permissions: (permsByRole.get(r.id) || []).sort(),
|
||||
user_count: countByRole.get(r.id) || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* The full permission catalog (for the editor's matrix), ordered by category.
|
||||
*/
|
||||
async function getPermissionCatalog() {
|
||||
return db('permissions')
|
||||
.select('id', 'name', 'display_name', 'category', 'description')
|
||||
.orderBy(['category', 'name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a custom role with an explicit permission set.
|
||||
*/
|
||||
async function createRole({ name, displayName, description, priority, permissions }, createdById) {
|
||||
const normalized = normalizeRoleName(name);
|
||||
if (!/^[a-z][a-z0-9_]{1,48}$/.test(normalized)) {
|
||||
throw new ValidationError('Role name must be lowercase letters, numbers and underscores (2–49 chars, starting with a letter).');
|
||||
}
|
||||
if (RESERVED_ROLE_NAMES.includes(normalized)) {
|
||||
throw new ConflictError(`"${normalized}" is a reserved system role name.`);
|
||||
}
|
||||
const existing = await db('roles').where('name', normalized).first();
|
||||
if (existing) throw new ConflictError(`A role named "${normalized}" already exists.`);
|
||||
|
||||
await assertActorMayGrant(createdById, permissions);
|
||||
const permIds = await resolvePermissionIds(permissions);
|
||||
const rawPriority = Number(priority);
|
||||
const safePriority = Number.isFinite(rawPriority) ? Math.max(0, Math.min(99, Math.floor(rawPriority))) : 50;
|
||||
|
||||
let roleId;
|
||||
await db.transaction(async (trx) => {
|
||||
const ins = await trx('roles').insert({
|
||||
name: normalized,
|
||||
display_name: displayName || normalized,
|
||||
description: description || null,
|
||||
is_system: false,
|
||||
priority: safePriority,
|
||||
created_at: trx.fn.now(),
|
||||
updated_at: trx.fn.now(),
|
||||
}).returning('id');
|
||||
roleId = ins[0]?.id ?? ins[0];
|
||||
if (permIds.length > 0) {
|
||||
await trx('role_permissions').insert(permIds.map((pid) => ({ role_id: roleId, permission_id: pid })));
|
||||
}
|
||||
});
|
||||
|
||||
clearPermCache();
|
||||
// logActivity AFTER commit — a global-db write inside the trx would deadlock
|
||||
// on SQLite. See feedback_sqlite_global_write_in_transaction.
|
||||
await logActivity('admin_role_created',
|
||||
{ roleId, name: normalized, permissionCount: permIds.length },
|
||||
null,
|
||||
{ type: 'admin', id: createdById, name: 'system' });
|
||||
logger.info('Role created', { roleId, name: normalized, createdById });
|
||||
return getRoleWithPermissionsById(roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a role's display/description/priority and/or replace its permission
|
||||
* set. super_admin is fully protected; system roles keep their name + priority.
|
||||
*/
|
||||
async function updateRole(id, { displayName, description, priority, permissions }, updatedById) {
|
||||
const role = await db('roles').where('id', id).first();
|
||||
if (!role) throw new NotFoundError('Role', id);
|
||||
if (role.name === 'super_admin') {
|
||||
throw new ValidationError('The Super Admin role is protected — it always holds every permission and cannot be edited.');
|
||||
}
|
||||
|
||||
// Self-amplification guard: a non-super_admin can't edit their OWN role (which
|
||||
// would let a roles.manage holder grant their own role more), and can only
|
||||
// grant permissions they already hold. See assertActorMayGrant.
|
||||
const actor = await db('admin_users')
|
||||
.leftJoin('roles', 'roles.id', 'admin_users.role_id')
|
||||
.where('admin_users.id', updatedById)
|
||||
.select('admin_users.role_id as role_id', 'roles.name as role_name')
|
||||
.first();
|
||||
const actorIsSuper = actor && actor.role_name === 'super_admin';
|
||||
if (!actorIsSuper && actor && actor.role_id === Number(id)) {
|
||||
throw new ForbiddenError('You cannot edit your own role.');
|
||||
}
|
||||
if (permissions !== undefined) {
|
||||
await assertActorMayGrant(updatedById, permissions);
|
||||
}
|
||||
|
||||
const patch = { updated_at: db.fn.now() };
|
||||
if (displayName !== undefined) patch.display_name = displayName;
|
||||
if (description !== undefined) patch.description = description;
|
||||
if (priority !== undefined && !role.is_system) {
|
||||
const p = Number(priority);
|
||||
if (Number.isFinite(p)) patch.priority = Math.max(0, Math.min(99, Math.floor(p)));
|
||||
}
|
||||
|
||||
let permIds = null;
|
||||
if (permissions !== undefined) {
|
||||
permIds = await resolvePermissionIds(permissions);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx('roles').where('id', id).update(patch);
|
||||
if (permIds !== null) {
|
||||
await trx('role_permissions').where('role_id', id).del();
|
||||
if (permIds.length > 0) {
|
||||
await trx('role_permissions').insert(permIds.map((pid) => ({ role_id: id, permission_id: pid })));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clearPermCache();
|
||||
await logActivity('admin_role_updated',
|
||||
{ roleId: id, name: role.name, permissionsChanged: permIds !== null },
|
||||
null,
|
||||
{ type: 'admin', id: updatedById, name: 'system' });
|
||||
logger.info('Role updated', { roleId: id, name: role.name, updatedById });
|
||||
return getRoleWithPermissionsById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a custom role. System roles are protected; a role still assigned to
|
||||
* users must be reassigned first (avoids leaving users permission-less).
|
||||
*/
|
||||
async function deleteRole(id, deletedById) {
|
||||
const role = await db('roles').where('id', id).first();
|
||||
if (!role) throw new NotFoundError('Role', id);
|
||||
if (role.is_system) throw new ValidationError('System roles cannot be deleted.');
|
||||
|
||||
const assigned = await db('admin_users').where('role_id', id).count('* as count').first();
|
||||
if (Number(assigned?.count) > 0) {
|
||||
throw new ConflictError('Reassign the users holding this role before deleting it.');
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx('role_permissions').where('role_id', id).del();
|
||||
await trx('roles').where('id', id).del();
|
||||
});
|
||||
|
||||
clearPermCache();
|
||||
await logActivity('admin_role_deleted',
|
||||
{ roleId: id, name: role.name },
|
||||
null,
|
||||
{ type: 'admin', id: deletedById, name: 'system' });
|
||||
logger.info('Role deleted', { roleId: id, name: role.name, deletedById });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone any role (including a preset) into a new custom role with the same
|
||||
* permission set — the "start from a preset" flow.
|
||||
*/
|
||||
async function cloneRole(sourceId, { name, displayName, description }, createdById) {
|
||||
const source = await db('roles').where('id', sourceId).first();
|
||||
if (!source) throw new NotFoundError('Role', sourceId);
|
||||
const permissions = await db('role_permissions')
|
||||
.join('permissions', 'permissions.id', 'role_permissions.permission_id')
|
||||
.where('role_permissions.role_id', sourceId)
|
||||
.pluck('permissions.name');
|
||||
return createRole({
|
||||
name,
|
||||
displayName: displayName || `${source.display_name} copy`,
|
||||
description: description !== undefined ? description : source.description,
|
||||
priority: source.is_system ? 50 : source.priority,
|
||||
permissions,
|
||||
}, createdById);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createInvitation,
|
||||
acceptInvitation,
|
||||
@@ -578,6 +835,13 @@ module.exports = {
|
||||
deleteAdminUser,
|
||||
resetAdminPassword,
|
||||
getAllRoles,
|
||||
getRolesWithPermissions,
|
||||
getPermissionCatalog,
|
||||
getRoleWithPermissionsById,
|
||||
createRole,
|
||||
updateRole,
|
||||
deleteRole,
|
||||
cloneRole,
|
||||
getPendingInvitations,
|
||||
cancelInvitation,
|
||||
validateInvitationToken
|
||||
|
||||
Reference in New Issue
Block a user