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:
@@ -3,6 +3,11 @@ import { getTranslations, getFormatter } from "next-intl/server";
|
||||
import { redirect } from "next/navigation";
|
||||
import { listTenants } from "@/lib/k8s";
|
||||
import { listActiveTenantRequestsByOrgId } from "@/lib/db";
|
||||
import {
|
||||
listVisibleTenants,
|
||||
canSeeInflightRequests,
|
||||
isUserScoped,
|
||||
} from "@/lib/visibility";
|
||||
import { Card, CardHeader } from "@/components/ui/card";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
import { OnboardingFlow } from "@/components/onboarding/onboarding-flow";
|
||||
@@ -134,19 +139,40 @@ export default async function DashboardPage() {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Customer view (Slice 3 multi-tenant)
|
||||
// Customer view (Slice 3 multi-tenant + Slice 6 visibility scoping)
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
const orgTenants = allTenants.filter(
|
||||
// Slice 6: orgTenants becomes "visible tenants for this user". For an
|
||||
// owner that's all of the org's tenants; for a `user`-role member
|
||||
// it's only the tenants they've been assigned to via
|
||||
// tenant_user_assignments. The dashboard renders fewer cards in the
|
||||
// user-role case but otherwise uses the same template.
|
||||
const orgTenants = await listVisibleTenants(user, allTenants);
|
||||
|
||||
// For the "no instances yet" empty state, we want to know whether
|
||||
// this user is being scoped down. A `user`-role with 0 visible
|
||||
// tenants gets a different message than an owner with 0 tenants
|
||||
// (the user might just need an assignment; the owner needs to
|
||||
// create one).
|
||||
const userScoped = isUserScoped(user);
|
||||
|
||||
// Pending/in-flight requests are only shown to roles that can act on
|
||||
// them. `user`-role customers see no request cards.
|
||||
const orgRequests = canSeeInflightRequests(user)
|
||||
? await listActiveTenantRequestsByOrgId(user.orgId)
|
||||
: [];
|
||||
|
||||
// Pending requests that don't yet have a tenant CR. Once the CR
|
||||
// exists, the tenant card carries the live phase, so a separate
|
||||
// "request" card would just duplicate it. We compare against
|
||||
// *all* org tenants here (not just visible ones) — otherwise a
|
||||
// request whose tenant is invisible to the caller would erroneously
|
||||
// show as in-flight.
|
||||
const orgScopedTenants = allTenants.filter(
|
||||
(t) => t.metadata.labels?.["pieced.ch/zitadel-org-id"] === user.orgId
|
||||
);
|
||||
const orgRequests = await listActiveTenantRequestsByOrgId(user.orgId);
|
||||
|
||||
// Pending/in-flight requests that don't yet have a tenant CR. Once the
|
||||
// CR exists, the tenant card carries the live phase, so a separate
|
||||
// "request" card would just duplicate it.
|
||||
const inflightRequests = orgRequests.filter(
|
||||
(r) => !r.tenantName || !orgTenants.some((t) => t.metadata.name === r.tenantName)
|
||||
(r) => !r.tenantName || !orgScopedTenants.some((t) => t.metadata.name === r.tenantName)
|
||||
);
|
||||
|
||||
// Slice 5: only owners (and platform users, who'd typically be using
|
||||
@@ -155,14 +181,56 @@ export default async function DashboardPage() {
|
||||
// they need to ask an owner.
|
||||
const canCreate = canMutate(user);
|
||||
|
||||
// First-time user: empty company. Show the onboarding wizard inline.
|
||||
// Note: the registering user is always granted `owner` on their new
|
||||
// org by registerCustomer, so this branch is only reachable by an
|
||||
// owner — no role check needed here. But a customer-side `user`
|
||||
// promoted into a fresh empty org (Slice 7 invites) would also land
|
||||
// here without permission to submit. Belt-and-braces gate.
|
||||
// First-time / no-visibility branch.
|
||||
//
|
||||
// Three sub-cases:
|
||||
// 1. owner / platform with 0 tenants and 0 requests → show wizard.
|
||||
// 2. owner / platform with 0 visibility but the org HAS tenants →
|
||||
// shouldn't happen (owners see all org tenants). Defensive
|
||||
// fall-through to the wizard.
|
||||
// 3. user-role with 0 visible tenants → show "ask your owner"
|
||||
// message, with copy distinguishing whether the org has any
|
||||
// tenants at all.
|
||||
if (orgTenants.length === 0 && inflightRequests.length === 0) {
|
||||
if (userScoped) {
|
||||
// Slice 6 empty state for `user` role. The org might or might
|
||||
// not have tenants — either way this user has none assigned.
|
||||
// The two messages are subtly different: "no instances exist"
|
||||
// means owner needs to create one; "you're not assigned" means
|
||||
// owner needs to grant access.
|
||||
const orgHasTenants = orgScopedTenants.length > 0;
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8 animate-in">
|
||||
<h1 className="font-display text-2xl font-semibold accent-rule mb-2">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="text-text-secondary text-sm mt-4">
|
||||
{t("welcome", { name: user.name || user.email })}
|
||||
</p>
|
||||
</div>
|
||||
<Card className="animate-in animate-in-delay-1">
|
||||
<div className="text-center py-6">
|
||||
<h2 className="font-display text-base font-semibold text-text-primary mb-2">
|
||||
{orgHasTenants
|
||||
? t("noAssignmentsTitle")
|
||||
: t("noInstancesYetTitle")}
|
||||
</h2>
|
||||
<p className="text-sm text-text-secondary max-w-sm mx-auto">
|
||||
{orgHasTenants
|
||||
? t("noAssignmentsDescription")
|
||||
: t("noInstancesYetDescription")}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!canCreate) {
|
||||
// Belt-and-braces: any role that's neither owner-with-create nor
|
||||
// user-scope ends up here (e.g. weird cases like a session with
|
||||
// no roles at all). Same generic message as before.
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8 animate-in">
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getSessionUser, canMutate } from "@/lib/session";
|
||||
import { getTranslations, getFormatter } from "next-intl/server";
|
||||
import { redirect, notFound } from "next/navigation";
|
||||
import { getTenant } from "@/lib/k8s";
|
||||
import { canUserSeeTenant } from "@/lib/visibility";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
import { UsageDisplay } from "@/components/dashboard/usage-display";
|
||||
import { PackageList } from "@/components/packages/package-list";
|
||||
@@ -26,11 +27,10 @@ export default async function TenantDetailPage({
|
||||
const tenant = await getTenant(name);
|
||||
if (!tenant) notFound();
|
||||
|
||||
// Scope check
|
||||
if (
|
||||
!user.isPlatform &&
|
||||
tenant.metadata.labels?.["pieced.ch/zitadel-org-id"] !== user.orgId
|
||||
) {
|
||||
// Slice 6: visibility check encompasses org membership AND, for
|
||||
// user-role members, the tenant_user_assignments check. notFound()
|
||||
// (404) rather than redirect/403 to avoid leaking tenant existence.
|
||||
if (!(await canUserSeeTenant(user, tenant))) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user