Skip to content

feat(aws): add IAM to Cloud Explorer#145

Open
thomhurst wants to merge 2 commits into
floci-io:mainfrom
thomhurst:agent/aws-iam-cloud-explorer
Open

feat(aws): add IAM to Cloud Explorer#145
thomhurst wants to merge 2 commits into
floci-io:mainfrom
thomhurst:agent/aws-iam-cloud-explorer

Conversation

@thomhurst

@thomhurst thomhurst commented Jul 18, 2026

Copy link
Copy Markdown

Summary

  • add the generic identity Cloud Explorer category and Identity sidebar entry
  • implement account-scoped AWS IAM user list, create, delete, and inspect operations through the cloud SPI
  • add IAM schema validation, pagination, normalized resource metadata, documentation, and adapter/route coverage

Validation

  • pnpm lint
  • pnpm type-check
  • pnpm dlx bun test — 168 tests passed
  • pnpm build
  • HTTP smoke check: AWS services reports identity available and /cloud-explorer/aws/identity returns 200

Closes #79

image

@thomhurst
thomhurst requested a review from hectorvent as a code owner July 18, 2026 11:53
@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds AWS IAM user management to the Cloud Explorer, following the established schema-driven multi-cloud SPI pattern precisely. Previous concerns raised in earlier reviews (delete conflict returning a silent 502, the schema pattern missing a length quantifier, and validation error messages not matching the normalizeRuntimeError catch patterns) are all resolved here.

  • Adds AwsIamAdapter supporting list (paginated), create, delete, and inspect, with client-side search filtering, proper userName/path validation, and DeleteConflictException surfaced as a 409.
  • Introduces iamSchema.ts with a consistent '^[A-Za-z0-9_+=,.@-]{1,64}$' pattern that now matches both the schema and the adapter's runtime guard.
  • Wires the identity service type end-to-end: backend types, route guard (isServiceType), CloudProxyService service listing and schema dispatch, adapter registration in cloudProxy.ts, and frontend nav/routing/label utilities.

Confidence Score: 5/5

Safe to merge — the implementation follows the established multi-cloud SPI pattern faithfully and all previously flagged concerns have been resolved.

The adapter, schema, routes, and frontend wiring are all internally consistent. Input validation messages now match the normalizeRuntimeError catch patterns, the schema regex includes the length quantifier that was previously missing, and DeleteConflictException is surfaced as a 409 rather than a silent 502. The only observation is a style note about accumulating service-specific branches in the generic error handler, which does not affect correctness.

No files require special attention. The one style note is in packages/api/src/routes/clouds.ts around the IAM-specific error wording in the generic handler.

Important Files Changed

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
Loading
%%{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
Loading

Reviews (2): Last reviewed commit: "fix(aws): clarify IAM request errors" | Re-trigger Greptile

Comment on lines +65 to +67
async delete(id: string): Promise<void> {
await this.iam.send(new DeleteUserCommand({UserName: id}))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Comment thread packages/api/src/cloud-spi/iamSchema.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(aws): IAM in Cloud Explorer

1 participant