diff --git a/app/app/(admin)/layout.tsx b/app/app/(admin)/layout.tsx index 73583fe..9d0cf5f 100644 --- a/app/app/(admin)/layout.tsx +++ b/app/app/(admin)/layout.tsx @@ -8,6 +8,7 @@ import { import { getEntityProfile } from "@/lib/entity/profile"; export const dynamic = "force-dynamic"; +const REAUTH_PATH = "/login?reauth=1"; export default async function AdminLayout({ children, @@ -17,17 +18,17 @@ export default async function AdminLayout({ const session = await getServerSession(); const token = await getServerToken(); if (!session || !token || isExpired(session.expiresAt)) { - redirect("/login"); + redirect(REAUTH_PATH); } let profile: Awaited>; try { profile = await getEntityProfile(session.entityId); } catch { - redirect("/login"); + redirect(REAUTH_PATH); } if (!profile) { - redirect("/login"); + redirect(REAUTH_PATH); } return ( diff --git a/app/components/dashboard/dashboard-overview.tsx b/app/components/dashboard/dashboard-overview.tsx index e5fc61b..60b147d 100644 --- a/app/components/dashboard/dashboard-overview.tsx +++ b/app/components/dashboard/dashboard-overview.tsx @@ -86,7 +86,9 @@ type RiskResponse = { }; type PostureResponse = { - policies: CountWithItems<{ effect: string; scopeKind: string }>; + policies: CountWithItems<{ + permissionBlock: { effect: string; scopeKind: string }; + }>; authzAllowed: Count; authzDenied: Count; }; @@ -130,7 +132,7 @@ const SUMMARY_QUERY = ` groupsActive: groups(status: active, limit: 1, offset: 0) { total } groupsInactive: groups(status: inactive, limit: 1, offset: 0) { total } resources(limit: 1, offset: 0) { total } - policies(limit: 1, offset: 0) { total } + policies: directPolicies(limit: 1, offset: 0) { total } roles(limit: 1, offset: 0) { total } auditLogs(limit: 1, offset: 0) { total } } @@ -166,9 +168,14 @@ const RISK_QUERY = ` const POSTURE_QUERY = ` query DashboardPosture { - policies(limit: 200, offset: 0) { + policies: directPolicies(limit: 200, offset: 0) { total - items { effect scopeKind } + items { + permissionBlock { + effect + scopeKind: scopeMode + } + } } authzAllowed: auditLogs(event: "authz.check", outcome: allow, limit: 1, offset: 0) { total } authzDenied: auditLogs(event: "authz.check", outcome: deny, limit: 1, offset: 0) { total } @@ -804,12 +811,13 @@ function topCounts>( } function policyStats(items: PostureResponse["policies"]["items"]) { - const allow = items.filter((item) => item.effect === "allow").length; - const deny = items.filter((item) => item.effect === "deny").length; + const blocks = items.map((item) => item.permissionBlock); + const allow = blocks.filter((block) => block.effect === "allow").length; + const deny = blocks.filter((block) => block.effect === "deny").length; return { allow, deny, - scopes: topCounts(items, "scopeKind", 5), + scopes: topCounts(blocks, "scopeKind", 5), }; } diff --git a/app/proxy.ts b/app/proxy.ts index 7945826..9eaf11c 100644 --- a/app/proxy.ts +++ b/app/proxy.ts @@ -1,27 +1,67 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; -import { AUTH_COOKIE } from "@/lib/auth/constants"; +import { AUTH_COOKIE, AUTH_META_COOKIE } from "@/lib/auth/constants"; const REDIRECT_TO_LOGIN = new Set(["/verify-email", "/callback"]); const PUBLIC_PAGES = new Set(["/login", "/register"]); +const REAUTH_PARAM = "reauth"; + +function isFreshSessionMeta(raw: string | undefined) { + if (!raw) return false; + + try { + const parsed = JSON.parse(raw) as { expiresAt?: unknown }; + if (typeof parsed.expiresAt !== "string") return false; + + const expiresAt = Date.parse(parsed.expiresAt); + return !Number.isNaN(expiresAt) && expiresAt > Date.now(); + } catch { + return false; + } +} + +function authState(request: NextRequest) { + const token = request.cookies.get(AUTH_COOKIE)?.value; + const session = request.cookies.get(AUTH_META_COOKIE)?.value; + const valid = Boolean(token && isFreshSessionMeta(session)); + + return { + valid, + stale: Boolean((token || session) && !valid), + }; +} + +function clearAuthCookies(response: NextResponse) { + response.cookies.delete(AUTH_COOKIE); + response.cookies.delete(AUTH_META_COOKIE); + return response; +} export function proxy(request: NextRequest) { const { pathname } = request.nextUrl; + const auth = authState(request); + const reauth = request.nextUrl.searchParams.has(REAUTH_PARAM); if (REDIRECT_TO_LOGIN.has(pathname)) { - return NextResponse.redirect(new URL("/login", request.url)); + return clearAuthCookies( + NextResponse.redirect(new URL(`/login?${REAUTH_PARAM}=1`, request.url)), + ); } - const token = request.cookies.get(AUTH_COOKIE)?.value; - - if (PUBLIC_PAGES.has(pathname) && token) { + if (PUBLIC_PAGES.has(pathname) && auth.valid && !reauth) { return NextResponse.redirect(new URL("/dashboard", request.url)); } - if (!PUBLIC_PAGES.has(pathname) && !token) { + if (PUBLIC_PAGES.has(pathname)) { + const response = NextResponse.next(); + return auth.stale || reauth ? clearAuthCookies(response) : response; + } + + if (!auth.valid) { const url = new URL("/login", request.url); url.searchParams.set("next", pathname); - return NextResponse.redirect(url); + const response = NextResponse.redirect(url); + return auth.stale ? clearAuthCookies(response) : response; } return NextResponse.next(); diff --git a/migrations/001_initial.sql b/migrations/001_initial.sql index b05fd40..f11ed1f 100644 --- a/migrations/001_initial.sql +++ b/migrations/001_initial.sql @@ -765,39 +765,11 @@ CROSS JOIN LATERAL ( ('entity', 'entity:service'), ('entity', 'entity:workload'), ('entity', 'entity:application'), - ('resource', 'resource:channel'), - ('resource', 'resource:rule'), - ('resource', 'resource:report'), - ('resource', 'resource:alarm'), ('group', NULL) ) AS applicability(object_kind, object_type) WHERE actions.name IN ('read', 'write', 'delete') ; -INSERT INTO action_applicability (action_id, object_kind, object_type) -SELECT id, 'resource', 'resource:channel' -FROM actions -WHERE name IN ('read', 'write', 'delete', 'manage', 'publish', 'subscribe') -ON CONFLICT DO NOTHING; - -INSERT INTO action_applicability (action_id, object_kind, object_type) -SELECT id, 'resource', 'resource:rule' -FROM actions -WHERE name IN ('read', 'write', 'delete', 'manage', 'execute') -ON CONFLICT DO NOTHING; - -INSERT INTO action_applicability (action_id, object_kind, object_type) -SELECT id, 'resource', 'resource:report' -FROM actions -WHERE name IN ('read', 'write', 'delete', 'manage', 'execute') -ON CONFLICT DO NOTHING; - -INSERT INTO action_applicability (action_id, object_kind, object_type) -SELECT id, 'resource', 'resource:alarm' -FROM actions -WHERE name IN ('read', 'write', 'delete', 'manage') -ON CONFLICT DO NOTHING; - INSERT INTO action_applicability (action_id, object_kind, object_type) SELECT id, object_kind, object_type FROM actions @@ -965,8 +937,6 @@ VALUES ('device', 'manage', 'resource', NULL, 'deny', TRUE), ('device', 'delete', 'resource', NULL, 'deny', TRUE), ('device', 'write', 'resource', NULL, 'deny', TRUE), - ('device', 'publish', 'resource', 'resource:channel', 'allow', FALSE), - ('device', 'subscribe', 'resource', 'resource:channel', 'allow', FALSE), ('human', 'manage', 'resource', NULL, 'allow', FALSE), ('human', 'manage', 'entity', NULL, 'allow', FALSE), ('human', 'manage', 'group', NULL, 'allow', FALSE),