Docker images / prepare-release (push) Successful in 5s
Docker images / backend-image (push) Successful in 2m52s
Docker images / frontend-image (push) Successful in 2m23s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 38s
Docker images / publish-release (push) Successful in 8s
JWT на backend, handoff/callback на UI, RBAC mm:*, AUTH_* в compose. Co-authored-by: Cursor <cursoragent@cursor.com>
174 lines
4.3 KiB
TypeScript
174 lines
4.3 KiB
TypeScript
/**
|
|
* Portal JWT RBAC helpers (mirrors @authportal/shared hasPermission).
|
|
* Format: mm:<section>:<read|write|admin>
|
|
*/
|
|
|
|
export type AuthUser = {
|
|
id: string
|
|
email: string
|
|
name: string
|
|
apps: string[]
|
|
permissions: string[]
|
|
isAdmin?: boolean
|
|
}
|
|
|
|
export function hasPermission(
|
|
granted: readonly string[],
|
|
required: string,
|
|
): boolean {
|
|
if (granted.includes(required)) return true
|
|
const parts = required.split(":")
|
|
if (parts.length !== 3) return false
|
|
const [app, section, action] = parts
|
|
if (action === "read") {
|
|
return (
|
|
granted.includes(`${app}:${section}:write`) ||
|
|
granted.includes(`${app}:${section}:admin`)
|
|
)
|
|
}
|
|
if (action === "write") {
|
|
return granted.includes(`${app}:${section}:admin`)
|
|
}
|
|
return false
|
|
}
|
|
|
|
type Rule = {
|
|
methods: string[]
|
|
match: (path: string) => boolean
|
|
permission: string
|
|
}
|
|
|
|
const RULES: Rule[] = [
|
|
{
|
|
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
match: (p) =>
|
|
p.startsWith("/api/system") ||
|
|
p.startsWith("/api/scheduler") ||
|
|
p.startsWith("/api/evobgp"),
|
|
permission: "mm:settings:admin",
|
|
},
|
|
{
|
|
methods: ["GET"],
|
|
match: (p) =>
|
|
p.startsWith("/api/sidebar-counts") || p.startsWith("/api/events"),
|
|
permission: "mm:dashboard:read",
|
|
},
|
|
{
|
|
methods: ["GET"],
|
|
match: (p) => p.startsWith("/api/servers"),
|
|
permission: "mm:servers:read",
|
|
},
|
|
{
|
|
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
match: (p) => p.startsWith("/api/servers"),
|
|
permission: "mm:servers:write",
|
|
},
|
|
{
|
|
methods: ["GET"],
|
|
match: (p) => p.startsWith("/api/filters"),
|
|
permission: "mm:filters:read",
|
|
},
|
|
{
|
|
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
match: (p) => p.startsWith("/api/filters"),
|
|
permission: "mm:filters:write",
|
|
},
|
|
{
|
|
methods: ["GET"],
|
|
match: (p) => p.startsWith("/api/bgp"),
|
|
permission: "mm:bgp:read",
|
|
},
|
|
{
|
|
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
match: (p) => p.startsWith("/api/bgp"),
|
|
permission: "mm:bgp:write",
|
|
},
|
|
{
|
|
methods: ["GET"],
|
|
match: (p) => p.startsWith("/api/uptime"),
|
|
permission: "mm:uptime:read",
|
|
},
|
|
{
|
|
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
match: (p) => p.startsWith("/api/uptime"),
|
|
permission: "mm:uptime:write",
|
|
},
|
|
{
|
|
methods: ["GET"],
|
|
match: (p) => p.startsWith("/api/traffic"),
|
|
permission: "mm:traffic:read",
|
|
},
|
|
{
|
|
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
match: (p) => p.startsWith("/api/traffic"),
|
|
permission: "mm:traffic:write",
|
|
},
|
|
{
|
|
methods: ["GET"],
|
|
match: (p) => p.startsWith("/api/alerts"),
|
|
permission: "mm:alerts:read",
|
|
},
|
|
{
|
|
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
match: (p) => p.startsWith("/api/alerts"),
|
|
permission: "mm:alerts:write",
|
|
},
|
|
{
|
|
methods: ["GET"],
|
|
match: (p) => p.startsWith("/api/backups"),
|
|
permission: "mm:backups:read",
|
|
},
|
|
{
|
|
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
match: (p) => p.startsWith("/api/backups"),
|
|
permission: "mm:backups:write",
|
|
},
|
|
{
|
|
methods: ["GET"],
|
|
match: (p) => p.startsWith("/api/certificates"),
|
|
permission: "mm:certificates:read",
|
|
},
|
|
{
|
|
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
match: (p) => p.startsWith("/api/certificates"),
|
|
permission: "mm:certificates:write",
|
|
},
|
|
{
|
|
methods: ["GET"],
|
|
match: (p) =>
|
|
p.startsWith("/api/network") ||
|
|
p.startsWith("/api/ospf") ||
|
|
p.startsWith("/api/recursive") ||
|
|
p.startsWith("/api/probes") ||
|
|
p.startsWith("/api/internet-path") ||
|
|
p.startsWith("/api/exec"),
|
|
permission: "mm:network:read",
|
|
},
|
|
{
|
|
methods: ["POST", "PUT", "PATCH", "DELETE"],
|
|
match: (p) =>
|
|
p.startsWith("/api/network") ||
|
|
p.startsWith("/api/ospf") ||
|
|
p.startsWith("/api/recursive") ||
|
|
p.startsWith("/api/probes") ||
|
|
p.startsWith("/api/internet-path") ||
|
|
p.startsWith("/api/exec"),
|
|
permission: "mm:network:write",
|
|
},
|
|
]
|
|
|
|
/** Resolve required permission for method+path, or null if public / unknown. */
|
|
export function permissionForRequest(
|
|
method: string,
|
|
path: string,
|
|
): string | null {
|
|
const m = method.toUpperCase()
|
|
const pathname = path.split("?")[0] ?? path
|
|
for (const rule of RULES) {
|
|
if (!rule.methods.includes(m)) continue
|
|
if (rule.match(pathname)) return rule.permission
|
|
}
|
|
if (pathname.startsWith("/api/")) return "mm:dashboard:read"
|
|
return null
|
|
}
|