Fix zitadel role issues
All checks were successful
Build and Push / build (push) Successful in 1m20s
All checks were successful
Build and Push / build (push) Successful in 1m20s
This commit is contained in:
@@ -156,6 +156,18 @@ export interface ProjectGrantResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Grant the "OpenClaw Platform" project to a customer organization.
|
* Grant the "OpenClaw Platform" project to a customer organization.
|
||||||
|
*
|
||||||
|
* The grant's `roleKeys` whitelist what authorizations the customer org
|
||||||
|
* may self-manage: a grant containing only "owner" prevents the customer
|
||||||
|
* from inviting members in the `user` role, because ZITADEL rejects
|
||||||
|
* `CreateAuthorization` for any role outside the grant with
|
||||||
|
* `Errors.Project.Role.NotFound`.
|
||||||
|
*
|
||||||
|
* Default is therefore `["owner", "user"]` — the full set of customer
|
||||||
|
* roles defined in `types/index.ts::CustomerRole`. Platform roles are
|
||||||
|
* intentionally NOT granted; those are administered separately and
|
||||||
|
* should never be assignable from inside a customer org.
|
||||||
|
*
|
||||||
* Connect RPC: zitadel.project.v2.ProjectService/CreateProjectGrant
|
* Connect RPC: zitadel.project.v2.ProjectService/CreateProjectGrant
|
||||||
*/
|
*/
|
||||||
export async function createProjectGrant(
|
export async function createProjectGrant(
|
||||||
@@ -168,11 +180,44 @@ export async function createProjectGrant(
|
|||||||
{
|
{
|
||||||
projectId: ZITADEL_PROJECT_ID,
|
projectId: ZITADEL_PROJECT_ID,
|
||||||
grantedOrganizationId: grantedOrgId,
|
grantedOrganizationId: grantedOrgId,
|
||||||
roleKeys: roleKeys || ["owner"],
|
roleKeys: roleKeys || ["owner", "user"],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List the role keys defined on the OpenClaw Platform project.
|
||||||
|
*
|
||||||
|
* Used by the instrumentation self-check on startup to warn loudly if
|
||||||
|
* the canonical role keys (owner / user / platform_admin / platform_operator)
|
||||||
|
* are missing — a misconfiguration that silently breaks team management
|
||||||
|
* and customer registration. See `scripts/zitadel-roles.mjs` for repair.
|
||||||
|
*
|
||||||
|
* Returns [] on any error (network, auth, shape drift) so callers can
|
||||||
|
* decide what to do without inheriting a thrown exception during boot.
|
||||||
|
*
|
||||||
|
* Connect RPC: zitadel.project.v2.ProjectService/ListProjectRoles
|
||||||
|
*/
|
||||||
|
export async function listProjectRoles(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const data = await connectRpc<{ projectRoles?: any[] }>(
|
||||||
|
"zitadel.project.v2.ProjectService",
|
||||||
|
"ListProjectRoles",
|
||||||
|
{ projectId: ZITADEL_PROJECT_ID }
|
||||||
|
);
|
||||||
|
if (!data?.projectRoles || !Array.isArray(data.projectRoles)) return [];
|
||||||
|
return data.projectRoles
|
||||||
|
.map((r: any) => (typeof r?.key === "string" ? r.key : ""))
|
||||||
|
.filter(Boolean);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`Failed to list project roles for ${ZITADEL_PROJECT_ID} (returning empty):`,
|
||||||
|
err
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// v2 Authorization API — Connect RPC
|
// v2 Authorization API — Connect RPC
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -305,6 +350,24 @@ export interface OrgAuthorization {
|
|||||||
*
|
*
|
||||||
* Connect RPC: zitadel.authorization.v2.AuthorizationService/ListAuthorizations
|
* Connect RPC: zitadel.authorization.v2.AuthorizationService/ListAuthorizations
|
||||||
*
|
*
|
||||||
|
* Implementation note (filter shape & response parsing)
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* The v2 AuthorizationService accepts a `filters` array of oneof variants
|
||||||
|
* (project_id, organization_id, role_key, …) but the JSON-over-Connect
|
||||||
|
* wrapper naming differs between ZITADEL versions and isn't well-documented
|
||||||
|
* for ID filters. Rather than chase a moving target, we fetch all
|
||||||
|
* authorizations the SA can see and narrow client-side by project+org.
|
||||||
|
* At pilot scale this is a single sub-100-row query — well within budget.
|
||||||
|
*
|
||||||
|
* Response shape (v2 stable, confirmed against ZITADEL v4.12):
|
||||||
|
* authorizations: [{
|
||||||
|
* id, state,
|
||||||
|
* project: { id, name, organizationId },
|
||||||
|
* organization: { id, name },
|
||||||
|
* user: { id, displayName, preferredLoginName, … },
|
||||||
|
* roles: [{ key, displayName, group }],
|
||||||
|
* }]
|
||||||
|
*
|
||||||
* Returns [] on any error so the team page can render a degraded view
|
* Returns [] on any error so the team page can render a degraded view
|
||||||
* (members visible, roles blank) rather than blowing up entirely.
|
* (members visible, roles blank) rather than blowing up entirely.
|
||||||
*/
|
*/
|
||||||
@@ -315,32 +378,29 @@ export async function listOrgAuthorizations(
|
|||||||
const data = await connectRpc<{ authorizations?: any[] }>(
|
const data = await connectRpc<{ authorizations?: any[] }>(
|
||||||
"zitadel.authorization.v2.AuthorizationService",
|
"zitadel.authorization.v2.AuthorizationService",
|
||||||
"ListAuthorizations",
|
"ListAuthorizations",
|
||||||
{
|
{ pagination: { limit: 1000 } }
|
||||||
filters: [
|
|
||||||
{ organizationId: orgId },
|
|
||||||
{ projectId: ZITADEL_PROJECT_ID },
|
|
||||||
],
|
|
||||||
// Cap at 500 — far more than a pilot org should ever need
|
|
||||||
pagination: { limit: 500 },
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (!data?.authorizations || !Array.isArray(data.authorizations)) {
|
if (!data?.authorizations || !Array.isArray(data.authorizations)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.authorizations.flatMap((row: any) => {
|
return data.authorizations
|
||||||
const userId = row?.userId ?? "";
|
.filter(
|
||||||
if (!userId) return [];
|
(row: any) =>
|
||||||
return [
|
row?.project?.id === ZITADEL_PROJECT_ID &&
|
||||||
{
|
row?.organization?.id === orgId
|
||||||
authorizationId: row.id ?? row.authorizationId ?? "",
|
)
|
||||||
userId,
|
.map((row: any) => ({
|
||||||
organizationId: row.organizationId ?? orgId,
|
authorizationId: row.id ?? "",
|
||||||
projectId: row.projectId ?? ZITADEL_PROJECT_ID,
|
userId: row.user?.id ?? "",
|
||||||
roleKeys: Array.isArray(row.roleKeys) ? row.roleKeys : [],
|
organizationId: row.organization?.id ?? orgId,
|
||||||
} as OrgAuthorization,
|
projectId: row.project?.id ?? ZITADEL_PROJECT_ID,
|
||||||
];
|
roleKeys: Array.isArray(row.roles)
|
||||||
});
|
? row.roles
|
||||||
|
.map((r: any) => (typeof r?.key === "string" ? r.key : ""))
|
||||||
|
.filter(Boolean)
|
||||||
|
: [],
|
||||||
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Failed to list authorizations for org ${orgId} (returning empty):`,
|
`Failed to list authorizations for org ${orgId} (returning empty):`,
|
||||||
@@ -402,8 +462,12 @@ export async function registerCustomer(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Grant project to org
|
// 4. Grant project to org with both customer roles so the org's
|
||||||
const grant = await createProjectGrant(org.organizationId, ["owner"]);
|
// owner can invite users in either `owner` or `user` role afterwards.
|
||||||
|
const grant = await createProjectGrant(org.organizationId, [
|
||||||
|
"owner",
|
||||||
|
"user",
|
||||||
|
]);
|
||||||
|
|
||||||
// 5. Assign "owner" role to user
|
// 5. Assign "owner" role to user
|
||||||
await createAuthorization({
|
await createAuthorization({
|
||||||
|
|||||||
Reference in New Issue
Block a user