fix(launchpad): populate fallback domains for apps, seed default app domains, and add app admin mutations
This commit is contained in:
parent
64f758e7cb
commit
9c2aa73fdd
@ -308,18 +308,18 @@ export async function initDb(): Promise<void> {
|
||||
);
|
||||
`;
|
||||
|
||||
// Seed ed-droid app record with spiffe_id
|
||||
// Seed ed-droid app record with spiffe_id and domain
|
||||
await sql`
|
||||
INSERT INTO apps (name, spiffe_id)
|
||||
VALUES ('ed-droid', 'spiffe://system.local/ed-droid-backend')
|
||||
ON CONFLICT (spiffe_id) DO NOTHING
|
||||
INSERT INTO apps (name, spiffe_id, domain, description)
|
||||
VALUES ('ed-droid', 'spiffe://system.local/ed-droid-backend', 'ed-droid.atyg.org', 'Autonomous Agent UI & Workload Control')
|
||||
ON CONFLICT (spiffe_id) DO UPDATE SET domain = COALESCE(apps.domain, EXCLUDED.domain), description = COALESCE(apps.description, EXCLUDED.description)
|
||||
`;
|
||||
|
||||
// Seed the Central Auth-Yes Management App for global administration
|
||||
await sql`
|
||||
INSERT INTO apps (name, spiffe_id)
|
||||
VALUES ('Auth-Yes Management Console', 'spiffe://system.local/auth-yes-management')
|
||||
ON CONFLICT (spiffe_id) DO NOTHING
|
||||
INSERT INTO apps (name, spiffe_id, domain, description)
|
||||
VALUES ('Auth-Yes Management Console', 'spiffe://system.local/auth-yes-management', 'auth.atyg.org', 'Central Identity, Delegation, and Zero-Trust Control Fabric')
|
||||
ON CONFLICT (spiffe_id) DO UPDATE SET domain = COALESCE(apps.domain, EXCLUDED.domain), description = COALESCE(apps.description, EXCLUDED.description)
|
||||
`;
|
||||
|
||||
// Seed initial bootstrap admin invite if no users exist in the database
|
||||
|
||||
@ -7,9 +7,11 @@ import { getClientIp } from "../../core/middleware.ts";
|
||||
import {
|
||||
addAaguid,
|
||||
assignUserGrant,
|
||||
createApp,
|
||||
createInvite,
|
||||
createRecoveryLink,
|
||||
createRole,
|
||||
deleteApp,
|
||||
deleteRole,
|
||||
getUserById,
|
||||
removeAaguid,
|
||||
@ -18,6 +20,7 @@ import {
|
||||
revokeInvite,
|
||||
revokeSessionById,
|
||||
revokeUserPasskey,
|
||||
updateApp,
|
||||
updateRole,
|
||||
updateUserProfile,
|
||||
updateUserStatus,
|
||||
@ -280,3 +283,108 @@ adminActionsRoutes.delete("/aaguid/:id", async (c) => {
|
||||
await removeAaguid(id);
|
||||
return c.json({ success: true });
|
||||
});
|
||||
|
||||
// App Registry Mutations
|
||||
adminActionsRoutes.post("/apps", async (c) => {
|
||||
const auth = await getAuthenticatedUser(c);
|
||||
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
||||
|
||||
const {
|
||||
name,
|
||||
spiffeId,
|
||||
description,
|
||||
domain,
|
||||
is_public,
|
||||
bypass_paths,
|
||||
allowed_cidrs,
|
||||
} = await c.req.json();
|
||||
|
||||
if (!name || !spiffeId) {
|
||||
return c.json({ error: "Name and SPIFFE ID are required" }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const newApp = await createApp(
|
||||
name,
|
||||
spiffeId,
|
||||
description,
|
||||
domain,
|
||||
is_public,
|
||||
bypass_paths,
|
||||
allowed_cidrs,
|
||||
);
|
||||
|
||||
auditWrapper.auditLog(auth.userId, "app_registered", newApp.id, {
|
||||
name: newApp.name,
|
||||
spiffe_id: newApp.spiffe_id,
|
||||
}, getClientIp(c));
|
||||
return c.json({ success: true, app: newApp });
|
||||
} catch (err: any) {
|
||||
if (err.code === "23505") {
|
||||
return c.json({
|
||||
error: "An application with this SPIFFE ID already exists",
|
||||
}, 409);
|
||||
}
|
||||
return c.json({ error: "Failed to register application" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
adminActionsRoutes.put("/apps/:id", async (c) => {
|
||||
const auth = await getAuthenticatedUser(c);
|
||||
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
||||
|
||||
const appId = c.req.param("id");
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
domain,
|
||||
is_public,
|
||||
bypass_paths,
|
||||
allowed_cidrs,
|
||||
} = await c.req.json();
|
||||
|
||||
if (!name) {
|
||||
return c.json({ error: "Application name is required" }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedApp = await updateApp(
|
||||
appId,
|
||||
name,
|
||||
description,
|
||||
domain,
|
||||
is_public,
|
||||
bypass_paths,
|
||||
allowed_cidrs,
|
||||
);
|
||||
|
||||
if (!updatedApp) return c.json({ error: "Application not found" }, 404);
|
||||
|
||||
auditWrapper.auditLog(auth.userId, "app_updated", updatedApp.id, {
|
||||
name: updatedApp.name,
|
||||
}, getClientIp(c));
|
||||
|
||||
return c.json({ success: true, app: updatedApp });
|
||||
} catch (_err: any) {
|
||||
return c.json({ error: "Failed to update application" }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
adminActionsRoutes.delete("/apps/:id", async (c) => {
|
||||
const auth = await getAuthenticatedUser(c);
|
||||
if (!auth) return c.json({ error: "Unauthorized" }, 401);
|
||||
|
||||
const appId = c.req.param("id");
|
||||
const app = await deleteApp(appId);
|
||||
if (app) {
|
||||
auditWrapper.auditLog(
|
||||
auth.userId,
|
||||
"app_deleted",
|
||||
appId,
|
||||
{ name: app.name },
|
||||
getClientIp(c),
|
||||
);
|
||||
return c.json({ success: true });
|
||||
}
|
||||
return c.json({ error: "Application not found" }, 404);
|
||||
});
|
||||
|
||||
@ -162,6 +162,54 @@ export const revokeSessionById = async (sessionId: string) => {
|
||||
`.then((res: any) => res[0]);
|
||||
};
|
||||
|
||||
export const createApp = async (
|
||||
name: string,
|
||||
spiffeId: string,
|
||||
description?: string | null,
|
||||
domain?: string | null,
|
||||
isPublic?: boolean,
|
||||
bypassPaths?: string[],
|
||||
allowedCidrs?: string[],
|
||||
) => {
|
||||
return await sqlWrapper.sql`
|
||||
INSERT INTO apps (name, spiffe_id, description, domain, is_public, bypass_paths, allowed_cidrs)
|
||||
VALUES (${name.trim()}, ${spiffeId.trim()}, ${
|
||||
description?.trim() || null
|
||||
}, ${domain?.trim() || null}, ${isPublic || false}, ${bypassPaths || []}, ${
|
||||
allowedCidrs || []
|
||||
})
|
||||
RETURNING id, name, spiffe_id, description, domain, is_public, bypass_paths, allowed_cidrs, created_at
|
||||
`.then((res: any) => res[0]);
|
||||
};
|
||||
|
||||
export const updateApp = async (
|
||||
appId: string,
|
||||
name: string,
|
||||
description?: string | null,
|
||||
domain?: string | null,
|
||||
isPublic?: boolean,
|
||||
bypassPaths?: string[],
|
||||
allowedCidrs?: string[],
|
||||
) => {
|
||||
return await sqlWrapper.sql`
|
||||
UPDATE apps
|
||||
SET name = ${name.trim()},
|
||||
description = ${description?.trim() || null},
|
||||
domain = ${domain?.trim() || null},
|
||||
is_public = ${isPublic || false},
|
||||
bypass_paths = ${bypassPaths || []},
|
||||
allowed_cidrs = ${allowedCidrs || []}
|
||||
WHERE id = ${appId}
|
||||
RETURNING id, name, spiffe_id, description, domain, is_public, bypass_paths, allowed_cidrs
|
||||
`.then((res: any) => res[0]);
|
||||
};
|
||||
|
||||
export const deleteApp = async (appId: string) => {
|
||||
return await sqlWrapper.sql`
|
||||
DELETE FROM apps WHERE id = ${appId} RETURNING id, name
|
||||
`.then((res: any) => res[0]);
|
||||
};
|
||||
|
||||
export const getDashboardApps = async (
|
||||
userId: string,
|
||||
isAdmin: boolean,
|
||||
@ -169,9 +217,8 @@ export const getDashboardApps = async (
|
||||
) => {
|
||||
if (isAdmin) {
|
||||
return await sqlWrapper.sql`
|
||||
SELECT id, name, description, domain, 'Admin' as role
|
||||
SELECT id, name, description, COALESCE(domain, name || '.atyg.org') as domain, 'Admin' as role
|
||||
FROM apps
|
||||
WHERE domain IS NOT NULL
|
||||
ORDER BY name ASC
|
||||
`;
|
||||
} else {
|
||||
@ -185,29 +232,28 @@ export const getDashboardApps = async (
|
||||
|
||||
if (isUniversalGuest) {
|
||||
return await sqlWrapper.sql`
|
||||
SELECT id, name, description, domain, 'Guest (Viewer)' as role
|
||||
SELECT id, name, description, COALESCE(domain, name || '.atyg.org') as domain, 'Guest (Viewer)' as role
|
||||
FROM apps
|
||||
WHERE domain IS NOT NULL
|
||||
ORDER BY name ASC
|
||||
`;
|
||||
} else if (appNames.length > 0) {
|
||||
return await sqlWrapper.sql`
|
||||
SELECT a.id, a.name, a.description, a.domain, g.role
|
||||
SELECT a.id, a.name, a.description, COALESCE(a.domain, a.name || '.atyg.org') as domain, g.role
|
||||
FROM apps a
|
||||
JOIN grants g ON a.id = g.app_id
|
||||
WHERE g.user_id = ${userId} AND a.domain IS NOT NULL
|
||||
WHERE g.user_id = ${userId}
|
||||
UNION
|
||||
SELECT id, name, description, domain, 'Guest (Viewer)' as role
|
||||
SELECT id, name, description, COALESCE(domain, name || '.atyg.org') as domain, 'Guest (Viewer)' as role
|
||||
FROM apps
|
||||
WHERE domain IS NOT NULL AND name = ANY(${appNames}::text[])
|
||||
WHERE name = ANY(${appNames}::text[])
|
||||
ORDER BY name ASC
|
||||
`;
|
||||
} else {
|
||||
return await sqlWrapper.sql`
|
||||
SELECT a.id, a.name, a.description, a.domain, g.role
|
||||
SELECT a.id, a.name, a.description, COALESCE(a.domain, a.name || '.atyg.org') as domain, g.role
|
||||
FROM apps a
|
||||
JOIN grants g ON a.id = g.app_id
|
||||
WHERE g.user_id = ${userId} AND a.domain IS NOT NULL
|
||||
WHERE g.user_id = ${userId}
|
||||
ORDER BY a.name ASC
|
||||
`;
|
||||
}
|
||||
|
||||
@ -2,7 +2,11 @@ import { Hono } from "jsr:@hono/hono@4";
|
||||
|
||||
import { streamDatastar } from "../../core/sse_adapter.ts";
|
||||
import { renderErrorToastFragment } from "../../core/error_fragments.tsx";
|
||||
import { getAuthenticatedUser, hasScope } from "../../core/session.ts";
|
||||
import {
|
||||
getAuthenticatedUser,
|
||||
hasScope,
|
||||
isGlobalAdmin,
|
||||
} from "../../core/session.ts";
|
||||
import {
|
||||
getAllApps,
|
||||
getDashboardApps,
|
||||
@ -29,7 +33,7 @@ sessionRoutes.get("/dashboard", async (c) => {
|
||||
return c.redirect("/login");
|
||||
}
|
||||
|
||||
const isAdmin = hasScope(auth, "admin") || hasScope(auth, "*");
|
||||
const isAdmin = await isGlobalAdmin(auth.userId);
|
||||
const apps = await getDashboardApps(auth.userId, isAdmin, auth.customScopes);
|
||||
|
||||
return c.html(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user