Require at least one capability to see/access apps that define them
An app-level grant (user_app_perms/role_app_perms) alone used to be enough to show an app's portal tile and pass AuthGate, even with zero capabilities held. Most apps only gate specific routes/buttons with capabilities, not their dashboard/shell, so a role with the app toggled on but no caps granted (e.g. wages, utilities, hvac, plant, tablet-checkin — none of which auto-grant Staff caps) still saw the full app. Apps with no capabilities defined at all keep prior behaviour.
This commit is contained in:
parent
f756a600b1
commit
97a0b50c92
1 changed files with 41 additions and 8 deletions
|
|
@ -79,6 +79,31 @@ export function cookieOpts(request, clear = false) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An app-level grant (user_app_perms / role_app_perms) alone used to be enough
|
||||||
|
// to show an app's tile and let AuthGate in — but most apps only enforce their
|
||||||
|
// granular capabilities on specific routes/buttons, not on the dashboard/shell
|
||||||
|
// itself. That meant a role with the app toggled on and zero capabilities still
|
||||||
|
// saw the full app. For any app that defines capabilities at all, require the
|
||||||
|
// user to hold at least one of them; apps with no capabilities defined yet keep
|
||||||
|
// the old app-perm-only behaviour unchanged.
|
||||||
|
function CAP_GATE_CLAUSE(appIdExpr, userIdParam) {
|
||||||
|
return `(
|
||||||
|
NOT EXISTS (SELECT 1 FROM app_capabilities ac WHERE ac.app_id = ${appIdExpr})
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM app_capabilities ac
|
||||||
|
WHERE ac.app_id = ${appIdExpr}
|
||||||
|
AND (
|
||||||
|
EXISTS (SELECT 1 FROM user_capabilities uc WHERE uc.user_id = ${userIdParam} AND uc.capability_id = ac.id)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM user_roles ur2
|
||||||
|
JOIN role_capabilities rc ON rc.role_id = ur2.role_id
|
||||||
|
WHERE ur2.user_id = ${userIdParam} AND rc.capability_id = ac.id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)`
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve a user's granular capabilities as "<app_slug>:<cap_slug>" strings.
|
// Resolve a user's granular capabilities as "<app_slug>:<cap_slug>" strings.
|
||||||
// Admins implicitly hold every capability of every active app; everyone else
|
// Admins implicitly hold every capability of every active app; everyone else
|
||||||
// gets the union of capabilities granted via their roles and direct grants.
|
// gets the union of capabilities granted via their roles and direct grants.
|
||||||
|
|
@ -136,6 +161,7 @@ export async function getUserWithApps(userId) {
|
||||||
WHERE ur.user_id = $1 AND rap.app_id = a.id
|
WHERE ur.user_id = $1 AND rap.app_id = a.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
AND ${CAP_GATE_CLAUSE('a.id', '$1')}
|
||||||
ORDER BY a.category NULLS FIRST, a.name`,
|
ORDER BY a.category NULLS FIRST, a.name`,
|
||||||
[userId]
|
[userId]
|
||||||
)
|
)
|
||||||
|
|
@ -218,6 +244,7 @@ export async function authRoutes(app) {
|
||||||
const { app: appSlug } = request.query
|
const { app: appSlug } = request.query
|
||||||
|
|
||||||
// Live DB check: user must be active and (if app requested) have permission
|
// Live DB check: user must be active and (if app requested) have permission
|
||||||
|
// AND (for apps that define capabilities) hold at least one of them.
|
||||||
const { rows: [user] } = await pool.query(
|
const { rows: [user] } = await pool.query(
|
||||||
`SELECT u.id, u.email, u.name, u.is_admin, u.offsite_allowed
|
`SELECT u.id, u.email, u.name, u.is_admin, u.offsite_allowed
|
||||||
FROM users u
|
FROM users u
|
||||||
|
|
@ -225,13 +252,18 @@ export async function authRoutes(app) {
|
||||||
AND (
|
AND (
|
||||||
$2::text IS NULL
|
$2::text IS NULL
|
||||||
OR u.is_admin = true
|
OR u.is_admin = true
|
||||||
OR EXISTS (SELECT 1 FROM user_app_perms p
|
OR (
|
||||||
JOIN apps a ON a.id = p.app_id
|
(
|
||||||
WHERE p.user_id = u.id AND a.slug = $2 AND a.active = true)
|
EXISTS (SELECT 1 FROM user_app_perms p
|
||||||
OR EXISTS (SELECT 1 FROM user_roles ur
|
JOIN apps a ON a.id = p.app_id
|
||||||
JOIN role_app_perms rap ON rap.role_id = ur.role_id
|
WHERE p.user_id = u.id AND a.slug = $2 AND a.active = true)
|
||||||
JOIN apps a ON a.id = rap.app_id
|
OR EXISTS (SELECT 1 FROM user_roles ur
|
||||||
WHERE ur.user_id = u.id AND a.slug = $2 AND a.active = true)
|
JOIN role_app_perms rap ON rap.role_id = ur.role_id
|
||||||
|
JOIN apps a ON a.id = rap.app_id
|
||||||
|
WHERE ur.user_id = u.id AND a.slug = $2 AND a.active = true)
|
||||||
|
)
|
||||||
|
AND ${CAP_GATE_CLAUSE('(SELECT id FROM apps WHERE slug = $2)', 'u.id')}
|
||||||
|
)
|
||||||
)`,
|
)`,
|
||||||
[payload.user_id, appSlug || null]
|
[payload.user_id, appSlug || null]
|
||||||
)
|
)
|
||||||
|
|
@ -282,7 +314,8 @@ export async function authRoutes(app) {
|
||||||
JOIN role_app_perms rap ON rap.role_id = ur.role_id
|
JOIN role_app_perms rap ON rap.role_id = ur.role_id
|
||||||
WHERE ur.user_id = $1 AND rap.app_id = a.id
|
WHERE ur.user_id = $1 AND rap.app_id = a.id
|
||||||
)
|
)
|
||||||
)`,
|
)
|
||||||
|
AND ${CAP_GATE_CLAUSE('a.id', '$1')}`,
|
||||||
[user.id]
|
[user.id]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue