Skip to content

feat(iam): add an AWS IAM adapter with a schema-declared kind facet - #162

Open
TheSaifZaman wants to merge 6 commits into
floci-io:mainfrom
TheSaifZaman:feat/iam-category
Open

feat(iam): add an AWS IAM adapter with a schema-declared kind facet#162
TheSaifZaman wants to merge 6 commits into
floci-io:mainfrom
TheSaifZaman:feat/iam-category

Conversation

@TheSaifZaman

Copy link
Copy Markdown
Contributor

Adds an iam service under Security covering users, roles and customer-managed
policies — and the small SPI change it needs.

Why the SPI change ships with a consumer

IAM holds three kinds of resource in one category, and kind=roles is not something
a free-text search box can express. So this adds ResourceQuery.filters, populated
by the list route.

I deliberately did not send the facet plumbing as its own PR. sortable and
copyable were removed from TableColumnSchema in review for being unused, so
schema surface with no consumer gets rejected here — rightly. This lands the facet
and its first real user together.

filters is populated only for the facet names a service's own schema declares,
so an undeclared or stale query param is ignored rather than reaching an adapter that
never asked for it — the same way the catalog gates unknown service slugs. search
stays a separate field, so every existing adapter is untouched.

Decisions that follow from the API's shape

  • Resource ids are kind/identifieruser/alice, role/deployer, and
    policy/{arn} for policies, because GetPolicy and DeletePolicy take an ARN
    rather than a name. A bare name would be ambiguous across the three kinds and
    would not tell get or delete which API to call. The ids survive the generic
    route because HttpClient.ts:257 encodes path params — verified with an encoded
    ARN, not assumed.
  • Policies are listed with Scope: 'Local'. Real IAM otherwise returns close to
    a thousand AWS-managed policies and buries the account's own. The emulator returns
    few enough that only a test can hold this line.
  • A role's trust policy is decoded from the percent-encoded form IAM returns,
    which is unreadable raw in the inspector.
  • create validates per kind: a role needs a trust policy, a policy needs a
    document, and both must parse as JSON. The message names the field and the kind,
    because a flat form cannot mark a field conditionally required and the form shows
    both for every kind.
  • Every List call pages on IsTruncated/Marker.

Verification

lint, type-check, test and build pass from the repo root; both lockfiles
install --frozen-lockfile clean.

25 adapter tests plus 4 route tests for the facet plumbing. Hermetic — the IAM,
route, capability-guard and catalog suites (149 tests) pass with globalThis.fetch
replaced by a throw. I also confirmed the route tests genuinely fail without the
plumbing rather than passing by construction.

End to end through the route: nav entry, create for all three kinds, list with and
without the facet, an undeclared param being ignored, an unknown facet value
returning 400, inspect by encoded id including a policy ARN, the decoded trust
policy, delete for each kind, and 404 for a missing entity.

Merge note

Branched off main. Adds 'iam-user' | 'iam-role' | 'iam-policy' to
CloudResource.type, which #156#161 also widen with their own members, so the last
to merge resolves a one-line conflict. The ResourceQuery and route changes are
additive and touched by no other open PR.

Follow-ups this unblocks: GCP IAM service accounts, and attach/detach of policies
once there is a generic resource actions route.

@TheSaifZaman
TheSaifZaman requested a review from hectorvent as a code owner July 28, 2026 10:05
Comment thread packages/api/src/adapter-aws/AwsIamAdapter.ts
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds an AWS IAM adapter (users, roles, customer-managed policies) plus schema-declared kind facet plumbing on list routes.

  • Registers @aws-sdk/client-iam and wires IAMClient into AWS clients.
  • Introduces AwsIamAdapter with list/get/create/delete, kind-prefixed ids, Local-scoped policy listing, and policy-document load on inspect.
  • Extends ResourceQuery.filters, catalog/schema, and route tests so only schema-declared facets reach adapters.

Confidence Score: 5/5

The PR appears safe to merge; the prior policy-inspect gap is addressed and no blocking failure remains.

Policy get loads the default version with GetPolicyVersion, decodes the document for inspect, degrades with policyDocumentUnavailable on version failure, and list still avoids N+1 version calls. No remaining blocking failure on the changed path.

Important Files Changed

Filename Overview
packages/api/src/adapter-aws/AwsIamAdapter.ts IAM adapter with kind-prefixed ids; policy inspect loads default version via GetPolicyVersion and surfaces decoded document (or unavailable flag).
packages/api/src/adapter-aws/AwsIamAdapter.test.ts Covers kind facet, pagination, create validation, and policy-document inspect including encoded/literal docs and version-lookup failure.
packages/api/src/routes/clouds.ts Populates ResourceQuery.filters only for schema-declared facet names.
packages/api/src/cloud-spi/types.ts Adds filters on ResourceQuery and iam resource types.

Reviews (5): Last reviewed commit: "docs(iam): state that the kind facet is ..." | Re-trigger Greptile

@hectorvent hectorvent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for a genuinely careful adapter. The details show real attention to the IAM contract: paging every List call on IsTruncated and Marker, listing policies with Scope Local so the account's own policies are not buried (with a test holding that line), decoding the percent-encoded trust policy, fetching the policy document only on inspect to avoid an N+1, and keying policies by ARN because GetPolicy and DeletePolicy take one. The hermetic test suite and the schema-gated facet plumbing in the route are both well thought through, and both lockfiles are regenerated, which is the most common CI trap here.

One coordination note before the substance: PR #145 has been adding AWS IAM to the Cloud Explorer since July 18 and is actively iterating through review, and the two PRs touch the same files (adapter, schema, catalog row, and the shared type unions). That is nobody's fault, but the direction (one identity category starting with users, or this three-kind service with a facet) is mine to decide, and I am working through it now. It would be worth holding off on rework here until that lands, so neither of you burns time rebasing twice.

On the merits, two questions:

  1. The kind facet currently has no way to be used from the UI. DynamicResourceView only sends search (cloudProxyClient passes just that one param), and schema.filters is not rendered as controls anywhere in the frontend, so today the facet is reachable only by calling the API directly. Given your own (good) argument that schema surface needs a consumer landing with it, could you add the small piece that renders non-search filters from the schema (a select next to the search box in DynamicResourceView would do), or say explicitly in the PR that the facet is API-only for now and the UI control is a follow-up? Either is fine, it just should be a stated choice rather than a gap.

  2. Nit, not a blocker: the README table row looks hand-added. AGENTS.md asks for the table to be regenerated with bun run scripts/service-matrix.ts so it cannot drift; could you confirm the row matches the script's output?

Everything else (catalog registration, typed errors, capability guard, tests colocated, no fake data, real empty states) follows the current pattern exactly. Nice work.

Both lockfiles updated with the manifest: CI installs the workspace with
pnpm install --frozen-lockfile and the api package with
bun install --frozen-lockfile, so updating only one fails a job.
Adds `ResourceQuery.filters`, populated by the list route from query
params — but only for the facet names the service's own schema declares
in `filters`. An undeclared or stale param is ignored rather than reaching
an adapter that never asked for it, the same way the service catalog gates
unknown service slugs.

Free-text search cannot express "only roles", which is what a category
holding several kinds of resource needs. `search` stays a separate field
on ResourceQuery, so every existing adapter is untouched.
Adds an `iam` service under Security covering users, roles and
customer-managed policies, and is the first consumer of the schema-declared
query facets added in the previous commit.

The facet is the point: IAM holds three kinds of resource in one category,
and `kind=roles` is not something a search box can express. Unset, list
returns all three together, which is what an audit view wants.

Decisions that follow from the API's shape:

- Resource ids are `kind/identifier` — user/alice, role/deployer, and
  policy/{arn} for policies, because GetPolicy and DeletePolicy take an
  ARN rather than a name. A bare name would be ambiguous across kinds and
  would not tell get or delete which API to call. The ids survive the
  generic route because HttpClient encodes path params.
- Policies are listed with Scope=Local. Real IAM otherwise returns close
  to a thousand AWS-managed policies and buries the account's own; the
  emulator returns few enough that only a test can hold this line.
- A role's trust policy is decoded from the percent-encoded form IAM
  returns, which is unreadable raw in the inspector.
- create validates per kind: a role needs a trust policy and a policy
  needs a document, both of which must parse as JSON. The message names
  the field and the kind, because the form shows both fields for every
  kind and a flat form cannot mark them conditionally required.
- Every List call pages on IsTruncated/Marker.

Verified end to end through the route: the nav entry, create for all three
kinds, list with and without the facet, an undeclared query param being
ignored, an unknown facet value returning 400, inspect by encoded id
including a policy ARN, the decoded trust policy, delete for each kind,
and 404 for a missing entity.
A role's inspector surfaced its decoded trust policy but a policy showed
only metadata, so the document being audited — the reason to open a policy
at all — was never visible.

get() now loads the default version with GetPolicyVersion and exposes the
decoded document. Only on inspect: doing it during list would issue one
extra call per row, which a test pins.

The decode handles both shapes. Real IAM percent-encodes the document;
the local runtime returns it literal, and decodeURIComponent leaves a
string with no escapes unchanged, so neither is corrupted.

A failed version lookup degrades the row with
metadata.policyDocumentUnavailable rather than failing the inspect, since
the rest of the policy metadata is still worth showing.
…nion

The frontend keeps its own copy of CloudResource.type — the packages are
independent and share no dependency — so widening the API union without
widening this one leaves the frontend contract behind the payloads the
API actually returns.

KnownResourceType | (string & {}). This branch is off main, where the
union is still closed, so the member is added directly; if floci-io#156 merges
first the one-line conflict resolves by moving it into KnownResourceType.
@TheSaifZaman

Copy link
Copy Markdown
Contributor Author

Rebased onto fd3bd2f after #147, #152 and #155 merged. Conflict was the CloudResource.type union in both packages against the 'secret' member #147 added; all three IAM members plus 'secret' kept. README regenerated from service-matrix.ts.

Gate green after the rebase: lint, type-check, 459 tests, build.

TheSaifZaman added a commit to TheSaifZaman/floci-ui that referenced this pull request Jul 29, 2026
Adds a `loadbalancing` service under Networking covering ELBv2 load
balancers. First of the ungated AWS categories.

Runtime contract notes:

- Only ELBv2 is implemented. A Version=2012-06-01 request comes back in
  the 2015-12-01 namespace, so there is no separate classic ELB to model.
- `State` is nested (<State><Code>active</Code></State>). Reading State
  directly yields an object and the column renders blank.
- A missing load balancer is reported with HTTP 400, not 404. The wire
  <Code> is LoadBalancerNotFound but the SDK names the modelled error
  LoadBalancerNotFoundException — matching only the wire code passed every
  stubbed test and still returned 400 against the live runtime, so both
  names are accepted and both are in awsErrors.ts.
- The runtime returns <NextMarker></NextMarker> at the end of a list,
  which the SDK surfaces as an empty string, so paging stops on falsy
  rather than undefined or it spins forever. A test pins that.
- create needs at least two subnets in different availability zones. They
  are taken as comma separated ids, following awsComputeSchema which
  handles security group ids the same way, rather than pushing create into
  a bespoke panel.

Target groups are deliberately excluded: they are a second resource kind
in this category and would need the `kind` facet from ResourceQuery.filters
(floci-io#162). Shipping load balancers alone keeps this independent of that PR.

Adds 'load-balancer' to CloudResource.type in both packages — the frontend
keeps its own union and there is no shared dependency, so a new resource
kind is always a two-package change.

Verified end to end through the route: nav entry under Networking, create
with two real subnets, list showing the state reaching active, inspect by
encoded ARN, 404 for a missing load balancer, all four validation
rejections, and delete.
@TheSaifZaman

Copy link
Copy Markdown
Contributor Author

@hectorvent Thank you, and understood on the #145 coordination — I have deliberately not reworked the adapter or the taxonomy while that decision is open, so this push is limited to the two things you asked for that are independent of it.

1. The kind facet is API-only, and it is now a stated choice rather than a gap. You are right that my own "schema surface needs a consumer" argument cuts against me here, so I have taken the second of your two options and written the reasoning into iamSchema.ts above the filter declaration: DynamicResourceView sends only search, nothing renders schema.filters as controls, so the facet is reachable only by calling the API directly.

I chose that over building the control because rendering non-search filters is shared frontend work — every category with a facet needs it, and it would collide with whatever #145 lands. It belongs in its own PR rather than half-built here. If the maintainer decision goes toward this PR's shape and you would rather the select ship with it, I am happy to add it then.

2. Confirmed the README row is generated. Re-ran bun run scripts/service-matrix.ts on the rebased branch and diffed: | Security | IAM | Yes (list, create, delete, inspect) | No | No | matches the script output exactly. It was hand-added originally, so thank you for catching it — the rebase regenerated the whole table anyway, since #147 changed the Security rows.

On the FieldType limitation: #167's review turned up the same wall from the other side — an ASL definition needs a textarea, this needs a filter control, and FieldType knows only text and select. That looks like one small SPI PR that unblocks both.

Gate green: lint, type-check, 459 tests, build.

fredpena added a commit that referenced this pull request Aug 31, 2026
Adds a `loadbalancing` service under Networking covering ELBv2 load
balancers. First
of the ungated AWS categories.

## Runtime contract notes

- **Only ELBv2 is implemented.** A `Version=2012-06-01` request comes
back in the
  `2015-12-01` namespace, so there is no separate classic ELB to model.
- **`State` is nested** (`<State><Code>active</Code></State>`). Reading
`State`
  directly yields an object and the column renders blank.
- **A missing load balancer is HTTP 400, not 404** — the fourth service
where this
  runtime does that. Worth reading closely: the wire `<Code>` is
  `LoadBalancerNotFound`, but the SDK names the modelled error
`LoadBalancerNotFoundException`. Matching only the wire code **passed
every stubbed
test and still returned 400 against the live runtime**, because my stub
used the
code I had seen in `curl` rather than the name the SDK produces. Both
names are now
  accepted, both are in `awsErrors.ts`, and there is a test for each.
- **The runtime returns `<NextMarker></NextMarker>`** at the end of a
list, which the
SDK surfaces as an empty string — paging stops on falsy rather than
`undefined`, or
  it spins forever. A test pins that.
- **`create` needs at least two subnets in different AZs.** They are
taken as comma
separated ids, following `awsComputeSchema`, which handles security
group ids the
same way — rather than pushing create into a bespoke panel as the AWS
networking
  flows needed.

## Scope

Target groups are deliberately excluded. They are a second resource kind
in this
category and would need the `kind` facet from `ResourceQuery.filters`
(#162, still
open). Shipping load balancers alone keeps this PR independent of that
one; target
groups are a clean follow-up once #162 lands.

## Verification

`lint`, `type-check`, `test` and `build` pass from the repo root. 15
adapter tests.

End to end through the route: nav entry under Networking, create with
two real
subnets from the default VPC, list showing the state reaching `active`,
inspect by
encoded ARN, **404** for a missing load balancer, all four validation
rejections, and
delete.

## Merge note

Adds `'load-balancer'` to `CloudResource.type` in **both** packages. The
frontend
keeps its own union and there is no shared dependency, so a new resource
kind is
always a two-package change — #156 makes that permanently safe by
opening the
frontend type. Also adds four entries to the not-found `Set` in
`awsErrors.ts`.

---------

Co-authored-by: fredpena <f.ant.pena@gmail.com>
fredpena pushed a commit to TheSaifZaman/floci-ui that referenced this pull request Sep 1, 2026
# [0.4.0](floci-io/floci-ui@0.3.0...0.4.0) (2026-09-01)

### Bug Fixes

* **ec2:** include catalog AMIs in launch selector ([floci-io#191](floci-io#191)) ([b72135d](floci-io@b72135d))

### Features

* **aws:** add CloudFormation adapter to Cloud Explorer ([floci-io#184](floci-io#184)) ([f3d6105](floci-io@f3d6105)), closes [floci-io#81](floci-io#81) [floci-io#75](floci-io#75) [floci-io#81](floci-io#81) [floci-io#75](floci-io#75) [floci-io#81](floci-io#81)
* **aws:** add EventBridge explorer ([floci-io#146](floci-io#146)) ([42944e9](floci-io@42944e9)), closes [floci-io#85](floci-io#85)
* **aws:** add IAM to Cloud Explorer ([floci-io#145](floci-io#145)) ([fc50d19](floci-io@fc50d19)), closes [floci-io#79](floci-io#79)
* **aws:** add Secrets Manager resource adapter ([floci-io#193](floci-io#193)) ([4811866](floci-io@4811866))
* **azure:** add databases and split Cosmos NoSQL ([floci-io#149](floci-io#149)) ([2e704f4](floci-io@2e704f4)), closes [floci-io#92](floci-io#92) [floci-io#67](floci-io#67) [floci-io/floci-az#138](floci-io/floci-az#138) [floci-io#143](floci-io#143)
* **azure:** add Service Bus explorer ([floci-io#144](floci-io#144)) ([a9f0d06](floci-io@a9f0d06)), closes [floci-io#89](floci-io#89)
* **eks:** Manage nodegroups and Fargate profiles via Cloud Proxy ([floci-io#194](floci-io#194)) ([3091cc9](floci-io@3091cc9)), closes [floci-io#106](floci-io#106)
* **loadbalancing:** add an AWS Elastic Load Balancing adapter ([floci-io#168](floci-io#168)) ([10d4298](floci-io@10d4298)), closes [floci-io#162](floci-io#162) [floci-io#162](floci-io#162) [floci-io#156](floci-io#156)
* **messaging:** add a messaging category with SQS and Pub/Sub ([floci-io#157](floci-io#157)) ([dfa6d1c](floci-io@dfa6d1c)), closes [floci-io#155](floci-io#155) [floci-io#155](floci-io#155) [floci-io#156](floci-io#156) [floci-io#156](floci-io#156)
* **secretsmanager:** add JSON key-value editor for secret values ([floci-io#195](floci-io#195)) ([8e88961](floci-io@8e88961)), closes [floci-io#151](floci-io#151)
* **ses:** Add AWS SES mailbox to Cloud Explorer ([floci-io#196](floci-io#196)) ([6389a56](floci-io@6389a56)), closes [floci-io#130](floci-io#130)
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.

2 participants