feat(aws): add IAM to Cloud Explorer#145
Conversation
|
| Filename | Overview |
|---|---|
| packages/api/src/adapter-aws/AwsIamAdapter.ts | New adapter implementing list (paginated), get, create, and delete for IAM users; validation errors now use the 'Use a valid' prefix that correctly routes to 400 in normalizeRuntimeError |
| packages/api/src/adapter-aws/AwsIamAdapter.test.ts | Full unit-test coverage for list pagination, search filter, get, not-found, create with/without path, input validation, delete, and schema shape |
| packages/api/src/cloud-spi/iamSchema.ts | Schema now includes length quantifier in the pattern ('^[A-Za-z0-9_+=,.@-]{1,64}$'), keeping it consistent with the adapter's runtime regex |
| packages/api/src/routes/clouds.ts | Adds DeleteConflictException → 409 handling and 'identity' to isServiceType; IAM-specific error message hardcoded in a generic function is a minor design concern |
| packages/api/src/routes/clouds.test.ts | Adds route-level tests for identity schema, validation error normalization to 400, and DeleteConflictException normalization to 409 |
| packages/api/src/service/CloudProxyService.ts | Adds identity service entry (availability driven by registry) and iamSchemaFor dispatch, consistent with existing service patterns |
| packages/frontend/src/components/Layout.tsx | Adds ShieldCheck icon and identity nav entry gated to AWS only, following the existing sidebar pattern |
| packages/frontend/src/pages/CloudExplorerPage.tsx | Adds 'identity' to normalizeService, serviceLabel, and limitationCopy — all three touched consistently |
| packages/api/src/cloud-spi/types.ts | Extends CloudServiceType with 'identity' and CloudResource type union with 'iam-user' |
| packages/api/src/aws.ts | Adds IAMClient to the per-account client map and exports the default singleton consistently with other AWS clients |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as Frontend
participant Route as clouds.ts route
participant SVC as CloudProxyService
participant Reg as CloudAdapterRegistry
participant Adapter as AwsIamAdapter
participant IAM as IAMClient (AWS SDK)
UI->>Route: GET /api/clouds/aws/services/identity/resources
Route->>SVC: "listResources('aws', 'identity', {search})"
SVC->>Reg: get('aws', 'identity')
Reg-->>SVC: AwsIamAdapter
SVC->>Adapter: "list({search})"
loop Pagination
Adapter->>IAM: "ListUsersCommand({Marker?})"
IAM-->>Adapter: "{Users, IsTruncated, Marker}"
end
Adapter-->>Route: CloudResource[]
Route-->>UI: 200 JSON
UI->>Route: POST /api/clouds/aws/services/identity/resources
Route->>SVC: "createResource('aws', 'identity', {values})"
SVC->>Adapter: "create({values})"
Note over Adapter: Validates userName and path
Adapter->>IAM: "CreateUserCommand({UserName, Path?})"
IAM-->>Adapter: "{User}"
Adapter-->>Route: CloudResource
Route-->>UI: 201 JSON
UI->>Route: DELETE /api/clouds/aws/services/identity/resources/alice
Route->>SVC: deleteResource('aws', 'identity', 'alice')
SVC->>Adapter: delete('alice')
Adapter->>IAM: "DeleteUserCommand({UserName})"
alt User has attached resources
IAM-->>Adapter: DeleteConflictException
Adapter-->>Route: throws
Route-->>UI: 409 resource_conflict
else Success
IAM-->>Adapter: ok
Route-->>UI: "200 {ok: true}"
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI as Frontend
participant Route as clouds.ts route
participant SVC as CloudProxyService
participant Reg as CloudAdapterRegistry
participant Adapter as AwsIamAdapter
participant IAM as IAMClient (AWS SDK)
UI->>Route: GET /api/clouds/aws/services/identity/resources
Route->>SVC: "listResources('aws', 'identity', {search})"
SVC->>Reg: get('aws', 'identity')
Reg-->>SVC: AwsIamAdapter
SVC->>Adapter: "list({search})"
loop Pagination
Adapter->>IAM: "ListUsersCommand({Marker?})"
IAM-->>Adapter: "{Users, IsTruncated, Marker}"
end
Adapter-->>Route: CloudResource[]
Route-->>UI: 200 JSON
UI->>Route: POST /api/clouds/aws/services/identity/resources
Route->>SVC: "createResource('aws', 'identity', {values})"
SVC->>Adapter: "create({values})"
Note over Adapter: Validates userName and path
Adapter->>IAM: "CreateUserCommand({UserName, Path?})"
IAM-->>Adapter: "{User}"
Adapter-->>Route: CloudResource
Route-->>UI: 201 JSON
UI->>Route: DELETE /api/clouds/aws/services/identity/resources/alice
Route->>SVC: deleteResource('aws', 'identity', 'alice')
SVC->>Adapter: delete('alice')
Adapter->>IAM: "DeleteUserCommand({UserName})"
alt User has attached resources
IAM-->>Adapter: DeleteConflictException
Adapter-->>Route: throws
Route-->>UI: 409 resource_conflict
else Success
IAM-->>Adapter: ok
Route-->>UI: "200 {ok: true}"
end
Reviews (2): Last reviewed commit: "fix(aws): clarify IAM request errors" | Re-trigger Greptile
| async delete(id: string): Promise<void> { | ||
| await this.iam.send(new DeleteUserCommand({UserName: id})) | ||
| } |
There was a problem hiding this comment.
delete fails silently for users with attached resources
AWS IAM DeleteUser raises DeleteConflictException if the user still has group memberships, access keys, signing certificates, MFA devices, or inline/attached policies. The adapter issues DeleteUserCommand with no pre-flight cleanup and no dedicated error handling. normalizeRuntimeError won't match the exception and maps it to a generic 502 response, so the UI shows "Runtime request failed" rather than anything actionable. Even in a local emulator context, users imported from fixtures or created outside this UI will often have access keys, making delete consistently unusable without a clearer error path.
Summary
identityCloud Explorer category and Identity sidebar entryValidation
pnpm lintpnpm type-checkpnpm dlx bun test— 168 tests passedpnpm buildidentityavailable and/cloud-explorer/aws/identityreturns 200Closes #79