Add identity access controls
This commit is contained in:
1 parent
276929b855
commit
1d7757c88b
21 files changed
+607
-8
No files matched your search
@@ -76,6 +76,9 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
const complianceExports = useMemo(() => snapshot.complianceExports.filter((item) => item.orgId === orgId), [snapshot.complianceExports, orgId]);
|
||||
const legalHolds = useMemo(() => snapshot.legalHolds.filter((item) => item.orgId === orgId), [snapshot.legalHolds, orgId]);
|
||||
const retentionFreezes = useMemo(() => snapshot.retentionFreezes.filter((item) => item.orgId === orgId), [snapshot.retentionFreezes, orgId]);
|
||||
const employeeIdentities = useMemo(() => snapshot.employeeIdentities.filter((item) => item.orgId === orgId), [snapshot.employeeIdentities, orgId]);
|
||||
const serviceIdentities = useMemo(() => snapshot.serviceIdentities.filter((item) => item.orgId === orgId), [snapshot.serviceIdentities, orgId]);
|
||||
const roleGrants = useMemo(() => snapshot.roleGrants.filter((item) => item.orgId === orgId), [snapshot.roleGrants, orgId]);
|
||||
|
||||
return (
|
||||
<div className="split-main" style={{ height: "calc(100vh - 44px)", margin: "-28px -32px -40px" }}>
|
||||
@@ -90,7 +93,15 @@ export function SettingsPage({ snapshot, actions }: PageProps) {
|
||||
<div className="main" style={{ flex: 1, minWidth: 0 }}>
|
||||
<div className="content">
|
||||
{tab === "workspace" && <WorkspacePanel organization={organization} workspaces={workspaces} />}
|
||||
{tab === "members" && <MembersPanel employees={employees} agents={agents} />}
|
||||
{tab === "members" && (
|
||||
<MembersPanel
|
||||
employees={employees}
|
||||
agents={agents}
|
||||
employeeIdentities={employeeIdentities}
|
||||
serviceIdentities={serviceIdentities}
|
||||
roleGrants={roleGrants}
|
||||
/>
|
||||
)}
|
||||
{tab === "billing" && <BillingPanel agents={agents} providers={providers} providerRequests={providerRequests} />}
|
||||
{tab === "models" && <ModelsPanel providers={providers} agents={agents} />}
|
||||
{tab === "providers" && (
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { Avatar, Pill } from "../../shared/ui";
|
||||
import type { AgentRecord, EmployeeRecord } from "../../../../shared/domain";
|
||||
import type { AgentRecord, EmployeeIdentityRecord, EmployeeRecord, RoleGrantRecord, ServiceIdentityRecord } from "../../../../shared/domain";
|
||||
import { money } from "../types";
|
||||
|
||||
export function MembersPanel({
|
||||
employees,
|
||||
agents,
|
||||
employeeIdentities,
|
||||
serviceIdentities,
|
||||
roleGrants,
|
||||
}: {
|
||||
employees: EmployeeRecord[];
|
||||
agents: AgentRecord[];
|
||||
employeeIdentities: EmployeeIdentityRecord[];
|
||||
serviceIdentities: ServiceIdentityRecord[];
|
||||
roleGrants: RoleGrantRecord[];
|
||||
}) {
|
||||
const total = employees.length + agents.length;
|
||||
return (
|
||||
@@ -63,6 +69,93 @@ export function MembersPanel({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="section-label" style={{ margin: "24px 0 10px" }}>Identity & Access</div>
|
||||
<div className="card" style={{ padding: 0, overflow: "hidden" }}>
|
||||
<div
|
||||
className="table-row head"
|
||||
style={{ gridTemplateColumns: "minmax(0, 1.6fr) minmax(0, 1.2fr) minmax(0, 1.8fr)" }}
|
||||
>
|
||||
<div>Principal</div><div>Identity source</div><div>Roles</div>
|
||||
</div>
|
||||
{employees.map((employee, index) => (
|
||||
<AccessRow
|
||||
key={employee.id}
|
||||
name={employee.displayName}
|
||||
detail={employee.role}
|
||||
source={identitySource(employee.id, employeeIdentities)}
|
||||
roles={principalRoles("employee", employee.id, roleGrants)}
|
||||
bot={false}
|
||||
last={agents.length === 0 && index === employees.length - 1}
|
||||
/>
|
||||
))}
|
||||
{agents.map((agent, index) => {
|
||||
const serviceIdentity = serviceIdentities.find((identity) => identity.agentId === agent.id);
|
||||
return (
|
||||
<AccessRow
|
||||
key={agent.id}
|
||||
name={agent.name}
|
||||
detail={agent.department}
|
||||
source={serviceIdentity ? "Service identity" : "Agent"}
|
||||
roles={serviceIdentity ? principalRoles("service_identity", serviceIdentity.id, roleGrants) : []}
|
||||
bot
|
||||
last={index === agents.length - 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AccessRow({
|
||||
name,
|
||||
detail,
|
||||
source,
|
||||
roles,
|
||||
bot,
|
||||
last,
|
||||
}: {
|
||||
name: string;
|
||||
detail: string;
|
||||
source: string;
|
||||
roles: string[];
|
||||
bot: boolean;
|
||||
last: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="table-row"
|
||||
style={{
|
||||
gridTemplateColumns: "minmax(0, 1.6fr) minmax(0, 1.2fr) minmax(0, 1.8fr)",
|
||||
borderBottom: last ? "none" : "1px solid var(--border-soft)",
|
||||
}}
|
||||
>
|
||||
<div className="row" style={{ gap: 10, minWidth: 0 }}>
|
||||
<Avatar name={name} size="sm" bot={bot} />
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{name}</div>
|
||||
<div style={{ fontSize: 11, color: "var(--text-3)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{detail}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ color: "var(--text-2)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{source}</div>
|
||||
<div className="row" style={{ gap: 6, flexWrap: "wrap" }}>
|
||||
{roles.map((role) => <Pill key={role}>{role}</Pill>)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function identitySource(employeeId: string, identities: EmployeeIdentityRecord[]): string {
|
||||
const identity = identities.find((item) => item.employeeId === employeeId && item.active);
|
||||
if (!identity) return "Unlinked";
|
||||
if (identity.source === "sso") return "SSO";
|
||||
if (identity.source === "scim") return "SCIM";
|
||||
return "Bootstrap";
|
||||
}
|
||||
|
||||
function principalRoles(principalType: RoleGrantRecord["principalType"], principalId: string, grants: RoleGrantRecord[]): string[] {
|
||||
return grants
|
||||
.filter((grant) => grant.principalType === principalType && grant.principalId === principalId)
|
||||
.map((grant) => grant.roleKey.replaceAll("_", " "))
|
||||
.sort();
|
||||
}
|
||||
Reference in new issue
Block a user