Files
prowler/ui/lib/organizations.ts
T

137 lines
4.6 KiB
TypeScript

/**
* Organization vocabulary, keyed by organization type and node kind.
*
* Every hierarchy surface (providers table, row actions, deletion dialogs,
* onboarding copy) reads its wording from here instead of branching on a
* per-provider boolean. The table is typed `satisfies Record<OrganizationType,
* …>`, so adding an organization type is a compile error until its vocabulary
* exists — a new type can never silently inherit AWS wording.
*/
import {
NODE_KIND,
NodeKind,
ORGANIZATION_TYPE,
OrganizationType,
} from "@/types/organizations";
interface CandidateNoun {
singular: string;
plural: string;
}
interface OrgTypeTerminology {
/** Hierarchy container label, used when a node carries no `kind`. */
containerLabel: string;
/** The identifier an organization is named after when no name is given. */
identifierLabel: string;
/** What a discovered candidate is called in the onboarding flow. */
candidateNoun: CandidateNoun;
}
const ORGANIZATION_TERMINOLOGY = {
[ORGANIZATION_TYPE.AWS]: {
containerLabel: "Organizational Unit",
identifierLabel: "AWS organization ID",
candidateNoun: { singular: "account", plural: "accounts" },
},
[ORGANIZATION_TYPE.AZURE]: {
containerLabel: "Management Group",
identifierLabel: "tenant ID",
candidateNoun: { singular: "subscription", plural: "subscriptions" },
},
[ORGANIZATION_TYPE.GCP]: {
containerLabel: "Folder",
identifierLabel: "organization ID",
candidateNoun: { singular: "project", plural: "projects" },
},
} as const satisfies Record<OrganizationType, OrgTypeTerminology>;
const NODE_KIND_LABEL = {
[NODE_KIND.ORGANIZATIONAL_UNIT]: "Organizational Unit",
[NODE_KIND.FOLDER]: "Folder",
[NODE_KIND.MANAGEMENT_GROUP]: "Management Group",
} as const satisfies Record<NodeKind, string>;
const NODE_KINDS: readonly string[] = Object.values(NODE_KIND);
/**
* The organization-type enum mirrors a server-side one, so a type this build
* doesn't know about can still arrive on the wire. Rendering neutral wording
* beats crashing a table cell — or claiming AWS.
*/
const NEUTRAL_TERMINOLOGY: OrgTypeTerminology = {
containerLabel: "Group",
identifierLabel: "organization identifier",
candidateNoun: { singular: "account", plural: "accounts" },
};
const ORGANIZATION_TYPES: readonly string[] = Object.values(ORGANIZATION_TYPE);
// Membership check, not a `??` on the lookup: the tables are object literals, so
// an inherited key ("toString") would resolve to a truthy non-string.
function terminologyFor(orgType: OrganizationType): OrgTypeTerminology {
return ORGANIZATION_TYPES.includes(orgType)
? ORGANIZATION_TERMINOLOGY[orgType]
: NEUTRAL_TERMINOLOGY;
}
/**
* Container label for a hierarchy node. `kind` decides when present (canonical
* contract); the organization type is the fallback. Never derived from ID
* prefixes.
*/
export function getNodeLabel(
orgType: OrganizationType,
kind?: NodeKind,
): string {
// `kind` is typed but unvalidated: node rows pass the wire attribute through.
const knownKind = toNodeKind(kind);
return knownKind
? NODE_KIND_LABEL[knownKind]
: terminologyFor(orgType).containerLabel;
}
/**
* Every management group in a tenant repeats the same ARM prefix, so only the
* trailing name tells them apart. Case-insensitive, as ARM ids are.
*/
const MANAGEMENT_GROUP_ID =
/^\/providers\/Microsoft\.Management\/managementGroups\/(.+)$/i;
/**
* The readable tail of a node id, or undefined when the whole id already is (AWS
* OU ids, GCP folder refs). Presentation only: the canonical id stays the node's
* identity, so a caller that shortens must keep it reachable.
*/
export function shortenNodeId(id: string): string | undefined {
return MANAGEMENT_GROUP_ID.exec(id)?.[1];
}
/**
* Shared helper copy for the optional organization-name field. The fallback is the
* organization's own identifier, never a provider-side name: the organization
* exists before discovery runs.
*/
export function organizationNameFallbackHint(
orgType: OrganizationType,
): string {
return `If left blank, Prowler will use the ${terminologyFor(orgType).identifierLabel}.`;
}
/**
* What discovered candidates are called for this organization type.
*/
export function getCandidateNoun(orgType: OrganizationType): CandidateNoun {
return terminologyFor(orgType).candidateNoun;
}
/**
* Narrows a tree item's opaque `kind` string (the generic `TreeDataItem` carries
* no organization types) to a canonical node kind.
*/
export function toNodeKind(kind?: string): NodeKind | undefined {
return kind && NODE_KINDS.includes(kind) ? (kind as NodeKind) : undefined;
}