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`
|
await sql`
|
||||||
INSERT INTO apps (name, spiffe_id)
|
INSERT INTO apps (name, spiffe_id, domain, description)
|
||||||
VALUES ('ed-droid', 'spiffe://system.local/ed-droid-backend')
|
VALUES ('ed-droid', 'spiffe://system.local/ed-droid-backend', 'ed-droid.atyg.org', 'Autonomous Agent UI & Workload Control')
|
||||||
ON CONFLICT (spiffe_id) DO NOTHING
|
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
|
// Seed the Central Auth-Yes Management App for global administration
|
||||||
await sql`
|
await sql`
|
||||||
INSERT INTO apps (name, spiffe_id)
|
INSERT INTO apps (name, spiffe_id, domain, description)
|
||||||
VALUES ('Auth-Yes Management Console', 'spiffe://system.local/auth-yes-management')
|
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 NOTHING
|
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
|
// Seed initial bootstrap admin invite if no users exist in the database
|
||||||
|
|||||||
@ -7,9 +7,11 @@ import { getClientIp } from "../../core/middleware.ts";
|
|||||||
import {
|
import {
|
||||||
addAaguid,
|
addAaguid,
|
||||||
assignUserGrant,
|
assignUserGrant,
|
||||||
|
createApp,
|
||||||
createInvite,
|
createInvite,
|
||||||
createRecoveryLink,
|
createRecoveryLink,
|
||||||
createRole,
|
createRole,
|
||||||
|
deleteApp,
|
||||||
deleteRole,
|
deleteRole,
|
||||||
getUserById,
|
getUserById,
|
||||||
removeAaguid,
|
removeAaguid,
|
||||||
@ -18,6 +20,7 @@ import {
|
|||||||
revokeInvite,
|
revokeInvite,
|
||||||
revokeSessionById,
|
revokeSessionById,
|
||||||
revokeUserPasskey,
|
revokeUserPasskey,
|
||||||
|
updateApp,
|
||||||
updateRole,
|
updateRole,
|
||||||
updateUserProfile,
|
updateUserProfile,
|
||||||
updateUserStatus,
|
updateUserStatus,
|
||||||
@ -280,3 +283,108 @@ adminActionsRoutes.delete("/aaguid/:id", async (c) => {
|
|||||||
await removeAaguid(id);
|
await removeAaguid(id);
|
||||||
return c.json({ success: true });
|
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]);
|
`.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 (
|
export const getDashboardApps = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
isAdmin: boolean,
|
isAdmin: boolean,
|
||||||
@ -169,9 +217,8 @@ export const getDashboardApps = async (
|
|||||||
) => {
|
) => {
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
return await sqlWrapper.sql`
|
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
|
FROM apps
|
||||||
WHERE domain IS NOT NULL
|
|
||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@ -185,29 +232,28 @@ export const getDashboardApps = async (
|
|||||||
|
|
||||||
if (isUniversalGuest) {
|
if (isUniversalGuest) {
|
||||||
return await sqlWrapper.sql`
|
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
|
FROM apps
|
||||||
WHERE domain IS NOT NULL
|
|
||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
`;
|
`;
|
||||||
} else if (appNames.length > 0) {
|
} else if (appNames.length > 0) {
|
||||||
return await sqlWrapper.sql`
|
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
|
FROM apps a
|
||||||
JOIN grants g ON a.id = g.app_id
|
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
|
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
|
FROM apps
|
||||||
WHERE domain IS NOT NULL AND name = ANY(${appNames}::text[])
|
WHERE name = ANY(${appNames}::text[])
|
||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
return await sqlWrapper.sql`
|
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
|
FROM apps a
|
||||||
JOIN grants g ON a.id = g.app_id
|
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
|
ORDER BY a.name ASC
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,11 @@ import { Hono } from "jsr:@hono/hono@4";
|
|||||||
|
|
||||||
import { streamDatastar } from "../../core/sse_adapter.ts";
|
import { streamDatastar } from "../../core/sse_adapter.ts";
|
||||||
import { renderErrorToastFragment } from "../../core/error_fragments.tsx";
|
import { renderErrorToastFragment } from "../../core/error_fragments.tsx";
|
||||||
import { getAuthenticatedUser, hasScope } from "../../core/session.ts";
|
import {
|
||||||
|
getAuthenticatedUser,
|
||||||
|
hasScope,
|
||||||
|
isGlobalAdmin,
|
||||||
|
} from "../../core/session.ts";
|
||||||
import {
|
import {
|
||||||
getAllApps,
|
getAllApps,
|
||||||
getDashboardApps,
|
getDashboardApps,
|
||||||
@ -29,7 +33,7 @@ sessionRoutes.get("/dashboard", async (c) => {
|
|||||||
return c.redirect("/login");
|
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);
|
const apps = await getDashboardApps(auth.userId, isAdmin, auth.customScopes);
|
||||||
|
|
||||||
return c.html(
|
return c.html(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user