TenantAssignment and readside filtering
All checks were successful
Build and Push / build (push) Successful in 1m23s
All checks were successful
Build and Push / build (push) Successful in 1m23s
This commit is contained in:
@@ -1,13 +1,21 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requirePlatformRole } from "@/lib/session";
|
||||
import { getTenant, deleteTenant } from "@/lib/k8s";
|
||||
import { markTenantRequestDeletedByTenantName } from "@/lib/db";
|
||||
import {
|
||||
markTenantRequestDeletedByTenantName,
|
||||
removeAllAssignmentsForTenant,
|
||||
} from "@/lib/db";
|
||||
import { safeError } from "@/lib/errors";
|
||||
|
||||
/**
|
||||
* POST /api/admin/tenants/[name]/delete
|
||||
* Delete a PiecedTenant CR. The operator handles cleanup
|
||||
* (namespace, vault, litellm team, etc.).
|
||||
*
|
||||
* Slice 6: also cascades the tenant_user_assignments rows so a
|
||||
* future tenant with the same name (won't happen given UUID-suffix
|
||||
* naming, but defense in depth) doesn't inherit stale assignments.
|
||||
*
|
||||
* Also marks the associated tenant_request as "deleted" so the
|
||||
* customer can re-submit the onboarding wizard.
|
||||
*/
|
||||
@@ -31,10 +39,14 @@ export async function POST(
|
||||
try {
|
||||
await deleteTenant(name);
|
||||
|
||||
// Mark the associated tenant_request as "deleted" so the customer
|
||||
// sees the wizard again instead of a stale "active" status
|
||||
// Best-effort DB cleanups. Both errors are logged but not surfaced —
|
||||
// the K8s deletion has already started, and the row state is just
|
||||
// for portal display.
|
||||
await markTenantRequestDeletedByTenantName(name).catch((e) =>
|
||||
console.error("Failed to update tenant request after delete:", e)
|
||||
console.error("Failed to mark tenant request deleted:", e)
|
||||
);
|
||||
await removeAllAssignmentsForTenant(name).catch((e) =>
|
||||
console.error("Failed to clean up tenant assignments:", e)
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -8,6 +8,11 @@ import {
|
||||
getMostRecentApprovedRequestForOrg,
|
||||
} from "@/lib/db";
|
||||
import { getTenant, listTenants } from "@/lib/k8s";
|
||||
import {
|
||||
listVisibleTenants,
|
||||
canUserSeeTenant,
|
||||
canSeeInflightRequests,
|
||||
} from "@/lib/visibility";
|
||||
import { sendAdminNotificationEmail } from "@/lib/email";
|
||||
import { encryptSecrets } from "@/lib/crypto";
|
||||
import { isPersonalOrgName } from "@/lib/personal-org";
|
||||
@@ -106,10 +111,24 @@ export async function GET(req: NextRequest) {
|
||||
if (!user.isPlatform && tr.zitadelOrgId !== user.orgId) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
// Slice 6: a `user`-role customer doesn't see in-flight requests
|
||||
// even within their own org — they can't act on them and showing
|
||||
// the row would be a permanent "pending" state with no exit. Owner
|
||||
// and platform skip this gate.
|
||||
if (!canSeeInflightRequests(user)) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
let tenant: PiecedTenant | null = null;
|
||||
if (tr.tenantName) {
|
||||
tenant = (await getTenant(tr.tenantName)) ?? null;
|
||||
// If a request is already linked to a tenant CR and the caller
|
||||
// can't see that tenant (assignment scope), don't expose it via
|
||||
// the request endpoint either. canSeeInflightRequests above
|
||||
// already shortcuts this for `user`-role, but defense in depth.
|
||||
if (tenant && !(await canUserSeeTenant(user, tenant))) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
}
|
||||
return NextResponse.json({
|
||||
request: publicRequestShape(tr),
|
||||
@@ -117,19 +136,21 @@ export async function GET(req: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
// List view: requests + tenants for this org
|
||||
// List view: requests + tenants for this org, filtered by visibility.
|
||||
// For owner/platform, this returns the same data as pre-Slice-6.
|
||||
// For user-role, requests is forced to [] and tenants is narrowed to
|
||||
// assignments.
|
||||
const [requests, allTenants] = await Promise.all([
|
||||
listActiveTenantRequestsByOrgId(user.orgId),
|
||||
listTenants(),
|
||||
]);
|
||||
|
||||
const orgTenants = allTenants.filter(
|
||||
(t) => t.metadata.labels?.["pieced.ch/zitadel-org-id"] === user.orgId
|
||||
);
|
||||
const visibleTenants = await listVisibleTenants(user, allTenants);
|
||||
const visibleRequests = canSeeInflightRequests(user) ? requests : [];
|
||||
|
||||
return NextResponse.json({
|
||||
requests: requests.map(publicRequestShape),
|
||||
tenants: orgTenants.map(publicTenantShape),
|
||||
requests: visibleRequests.map(publicRequestShape),
|
||||
tenants: visibleTenants.map(publicTenantShape),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getSessionUser, canMutate } from "@/lib/session";
|
||||
import { canUserSeeTenant } from "@/lib/visibility";
|
||||
import { getTenant, patchTenantSpec } from "@/lib/k8s";
|
||||
import { getPackageDef } from "@/lib/packages";
|
||||
import { safeError } from "@/lib/errors";
|
||||
@@ -22,11 +23,11 @@ export async function GET(
|
||||
if (!tenant)
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
if (
|
||||
!user.isPlatform &&
|
||||
tenant.metadata.labels?.["pieced.ch/zitadel-org-id"] !== user.orgId
|
||||
) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
// Slice 6: visibility now includes assignment-table check for
|
||||
// user-role members. We return 404 (not 403) to avoid leaking
|
||||
// tenant existence — same as cross-org reads.
|
||||
if (!(await canUserSeeTenant(user, tenant))) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(tenant);
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getSessionUser } from "@/lib/session";
|
||||
import { listTenants } from "@/lib/k8s";
|
||||
import { listVisibleTenants } from "@/lib/visibility";
|
||||
|
||||
export async function GET() {
|
||||
const user = await getSessionUser();
|
||||
if (!user)
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const tenants = await listTenants();
|
||||
|
||||
if (user.isPlatform) {
|
||||
return NextResponse.json(tenants);
|
||||
}
|
||||
|
||||
// Customers see only their own tenant
|
||||
const own = tenants.filter(
|
||||
(t) => t.metadata.labels?.["pieced.ch/zitadel-org-id"] === user.orgId
|
||||
);
|
||||
return NextResponse.json(own);
|
||||
const all = await listTenants();
|
||||
const visible = await listVisibleTenants(user, all);
|
||||
return NextResponse.json(visible);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getSessionUser } from "@/lib/session";
|
||||
import { listTenants } from "@/lib/k8s";
|
||||
import { listVisibleTenants } from "@/lib/visibility";
|
||||
import { getTeamInfo, getTeamSpendLogsV2 } from "@/lib/litellm";
|
||||
import { safeError } from "@/lib/errors";
|
||||
|
||||
@@ -36,12 +37,17 @@ export async function GET(req: NextRequest) {
|
||||
keyAlias = req.nextUrl.searchParams.get("keyAlias") ?? null;
|
||||
}
|
||||
|
||||
// For customers (or admins without explicit params): resolve from their tenant.
|
||||
// For customers (or admins without explicit params): resolve from
|
||||
// the user's *visible* tenants. With Slice 6, a `user`-role member
|
||||
// can only see usage for tenants they're assigned to — a non-assigned
|
||||
// user defaults to "no active tenant" (404).
|
||||
//
|
||||
// Owner and platform get the full org-scoped list and pick the first
|
||||
// tenant, matching the dashboard's "current instance" semantics.
|
||||
if (!teamId) {
|
||||
const tenants = await listTenants();
|
||||
const orgTenant = tenants.find(
|
||||
(t) => t.metadata.labels?.["pieced.ch/zitadel-org-id"] === user.orgId
|
||||
);
|
||||
const allTenants = await listTenants();
|
||||
const visible = await listVisibleTenants(user, allTenants);
|
||||
const orgTenant = visible.find((t) => !!t.status?.litellmTeamId);
|
||||
|
||||
if (!orgTenant?.status?.litellmTeamId) {
|
||||
return NextResponse.json(
|
||||
|
||||
Reference in New Issue
Block a user