Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app/app/(admin)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<ReturnType<typeof getEntityProfile>>;
try {
profile = await getEntityProfile(session.entityId);
} catch {
redirect("/login");
redirect(REAUTH_PATH);
}
if (!profile) {
redirect("/login");
redirect(REAUTH_PATH);
}

return (
Expand Down
22 changes: 15 additions & 7 deletions app/components/dashboard/dashboard-overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down Expand Up @@ -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 }
}
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -804,12 +811,13 @@ function topCounts<T extends Record<string, unknown>>(
}

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),
};
}

Expand Down
54 changes: 47 additions & 7 deletions app/proxy.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
30 changes: 0 additions & 30 deletions migrations/001_initial.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Comment on lines 765 to 770

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add a new migration instead of editing 001

Because startup runs sqlx::migrate::Migrator::run from src/main.rs, any database that has already applied migration version 1 will have the old checksum recorded in _sqlx_migrations; changing 001_initial.sql in place makes upgrades fail migration validation before the service starts. Put these seed-data removals in a new migrations/NNN_*.sql migration instead of rewriting the applied initial migration.

Useful? React with 👍 / 👎.

Comment on lines 765 to 770

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve generic resource applicability

After removing the resource-specific rows here, there is no action_applicability entry for read/write/delete on resource at all. The resource listing path (resources query via authorized_resource_ids) only returns objects when the requested read action has a matching resource applicability row, so fresh databases will show zero resources and PDP checks for read on resources report the action as unknown even for admins. Replace the MG-specific types with a generic ('resource', NULL) entry for these actions if resources are still supported.

Useful? React with 👍 / 👎.

;

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
Expand Down Expand Up @@ -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),
Expand Down
Loading