Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 3a7ccec2ff chore: bump the docker-images group across 3 directories with 3 updates
Bumps the docker-images group with 1 update in the /build directory: golang.
Bumps the docker-images group with 1 update in the /docs/terraform-workbooks/demo-app directory: distroless/static.
Bumps the docker-images group with 1 update in the /scripts directory: debian.


Updates `golang` from `5c3e9b4` to `972b411`

Updates `distroless/static` from `e2e927e` to `18f4cc2`

Updates `debian` from `a29215f` to `62bd423`

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.27.3-bookworm
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: docker-images
- dependency-name: distroless/static
  dependency-version: nonroot
  dependency-type: direct:production
  dependency-group: docker-images
- dependency-name: debian
  dependency-version: trixie-slim
  dependency-type: direct:production
  dependency-group: docker-images
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-25 01:13:02 +00:00
199 changed files with 1096 additions and 1094 deletions
+14 -13
View File
@@ -34,7 +34,7 @@ var allowlist = []allowance{
{From: "daemon", To: "domains/ec2/awsapi/instance", Debt: "Instance adapter consumers outside EC2", Reason: "daemon/eks_cp_control.go and rds_deps.go call the instance adapter; needs an EC2 capability"},
{From: "domains/acm", To: "domains/iam", Debt: "ACM: the IAM AES-GCM helper import (ACM relocation residual)", Reason: "IAM AES-GCM helpers; needs a shared crypto boundary"},
{From: "domains/acm", To: "handlers/iam", Debt: "ACM: the IAM AES-GCM helper import (ACM relocation residual)", Reason: "IAM AES-GCM helpers; needs a shared crypto boundary"},
{From: "domains/admission/quota", To: "domains/ec2/awsapi/eip", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "describe adapter used as a counter; needs a reader capability"},
{From: "domains/admission/quota", To: "domains/ec2/awsapi/instance", Debt: "domains/admission/quota (dependency, moved from handlers/quota unchanged)", Reason: "vcpu.go counts instances through the instance adapter; needs an EC2 reader capability"},
@@ -48,17 +48,17 @@ var allowlist = []allowance{
{From: "domains/dns", To: "domains/network/reconcile", Debt: "domains/dns", Reason: "takes the reconcile lock from network's implementation"},
{From: "domains/dns", To: "runtime/compute/vm", Debt: "domains/dns", Reason: "domain-to-runtime import of VM state"},
{From: "domains/ec2/awsapi/instance", To: "domains/iam", Debt: "domains/ec2/awsapi/instance (dependency)", Reason: "IAM edge needs an IAM capability"},
{From: "domains/ec2/awsapi/instance", To: "handlers/iam", Debt: "domains/ec2/awsapi/instance (dependency)", Reason: "IAM edge needs an IAM capability"},
{From: "domains/ec2/awsapi/instance", To: "runtime/compute/cache", Debt: "domains/ec2/awsapi/instance (dependency)", Reason: "node liveness and VM state; needs an EC2-owned projection"},
{From: "domains/ec2/awsapi/instance", To: "runtime/compute/vm", Debt: "domains/ec2/awsapi/instance (dependency)", Reason: "node liveness and VM state; needs an EC2-owned projection"},
{From: "domains/ec2/awsapi/spotinstance", To: "domains/admission/quota", Debt: "domains/ec2/awsapi/spotinstance (dependency)", Reason: "RequestSpotInstances.go uses IAMService and quota.Service"},
{From: "domains/ec2/awsapi/spotinstance", To: "domains/iam", Debt: "domains/ec2/awsapi/spotinstance (dependency)", Reason: "RequestSpotInstances.go uses IAMService and quota.Service"},
{From: "domains/ec2/awsapi/spotinstance", To: "handlers/iam", Debt: "domains/ec2/awsapi/spotinstance (dependency)", Reason: "RequestSpotInstances.go uses IAMService and quota.Service"},
{From: "domains/ec2/eip", To: "domains/network/topology", Debt: "EC2 → network implementation", Reason: "EC2 reads network implementation; clears with the EC2-owned projection"},
{From: "domains/ec2/guestmetadata", To: "domains/dns", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
{From: "domains/ec2/guestmetadata", To: "domains/iam", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
{From: "domains/ec2/guestmetadata", To: "handlers/iam", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
{From: "domains/ec2/guestmetadata", To: "handlers/sts", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
{From: "domains/ec2/guestmetadata", To: "runtime/compute/vm", Debt: "Guest metadata: the cross-domain and runtime imports above", Reason: "guest-metadata relocation residual; needs capabilities, projections or contracts"},
@@ -76,7 +76,7 @@ var allowlist = []allowance{
{From: "domains/ec2/vpc", To: "domains/network/identifiers", Debt: "EC2 → network implementation", Reason: "EC2 reads network implementation; clears with the EC2-owned projection"},
{From: "domains/ec2/vpc", To: "domains/network/topology", Debt: "EC2 → network implementation", Reason: "EC2 reads network implementation; clears with the EC2-owned projection"},
{From: "domains/ecr/auth", To: "domains/iam", Debt: "Cross-domain", Reason: "IAM implementation import; needs an IAM capability"},
{From: "domains/ecr/auth", To: "handlers/iam", Debt: "Cross-domain", Reason: "IAM implementation import; needs an IAM capability"},
{From: "domains/network/host", To: "runtime/compute/vm", Debt: "domains/network", Reason: "domain-to-runtime import"},
{From: "domains/network/host", To: "runtime/host/command", Debt: "domains/network", Reason: "domain-to-runtime import"},
@@ -93,7 +93,7 @@ var allowlist = []allowance{
{From: "domains/ochre", To: "gateway/bedrock", Debt: "gateway/bedrock", Reason: "Ochre domain core still lives in gateway/bedrock; needs the recorded split"},
{From: "domains/ochre", To: "runtime/compute/gpu", Debt: "Domain → runtime", Reason: "GPU runtime state read directly; needs a domain-owned projection"},
{From: "domains/ochre/vector", To: "domains/iam", Debt: "Cross-domain", Reason: "consumes another domain's implementation rather than a capability"},
{From: "domains/ochre/vector", To: "handlers/iam", Debt: "Cross-domain", Reason: "consumes another domain's implementation rather than a capability"},
{From: "gateway", To: "domains/ec2/awsapi", Debt: "gateway/ec2.go (registration)", Reason: "EC2 dispatch map stays in gateway until the ingress registration seam"},
{From: "gateway", To: "domains/ec2/awsapi/account", Debt: "gateway/ec2.go (registration)", Reason: "EC2 dispatch map stays in gateway until the ingress registration seam"},
@@ -116,17 +116,17 @@ var allowlist = []allowance{
{From: "gateway", To: "domains/ec2/awsapi/vpc", Debt: "gateway/ec2.go (registration)", Reason: "EC2 dispatch map stays in gateway until the ingress registration seam"},
{From: "gateway", To: "domains/ec2/awsapi/zone", Debt: "gateway/ec2.go (registration)", Reason: "EC2 dispatch map stays in gateway until the ingress registration seam"},
{From: "gateway/bedrock", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "secret encrypt/decrypt helpers; needs a shared crypto boundary"},
{From: "gateway/bedrock", To: "handlers/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "secret encrypt/decrypt helpers; needs a shared crypto boundary"},
{From: "gateway/rds", To: "domains/ec2/awsapi/instance", Debt: "Instance adapter consumers outside EC2", Reason: "enginecatalog.go calls the instance adapter; needs an EC2 capability"},
{From: "gateway/sts", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "GetCallerIdentity takes IAMService; needs an IAM capability"},
{From: "gateway/sts", To: "handlers/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "GetCallerIdentity takes IAMService; needs an IAM capability"},
{From: "handlers/ecs", To: "domains/ec2/ebs/metadata", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "handlers/ecs", To: "domains/ec2/eip", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "handlers/ecs", To: "domains/ec2/instancetypes", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "handlers/ecs", To: "handlers/elbv2", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "handlers/ecs", To: "domains/iam", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "handlers/ecs", To: "handlers/iam", Debt: "close-out: handlers/ecs (ADR-0004 blocked-dependency list; inventory-ecs boundary imports)", Reason: "legacy ECS calls another domain's implementation as an internal API"},
{From: "handlers/eks", To: "domains/dns", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
{From: "handlers/eks", To: "domains/ec2/ebs/metadata", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
@@ -134,7 +134,7 @@ var allowlist = []allowance{
{From: "handlers/eks", To: "domains/ec2/placementgroup", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
{From: "handlers/eks", To: "domains/ec2/systeminstance", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
{From: "handlers/eks", To: "domains/network/systemvpc", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
{From: "handlers/eks", To: "domains/iam", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
{From: "handlers/eks", To: "handlers/iam", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
{From: "handlers/eks", To: "runtime/compute/vm", Debt: "close-out: handlers/eks (ADR-0004 blocked-dependency list; inventory-eks boundary imports)", Reason: "legacy EKS calls another domain's or runtime implementation directly"},
{From: "handlers/elbv2", To: "domains/acm", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
@@ -142,7 +142,7 @@ var allowlist = []allowance{
{From: "handlers/elbv2", To: "domains/ec2/systeminstance", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From: "handlers/elbv2", To: "domains/ec2/vpc", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From: "handlers/elbv2", To: "domains/network/topology", Debt: "handlers/elbv2 (cross-domain)", Reason: "legacy ELBv2 cross-domain import; clears when ELBv2 is extracted behind consumer-owned capabilities"},
{From: "handlers/elbv2", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "system-instance role ensurer; needs an IAM capability"},
{From: "handlers/elbv2", To: "handlers/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "system-instance role ensurer; needs an IAM capability"},
{From: "handlers/elbv2", To: "lbagent", Debt: "handlers/elbv2/{agent_types,haproxy,health_checker,nginx}.go import lbagent", Reason: "health-report and cert/PID-path vocabulary; needs a guest/controller contract"},
{From: "handlers/rds", To: "domains/dns", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
@@ -150,14 +150,15 @@ var allowlist = []allowance{
{From: "handlers/rds", To: "domains/ec2/instancetypes", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
{From: "handlers/rds", To: "domains/ec2/systeminstance", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
{From: "handlers/rds", To: "domains/network/systemvpc", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
{From: "handlers/rds", To: "domains/iam", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
{From: "handlers/rds", To: "handlers/iam", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
{From: "handlers/rds", To: "runtime/compute/vm", Debt: "close-out: handlers/rds (ADR-0004 blocked-dependency list; inventory-rds boundary imports)", Reason: "legacy RDS calls another domain's or runtime implementation directly"},
{From: "handlers/sts", To: "domains/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "STS reads IAM service, trust-policy and OIDC-provider state; needs an IAM capability"},
{From: "handlers/sts", To: "handlers/iam", Debt: "IAM implementation consumers in STS, Ochre and ELBv2", Reason: "STS reads IAM service, trust-policy and OIDC-provider state; needs an IAM capability"},
{From: "operator/cli", To: "accountteardown", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
{From: "operator/cli", To: "daemon", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
{From: "operator/cli", To: "gateway", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
{From: "operator/cli", To: "gateway/bedrock", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
{From: "operator/cli", To: "handlers/eks", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
{From: "operator/cli", To: "handlers/iam", Debt: "operator/cli (dependency)", Reason: "command tree imports implementations; needs public capabilities"},
}
+2
View File
@@ -24,6 +24,8 @@ var frozenLegacyPackages = []string{
"handlers/ecs",
"handlers/eks",
"handlers/elbv2",
"handlers/iam",
"handlers/iam/mock",
"handlers/rds",
"handlers/sts",
"lbagent",
+1 -1
View File
@@ -1,6 +1,6 @@
ARG VERSION=dev
FROM golang:1.27.3-bookworm@sha256:5c3e9b43cfd3bb95c7937462e29d87ada57b481a96fa23ad0eb61778d7a00110 AS builder
FROM golang:1.27.3-bookworm@sha256:972b4110ba0f47539a975c31e52c64765e1ff19f2d74469ae4a595c6676e80a5 AS builder
ARG VERSION
+9 -10
View File
@@ -340,7 +340,6 @@ while the branch is in flight; they do not describe the umbrella repository's
| `0d830e901` | — | `spinifex/domains/tagging` (`TaggedLister`, `TaggedResource`) | The read-only list capability for the Resource Groups Tagging aggregation, declared by its consumer per ADR-0001. `ListTagged` takes the caller's account and the lower-cased `ResourceTypeFilters` set, where an entry is a full `service:type` or a bare service, and returns ARNs with read-only tag maps. The package imports no domain, handler or contract package and the layering ratchet classifies it with no allowlist row. It carries no mutation, limit or ever-tagged surface. |
| `f3545e3ea` | `spinifex/gateway/tagging` (`listEC2`, `buildEC2Mappings`, `ec2ARN`, `elbv2Resources`, `matchesType`), copied | `spinifex/runtime/roles/awsgw` (`ec2IndexLister`, `elbv2Lister`), `spinifex/domains/tagging` (`MatchesType`) | AWSGW-side NATS client adapters implement `tagging.TaggedLister`, each with its compile-time check beside it in the composition root; no owner package carries one. The EC2 index adapter wraps `ec2.DescribeTags` and builds each ARN in the gateway's region from the stored resource type, including the unresolved-ID shapes today's code produces. The ELBv2 adapter lists load balancers and target groups admitted by the type filter, tagged or not, and reads tags in `DescribeTags` batches of 20. Both reuse existing subjects and clients, so no wire contract is added, and each call builds fresh tag maps. Type matching is exported once from `domains/tagging` so no copy survives the move. Embedded-JetStream tests cover type filters including a bare service, untagged ELBv2 resources, EC2 ARN mapping, account scoping, batch sizes and owner error propagation. |
| `aedd0b2c4` | `spinifex/gateway/tagging` (`GetResources` parsing, tag filtering, sorting, offset token, authorization, response writing) | `spinifex/domains/tagging` (`Aggregator`, `NewAggregator`, `HasScope`, `ScopedActions`), wired by `spinifex/runtime/roles/awsgw` | The aggregation moves unchanged and consumes `[]TaggedLister`: ELBv2 is called before EC2, the non-stable ARN sort is kept, lister errors propagate with the same codes, and tag maps return as key-sorted tag lists with `[]` for untagged resources. AWSGW builds the aggregator with both adapters and sets the new `GatewayConfig.TaggingResources` field; the dispatch in `gateway/tagging.go` stays and still answers `ServerInternal` when NATS or the aggregator is not wired. The `TestCurrentBehaviour_Tagging_GetResources*` tests move to `runtime/roles/awsgw` with unchanged assertions, seeding EC2 tags through the existing EC2 tags client over NATS; pure aggregator tests move to `domains/tagging` against fake listers; the gateway keeps dispatch and error-path tests. The gateway's private copies of the listing helpers and the legacy NATS lister are deleted with their tests, `gateway/tagging` is removed, and its two allowlist rows (`gateway/tagging` → `domains/ec2/tags`, `gateway/tagging` → `handlers/elbv2`) and its frozen-legacy entry go with it. |
| `spinifex#1249` | `spinifex/handlers/iam`, `spinifex/handlers/iam/mock` | `spinifex/domains/iam` (`iam`, imported as `iamdomain`), `spinifex/domains/iam/mock` | A mechanical relocation of IAM's implementation to its ADR-0001 home; APIs, NATS subjects, KV buckets and behaviour are unchanged and only the package name changed from `handlers_iam`. Per-consumer IAM capabilities remain dependency-rule debt but were not a prerequisite. The two frozen-legacy entries are removed, the cross-domain allowlist entries now target `domains/iam`, and the `operator/cli` → `handlers/iam` entry is removed because an operator import of a domain package does not break the legacy-implementation rule. |
The EC2 contract row is intentionally different from the directory moves: it creates
a compatibility boundary. `ec2.cmd.*` retains its deployed one-token NATS
subscription and permission shape, while callers use
@@ -408,7 +407,7 @@ These residuals were recorded by individual slices and must be resolved once the
- Guest metadata: the cross-domain and runtime imports above.
- EC2 → network implementation: `domains/ec2/vpc/eni.go:18` and `eni_lease_rebind.go:11` import `domains/network/topology`, `eni.go:17` also imports `domains/network/identifiers`, `external_ipam.go:10-11` imports `domains/network/external` and `domains/network/external/dhcp`, `domains/ec2/eip/service_impl.go:17` and `lease_rebind.go:11` import `domains/network/topology`, and `domains/ec2/instance/service_impl.go:31` imports `domains/network/topology`. EC2 reads network's implementation directly rather than through a capability or projection; this clears when EC2's network-facing reads and writes go through the EC2-owned projection recorded above (step 7).
- Cross-domain: `domains/ecr/auth/signingkey.go:24` and `domains/ochre/vector/appliance.go:15` import `domains/iam`; this needs an IAM capability rather than the IAM implementation package. `domains/ochre/launch.go:17-18` and `domains/ochre/vpc.go:9` import `domains/ec2/systeminstance` and `domains/network/systemvpc`; Ochre consumes managed-service launch and system-VPC realisation as domain implementations rather than capabilities.
- Cross-domain: `domains/ecr/auth/signingkey.go:24` and `domains/ochre/vector/appliance.go:15` import `handlers/iam`; this needs an IAM capability rather than the IAM implementation package. `domains/ochre/launch.go:17-18` and `domains/ochre/vpc.go:9` import `domains/ec2/systeminstance` and `domains/network/systemvpc`; Ochre consumes managed-service launch and system-VPC realisation as domain implementations rather than capabilities.
- Domain → runtime: `domains/ochre/capacity.go:9` imports `runtime/compute/gpu`; `domains/ec2/volume/service_impl.go:30` and `volume_leak_reaper.go:8` import `runtime/compute/vm`; `domains/ec2/instance/status_selection.go:9` also imports `runtime/compute/vm`. Each reads VM or GPU runtime state directly instead of through a domain-owned projection.
- STS web-identity verification never consults cluster status: tokens verify while a cluster is DELETING until teardown sweeps the prefix, and a FAILED launch keeps its JWKS. AWS behaviour must be established before calling it a divergence. Removal condition: AWS behaviour verified, and the resolver or EKS lifecycle aligned with it.
- The public `/keys` endpoint (root `gateway`, `oidc_discovery.go`) serves malformed stored JWKS bytes with HTTP 200. Removal condition: validate before serving, or record it as accepted.
@@ -449,8 +448,8 @@ These residuals were recorded by individual slices and must be resolved once the
- `gateway/requestaudit.go`: the outermost request-audit middleware records AWS fields and is updated from four places in the gateway's authentication flow through the gateway's own context key. Moving it would export about seven names used only by that flow, so it stays in `gateway` until the generic ingress registration seam defines how ingress middleware receives authentication results.
- `accountteardown` (dependency): its EC2 reapers call `domains/ec2/awsapi/*` action adapters directly, as they did under `gateway/ec2`. The ADR's `accountteardown` current-to-target map row replaces this with domain-owned cleanup capabilities before the package moves. It also holds a read-only consumer of the EC2 platform-inventory contract (`contracts/ec2/v1`, `ec2.v1.PlatformInventory`), served by `domains/ec2/accountinventory`; only the explicit dry run (`Engine.DryRun`) calls it, to log a shadow comparison, and the key-pair, placement-group and launch-template reapers still use the `awsapi` adapters. A domain-owned cleanup capability for the proven-safe subset of those three kinds (key pairs and empty placement groups no instance, spot request or live launch-template version names, and launch-template residue; never live launch templates) exists as `domains/ec2/accountcleanup` (a Go interface with injected owners and reference readers, no NATS subject); nothing calls it yet. It also holds the StageAccount settings-seal reaper, a consumer of `contracts/ec2/v1`'s `ec2.v1.SealAccountSettings` and per-node `ec2.v1.AccountSettingsSealCapability.<node>` subjects, served by `domains/ec2/account` through the daemon; the reaper reads no KV and imports no domain package, reaching both subjects only through NATS and the configured node set `NewClusterEngine` already receives.
- `domains/admission/quota` (dependency, moved from `handlers/quota` unchanged): `live.go` imports `gateway/elbv2` and `handlers/rds` for its live ELBv2 and RDS counters; `records.go` imports `runtime/compute/vm` for the instance-record vCPU sweep; `live.go`, `vcpu.go` and `storage.go` call `domains/ec2/awsapi/{vpc,eip,volume}` describe adapters to count VPCs, subnets, addresses and volumes, as they did under `gateway/ec2`. The ADR's `handlers/quota` current-to-target map row replaces these with reader capabilities once their final locations exist.
- `domains/ec2/awsapi/instance` (dependency): imports `domains/iam` (`IAMService` in `DescribeInstances.go`, `RunInstances.go` and `IamInstanceProfileAssociation.go`; `InstanceProfile` in the latter two), `runtime/compute/cache` (`NodeState`, `NodeUnknown`, `NodeLive`) and `runtime/compute/vm` (`VM`), both in `status_synthesis.go`. The IAM edge needs an IAM capability; the runtime edges are domain-to-runtime and need the instance record and node liveness exposed through an EC2-owned projection.
- `domains/ec2/awsapi/spotinstance` (dependency): imports `domains/iam.IAMService` and `domains/admission/quota.Service` in `RequestSpotInstances.go`.
- `domains/ec2/awsapi/instance` (dependency): imports `handlers/iam` (`IAMService` in `DescribeInstances.go`, `RunInstances.go` and `IamInstanceProfileAssociation.go`; `InstanceProfile` in the latter two), `runtime/compute/cache` (`NodeState`, `NodeUnknown`, `NodeLive`) and `runtime/compute/vm` (`VM`), both in `status_synthesis.go`. The IAM edge needs an IAM capability; the runtime edges are domain-to-runtime and need the instance record and node liveness exposed through an EC2-owned projection.
- `domains/ec2/awsapi/spotinstance` (dependency): imports `handlers/iam.IAMService` and `domains/admission/quota.Service` in `RequestSpotInstances.go`.
- Instance adapter consumers outside EC2: `gateway/rds/enginecatalog.go`, `daemon/eks_cp_control.go`, `daemon/rds_deps.go`, `domains/admission/quota`'s `vcpu.go` and `gateway/iam.go` (`CountInstanceProfileAssociations`) call `domains/ec2/awsapi/instance` directly; each needs an EC2 capability rather than the AWS action adapter.
Enforcement: the owner decided the layering test is blocking now, with a ratchet. Its allowlist mirrors this debt list exactly, each edge carrying its owner and removal condition; any new unrecorded forbidden edge fails local preflight and CI, and an allowlisted edge that no longer exists must be removed from the list.
@@ -464,7 +463,7 @@ Enforcement: the owner decided the layering test is blocking now, with a ratchet
- `agents/rds/agent` (dependency, narrowed): engine metadata now arrives through the agent-declared `EngineCatalog`, but the agent still imports `handlers/rds` for the command protocol, wire and record types (`Command*`, `Parameter`, `EngineHealth*`, `BootstrapMode*`) and API output types. This clears when those move behind a guest/controller wire contract.
- `gateway/rds` → `handlers/rds` (`InstanceSizing`): the gateway obtains its `Sizing` through the RDS handler package's adapter rather than its own composition root. This clears when the gateway's RDS composition moves to `runtime/roles/awsgw` and builds `Sizing` there.
- `domains/admission/quota` → `domains/ec2/instancetypes`: `reconcile.go`, `records.go` and `vcpu.go` read `DefaultVCPUs` to size instances without a recorded vCPU count. This clears when quota receives vCPU sizing through a consumer-owned capability or an EC2 projection.
- IAM implementation consumers in STS, Ochre and ELBv2: `handlers/sts` imports `domains/iam` across five files (`IAMService`, `SessionRevoker`, `EncryptSecret`/`DecryptSecret`, `AccessKeyStatusActive`, `ValidateTrustPolicyDocument`, `PolicyEffectAllow`/`PolicyEffectDeny`, `IAMAccountBucketName`, `OIDCProviderARN`, `OIDCProviderKey`), not only `oidc_jwks.go`; `gateway/sts/GetCallerIdentity.go` takes `IAMService`; `gateway/bedrock/credentials.go` uses `EncryptSecret`/`DecryptSecret`; and `handlers/elbv2/{lb_role,service_impl}.go` use `EnsureSystemInstanceProfile` and `SystemInstanceRoleEnsurer`. Each clears when its consumer receives a narrow IAM capability, or a shared crypto boundary for the secret helpers, rather than importing the IAM implementation.
- IAM implementation consumers in STS, Ochre and ELBv2: `handlers/sts` imports `handlers/iam` across five files (`IAMService`, `SessionRevoker`, `EncryptSecret`/`DecryptSecret`, `AccessKeyStatusActive`, `ValidateTrustPolicyDocument`, `PolicyEffectAllow`/`PolicyEffectDeny`, `IAMAccountBucketName`, `OIDCProviderARN`, `OIDCProviderKey`), not only `oidc_jwks.go`; `gateway/sts/GetCallerIdentity.go` takes `IAMService`; `gateway/bedrock/credentials.go` uses `EncryptSecret`/`DecryptSecret`; and `handlers/elbv2/{lb_role,service_impl}.go` use `EnsureSystemInstanceProfile` and `SystemInstanceRoleEnsurer`. Each clears when its consumer receives a narrow IAM capability, or a shared crypto boundary for the secret helpers, rather than importing the IAM implementation.
- `handlers/elbv2` (cross-domain): `service_impl.go` imports `domains/acm` (`Store`, `NewStore`), `domains/dns` (`ELBChanges`, `ELBName`, `Change`, `ActionUpsert`, `ResolveBaseDomain`), `domains/ec2/vpc` (`VPCServiceImpl`) and `domains/network/topology` (`Port`), and `launcher.go` re-exports `domains/ec2/systeminstance` launch types as aliases. These clear when ELBv2 is extracted and each edge becomes a consumer-owned capability, which ADR-0001 sequences after the ELBv2 data-plane boundary is settled.
- `docs/service-interfaces.yaml` (ECS coverage gap, closed): the traced `ecs.bus.*` family and the ECS AWS-API action subjects are both listed. The action subjects are declared as `ecs.*`, the same wildcard convention as `rds.*`: `handlers/ecs/service_nats.go` publishes them from the gateway and `daemon/daemon.go` subscribes to them. Declaring `ecs.bus.*` alone had made `ecs` a managed prefix, so `manifest-lint` failed CI on all 34 undeclared action subjects until this was added.
- `contracts/ecs/v1` (dead routes): four of five bus subjects have no producer. The scheduler leader still subscribes to `instance-register`, `instance-heartbeat` and `task-state`, which can never fire, and `assign` has neither producer nor consumer; only `service-reconcile` is live. Removal condition: an owner decision to retire these routes (a successor contract under the compatibility rules) or to adopt them in the agent.
@@ -651,13 +650,13 @@ series above; "needs decision" is open.
| Legacy path | What remains | What blocks it |
|---|---|---|
| `handlers/iam` | — | Done. The implementation moved to `domains/iam` (package `iam`) in `spinifex#1249` with no API, NATS subject, storage or behaviour change. ADR-0001's row says a shared IAM contract is not inferred from current KV readers, and the move adds none. The cross-domain readers (the EC2 instance and spot-instance AWS adapters, `handlers/sts`, `gateway/sts`, `gateway/bedrock`, the ELBv2 system-instance role path and ECR/Ochre signing-key crypto) still import the implementation and each still needs an IAM capability; that is recorded dependency-rule debt, with its allowlist entries now targeting `domains/iam`, not a prerequisite to relocating IAM's implementation. |
| `handlers/sts` | Everything except the EKS OIDC JWKS lookup, which now reaches `handlers/eks` only through the injected `ClusterJWKSResolver`. | Deferred: per-consumer IAM capabilities (dependency-rule debt above); `handlers/sts` still imports `domains/iam` directly. |
| `handlers/iam` | Private identity state and credential issuance; consumed by the EC2 instance and spot-instance AWS adapters, `handlers/sts`, `gateway/sts`, `gateway/bedrock`, the ELBv2 system-instance role path, ECR/Ochre signing-key crypto and `operator/cli`. | Deferred: the ADR's `handlers/iam` row states a shared IAM contract is not inferred from current KV readers; each cross-domain reader needs an IAM capability first. |
| `handlers/sts` | Everything except the EKS OIDC JWKS lookup, which now reaches `handlers/eks` only through the injected `ClusterJWKSResolver`. | Deferred: per-consumer IAM capabilities (dependency-rule debt above); `handlers/sts` still imports `handlers/iam` directly. |
| `handlers/ecs` | Task, service and capacity resources (`taskdefinition` already extracted to `domains/ecs/taskdefinition`). | Deferred: ADR-0004 blocked-dependency list — `iam:PassRole` in the gateway adapter and EC2/ELBv2/EIP adapters called as internal APIs. |
| `handlers/eks` | Nodegroup and cluster resources (`access` and `addon` already extracted). | Deferred: ADR-0004 blocked-dependency list — IAM role/instance-profile creation, Bluebottle `auth`, the NATS CA, and EKS identity (token review, OIDC). |
| `handlers/rds` | The RDS instance resource (subnet group, parameter group and engine catalogue already extracted). | Deferred: ADR-0004 blocked-dependency list — quota admission, the duplicated expected-node fan-out, the IAM ensurer and the Bluebottle master key. |
| `handlers/elbv2` | Everything; not yet touched. | Deferred: ADR-0001 sequences this after the ELBv2 data-plane boundary is settled. Needs decision: what evidence establishes that boundary is settled. |
| `handlers/iam/mock` | — | Done. Moved with the implementation to `domains/iam/mock`. |
| `handlers/iam/mock` | A test double for `handlers/iam`. | Moves with `handlers/iam`; it has no independent target. |
| `gateway` (root) | Generic HTTP route assembly, SigV4/registry authentication, the policy gate, request-audit middleware, request-context keys, and the legacy per-service dispatch switch for every service not yet registered. | Deferred: the ADR-0002 phases above (auth/IAM and request-context boundary design) gate moving the audit, context-key and legacy-dispatch mechanics. |
| `gateway/bedrock` | The Ochre domain core, its stores and the inference engine, together with the Bedrock action layer, in one package with internal cycles. | Deferred: needs the split plan recorded above — domain core and stores to `domains/ochre/<resource>`, inference to an Ochre-owned package, and only the action layer to `domains/ochre/awsapi`. |
| `gateway/{ecs,eks,rds,iam,sts,elbv2}` | Each service's AWS action adapters and dispatch, not yet registered through ADR-0002. | Deferred: the ADR-0002 registration contract has only reached ECR and ACM so far; each service needs its own registration slice. |
@@ -683,9 +682,9 @@ ADR-0001 completion is structural, so none of these is cleared by remediating se
| Explicitly deferred by ADR-0001 | `daemon` (Q-28), `lbagent` (Q-30) | Closed for this refactor; the ADR names the prerequisite. |
| No missing boundary (candidate moves) | `handlers/ecs/bus` → `contracts/ecs/v1` (`a130f6b8e`); `admin/images_promote.go` → `domains/ec2/image` (`263c58167`); `admin` bootstrap account identifiers → `foundation/aws/identifiers` (`3fdaa54d9`); `admin` credential-format generators → `foundation/aws/credentials` `0368e861a`, retiring `admin` | Done. |
| One narrow missing boundary | Done: `handlers/sts` EKS JWKS lookup → `handlers/eks` `ClusterJWKSResolver` (`254d898a3`); `gateway/tagging` → `domains/tagging` with AWSGW-composed listers (`0d830e901`, `f3545e3ea`, `aedd0b2c4`). Open: `gateway/spx` (owner of operator-plane dispatch registration) | Each needs one decision or one capability, then a mechanical move. |
| Broader boundary or follows another package | `handlers/{ecs,eks,rds}` (ADR-0004 blocked-dependency lists); `handlers/elbv2` (data-plane boundary evidence); `accountteardown` (domain cleanup capabilities); `gateway` root (ADR-0002 auth and request-context phase); `gateway/{iam,sts,ecs,eks,rds,elbv2}` (each follows its `handlers/<svc>`, because a new-path `awsapi` cannot import a legacy handler); `gateway/bedrock` (the recorded Ochre split plan) | Recorded debt with removal conditions above. |
| Broader boundary or follows another package | `handlers/iam` and `handlers/iam/mock` (per-consumer IAM capabilities); `handlers/{ecs,eks,rds}` (ADR-0004 blocked-dependency lists); `handlers/elbv2` (data-plane boundary evidence); `accountteardown` (domain cleanup capabilities); `gateway` root (ADR-0002 auth and request-context phase); `gateway/{iam,sts,ecs,eks,rds,elbv2}` (each follows its `handlers/<svc>`, because a new-path `awsapi` cannot import a legacy handler); `gateway/bedrock` (the recorded Ochre split plan) | Recorded debt with removal conditions above. |
Per-consumer IAM capabilities are the widest unblocker: they gate STS's IAM reads, Ochre's IAM reads, the ELBv2 role path and the ECS, EKS and RDS ensurers.
Per-consumer IAM capabilities are the widest unblocker: they gate `handlers/iam` itself, STS's IAM reads, Ochre's IAM reads, the ELBv2 role path and the ECS, EKS and RDS ensurers.
`runtime/roles/awsgw` carries two recorded residuals from its own move (`931d86d5c`
above) that are not yet re-homed: `runQuotaReconcile`/`quotaKeyFor` belong to
+1 -1
View File
@@ -76,7 +76,7 @@ services:
- spinifex/domains/ec2/vpc
- spinifex/handlers/elbv2
- spinifex/domains/acm
- spinifex/domains/iam
- spinifex/handlers/iam
- spinifex/handlers/ecs
- spinifex/handlers/rds
- spinifex/domains/rds/subnetgroup
+1 -1
View File
@@ -10,7 +10,7 @@ COPY go.mod ./
COPY main.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/spinifex-demo .
FROM gcr.io/distroless/static:nonroot@sha256:e2e927ec666bae08560abb3c55d0659eceabb657f56b6782ab500a9fc7f555e3
FROM gcr.io/distroless/static:nonroot@sha256:18f4cc26c9ffa7f20a359298be7deadac5c607c8e03a02b8119f1a5fc2f82c99
COPY --from=build /out/spinifex-demo /spinifex-demo
EXPOSE 8080
USER nonroot:nonroot
+1 -1
View File
@@ -7,7 +7,7 @@
# Debian is the build host regardless of what is being built — mkosi bootstraps
# the target distribution from its own archive, so an Ubuntu image builds fine
# from here (see ubuntu-keyring below).
FROM debian:trixie-slim@sha256:a29215f6a35e51e22adffa17f89e9d2ef06214e64a2bad10d765c46aea49f11f
FROM debian:trixie-slim@sha256:62bd423aeee912639ca8ee681c2b762cb3e25137e0a92b20be455898a9f034ad
# The runner user is uid 1000; matching it keeps bind-mounted build output
# owned by the invoking user instead of root.
+3 -3
View File
@@ -6,8 +6,8 @@ import (
"log/slog"
"github.com/mulgadc/spinifex/spinifex/domains/admission/quota"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
@@ -22,7 +22,7 @@ func NewClusterEngine(
nc *nats.Conn,
expectedNodes int,
nodeIDs []string,
svc iamdomain.IAMService,
svc handlers_iam.IAMService,
buckets BucketStore,
) (*Engine, error) {
// Refused rather than skipped. A teardown missing its bucket reaper still
@@ -37,7 +37,7 @@ func NewClusterEngine(
return nil, fmt.Errorf("get JetStream context: %w", err)
}
names, err := iamdomain.NewAccountNameIndex(ctx, js)
names, err := handlers_iam.NewAccountNameIndex(ctx, js)
if err != nil {
return nil, fmt.Errorf("open account name index: %w", err)
}
+7 -7
View File
@@ -7,7 +7,7 @@ import (
"testing"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -18,9 +18,9 @@ import (
func TestNewClusterEngineCoversEveryStage(t *testing.T) {
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := iamdomain.NewIAMServiceImpl(t.Context(), nc, masterKey)
svc, err := handlers_iam.NewIAMServiceImpl(t.Context(), nc, masterKey)
require.NoError(t, err)
engine, err := NewClusterEngine(t.Context(), nc, 1, []string{"node-1"}, svc, newFakeBuckets())
@@ -47,9 +47,9 @@ func TestNewClusterEngineCoversEveryStage(t *testing.T) {
func TestNewClusterEngineRegistersTheEC2SettingsSealReaper(t *testing.T) {
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := iamdomain.NewIAMServiceImpl(t.Context(), nc, masterKey)
svc, err := handlers_iam.NewIAMServiceImpl(t.Context(), nc, masterKey)
require.NoError(t, err)
engine, err := NewClusterEngine(t.Context(), nc, 1, []string{"node-1"}, svc, newFakeBuckets())
@@ -67,9 +67,9 @@ func TestNewClusterEngineRegistersTheEC2SettingsSealReaper(t *testing.T) {
func TestClusterEnginePrechecks(t *testing.T) {
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := iamdomain.NewIAMServiceImpl(t.Context(), nc, masterKey)
svc, err := handlers_iam.NewIAMServiceImpl(t.Context(), nc, masterKey)
require.NoError(t, err)
engine, err := NewClusterEngine(t.Context(), nc, 1, []string{"node-1"}, svc, newFakeBuckets())
@@ -17,9 +17,9 @@ import (
ec2v1 "github.com/mulgadc/spinifex/contracts/ec2/v1"
"github.com/mulgadc/spinifex/internal/testkit"
"github.com/mulgadc/spinifex/spinifex/domains/ec2/accountinventory"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
awserrors "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
natsmsg "github.com/mulgadc/spinifex/spinifex/foundation/messaging/nats"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -165,9 +165,9 @@ func TestEC2PlatformInventoryClientDistrustsAMalformedReply(t *testing.T) {
// a Teardown request marked dry run and a full teardown all leave it uncalled.
func TestOnlyTheExplicitDryRunCallsTheEC2PlatformInventory(t *testing.T) {
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := iamdomain.NewIAMServiceImpl(t.Context(), nc, masterKey)
svc, err := handlers_iam.NewIAMServiceImpl(t.Context(), nc, masterKey)
require.NoError(t, err)
cluster, err := NewClusterEngine(t.Context(), nc, 1, []string{"node-1"}, svc, newFakeBuckets())
require.NoError(t, err)
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"time"
)
// AccountStatusTerminating mirrors iamdomain.AccountStatusTerminating.
// AccountStatusTerminating mirrors handlers_iam.AccountStatusTerminating.
// Redeclared so this package does not depend on the IAM service to be tested.
const AccountStatusTerminating = "TERMINATING"
+3 -3
View File
@@ -13,7 +13,7 @@ import (
"testing"
"time"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
const (
@@ -129,11 +129,11 @@ func TestProtectedAccountsMatchTheRestOfTheSystem(t *testing.T) {
if _, protected := protectedAccountIDs[id]; !protected {
t.Fatalf("account %s is not protected by teardown", id)
}
if _, protected := iamdomain.UndeletableAccountIDs[id]; !protected {
if _, protected := handlers_iam.UndeletableAccountIDs[id]; !protected {
t.Fatalf("account %s is not protected by the IAM service", id)
}
}
if len(protectedAccountIDs) != len(iamdomain.UndeletableAccountIDs) {
if len(protectedAccountIDs) != len(handlers_iam.UndeletableAccountIDs) {
t.Fatal("teardown and the IAM service disagree on which accounts are protected")
}
}
+6 -6
View File
@@ -5,13 +5,13 @@ import (
"errors"
"fmt"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go/jetstream"
)
// AccountReapers returns the final-stage reapers: the per-account counters and
// reservations that outlive every resource but must not outlive the account.
func AccountReapers(svc iamdomain.IAMService, names *iamdomain.AccountNameIndex, usage jetstream.KeyValue) []Reaper {
func AccountReapers(svc handlers_iam.IAMService, names *handlers_iam.AccountNameIndex, usage jetstream.KeyValue) []Reaper {
return []Reaper{
&quotaUsageReaper{usage: usage},
&nameReservationReaper{svc: svc, names: names},
@@ -53,8 +53,8 @@ func (r *quotaUsageReaper) Delete(ctx context.Context, _ string, resource Resour
// up again. A customer who deletes an account and returns must not be told the
// address is taken by a tenant that no longer exists.
type nameReservationReaper struct {
svc iamdomain.IAMService
names *iamdomain.AccountNameIndex
svc handlers_iam.IAMService
names *handlers_iam.AccountNameIndex
}
func (r *nameReservationReaper) Kind() string { return "name-reservation" }
@@ -92,12 +92,12 @@ func (r *nameReservationReaper) Delete(ctx context.Context, accountID string, re
// IAMAccounts adapts the IAM service to the engine's AccountStore. The engine
// declares its own Account type so its tests need no cluster; this is the one
// place the two meet.
type IAMAccounts struct{ svc iamdomain.IAMService }
type IAMAccounts struct{ svc handlers_iam.IAMService }
var _ AccountStore = (*IAMAccounts)(nil)
// NewIAMAccounts wraps svc as the engine's AccountStore.
func NewIAMAccounts(svc iamdomain.IAMService) *IAMAccounts {
func NewIAMAccounts(svc handlers_iam.IAMService) *IAMAccounts {
return &IAMAccounts{svc: svc}
}
@@ -10,8 +10,8 @@ import (
"testing"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -22,20 +22,20 @@ import (
type accountIAM struct {
fakeIAM
account *iamdomain.Account
account *handlers_iam.Account
getErr error
deleted bool
setStatus string
}
func (f *accountIAM) GetAccount(string) (*iamdomain.Account, error) {
func (f *accountIAM) GetAccount(string) (*handlers_iam.Account, error) {
if f.getErr != nil {
return nil, f.getErr
}
return f.account, nil
}
func (f *accountIAM) SetAccountStatus(_, status string) (*iamdomain.Account, error) {
func (f *accountIAM) SetAccountStatus(_, status string) (*handlers_iam.Account, error) {
f.setStatus = status
updated := *f.account
updated.Status = status
@@ -102,12 +102,12 @@ func TestNameReservationReaperReleasesTheName(t *testing.T) {
_, js, _ := testKV(t, "spinifex-name-reaper-warmup")
ctx := testCtx(t)
names, err := iamdomain.NewAccountNameIndex(ctx, js)
names, err := handlers_iam.NewAccountNameIndex(ctx, js)
require.NoError(t, err)
require.NoError(t, names.Reserve(ctx, "tenant@example.com", "token-1"))
require.NoError(t, names.Commit(ctx, "tenant@example.com", "000000000042", "token-1"))
svc := &accountIAM{account: &iamdomain.Account{
svc := &accountIAM{account: &handlers_iam.Account{
AccountID: "000000000042", AccountName: "tenant@example.com",
}}
reaper := &nameReservationReaper{svc: svc, names: names}
@@ -130,12 +130,12 @@ func TestNameReservationReaperIgnoresAnotherAccountsName(t *testing.T) {
_, js, _ := testKV(t, "spinifex-name-reaper-other")
ctx := testCtx(t)
names, err := iamdomain.NewAccountNameIndex(ctx, js)
names, err := handlers_iam.NewAccountNameIndex(ctx, js)
require.NoError(t, err)
require.NoError(t, names.Reserve(ctx, "tenant@example.com", "token-1"))
require.NoError(t, names.Commit(ctx, "tenant@example.com", "000000000099", "token-1"))
svc := &accountIAM{account: &iamdomain.Account{
svc := &accountIAM{account: &handlers_iam.Account{
AccountID: "000000000042", AccountName: "tenant@example.com",
}}
@@ -154,7 +154,7 @@ func TestNameReservationReaperReportsAMissingAccount(t *testing.T) {
_, js, _ := testKV(t, "spinifex-name-reaper-missing")
ctx := testCtx(t)
names, err := iamdomain.NewAccountNameIndex(ctx, js)
names, err := handlers_iam.NewAccountNameIndex(ctx, js)
require.NoError(t, err)
svc := &accountIAM{getErr: errors.New("account not found")}
@@ -177,10 +177,10 @@ func TestAccountReapersRunLast(t *testing.T) {
// The adapter is the one place the engine's Account and the IAM record meet,
// so a field dropped in translation would silently break the name confirmation.
func TestIAMAccountsAdapterCarriesEveryField(t *testing.T) {
svc := &accountIAM{account: &iamdomain.Account{
svc := &accountIAM{account: &handlers_iam.Account{
AccountID: "000000000042",
AccountName: "tenant@example.com",
Status: iamdomain.AccountStatusActive,
Status: handlers_iam.AccountStatusActive,
}}
accounts := NewIAMAccounts(svc)
@@ -188,12 +188,12 @@ func TestIAMAccountsAdapterCarriesEveryField(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "000000000042", account.AccountID)
assert.Equal(t, "tenant@example.com", account.AccountName)
assert.Equal(t, iamdomain.AccountStatusActive, account.Status)
assert.Equal(t, handlers_iam.AccountStatusActive, account.Status)
updated, err := accounts.SetAccountStatus("000000000042", AccountStatusTerminating)
require.NoError(t, err)
assert.Equal(t, AccountStatusTerminating, updated.Status)
assert.Equal(t, iamdomain.AccountStatusTerminating, svc.setStatus)
assert.Equal(t, handlers_iam.AccountStatusTerminating, svc.setStatus)
require.NoError(t, accounts.DeleteAccount("000000000042"))
assert.True(t, svc.deleted)
@@ -203,5 +203,5 @@ func TestIAMAccountsAdapterCarriesEveryField(t *testing.T) {
// two must not drift: a mismatch would mark accounts with a status the auth
// gate does not recognise, and they would keep authenticating.
func TestTerminatingStatusMatchesTheIAMRecord(t *testing.T) {
assert.Equal(t, iamdomain.AccountStatusTerminating, AccountStatusTerminating)
assert.Equal(t, handlers_iam.AccountStatusTerminating, AccountStatusTerminating)
}
+9 -9
View File
@@ -6,7 +6,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// IAMReapers returns the identity-stage reapers in teardown order.
@@ -14,7 +14,7 @@ import (
// Access keys go first and deliberately: it is the second quiesce, and unlike
// the account-status gate it holds even against a caller that reached the
// cluster some other way.
func IAMReapers(svc iamdomain.IAMService) []Reaper {
func IAMReapers(svc handlers_iam.IAMService) []Reaper {
return []Reaper{
&accessKeyReaper{svc: svc},
&instanceProfileReaper{svc: svc},
@@ -34,7 +34,7 @@ func IAMReapers(svc iamdomain.IAMService) []Reaper {
// They survive their cluster's deletion, so nothing else in teardown reaches
// them and an account would be left holding a trust anchor for a cluster that
// no longer exists.
type oidcProviderReaper struct{ svc iamdomain.IAMService }
type oidcProviderReaper struct{ svc handlers_iam.IAMService }
func (r *oidcProviderReaper) Kind() string { return "oidc-provider" }
func (r *oidcProviderReaper) Stage() Stage { return StagePlatform }
@@ -62,7 +62,7 @@ func (r *oidcProviderReaper) Delete(_ context.Context, accountID string, resourc
return ignoreAlreadyGone(err)
}
type accessKeyReaper struct{ svc iamdomain.IAMService }
type accessKeyReaper struct{ svc handlers_iam.IAMService }
func (r *accessKeyReaper) Kind() string { return "access-key" }
func (r *accessKeyReaper) Stage() Stage { return StageIdentity }
@@ -102,7 +102,7 @@ func (r *accessKeyReaper) Delete(_ context.Context, accountID string, resource R
return ignoreAlreadyGone(err)
}
type iamUserReaper struct{ svc iamdomain.IAMService }
type iamUserReaper struct{ svc handlers_iam.IAMService }
func (r *iamUserReaper) Kind() string { return "iam-user" }
func (r *iamUserReaper) Stage() Stage { return StageIdentity }
@@ -166,7 +166,7 @@ func (r *iamUserReaper) Delete(_ context.Context, accountID string, resource Res
return ignoreAlreadyGone(err)
}
type iamRoleReaper struct{ svc iamdomain.IAMService }
type iamRoleReaper struct{ svc handlers_iam.IAMService }
func (r *iamRoleReaper) Kind() string { return "iam-role" }
func (r *iamRoleReaper) Stage() Stage { return StageIdentity }
@@ -228,7 +228,7 @@ func (r *iamRoleReaper) Delete(_ context.Context, accountID string, resource Res
return ignoreAlreadyGone(err)
}
type iamGroupReaper struct{ svc iamdomain.IAMService }
type iamGroupReaper struct{ svc handlers_iam.IAMService }
func (r *iamGroupReaper) Kind() string { return "iam-group" }
func (r *iamGroupReaper) Stage() Stage { return StageIdentity }
@@ -254,7 +254,7 @@ func (r *iamGroupReaper) Delete(_ context.Context, accountID string, resource Re
return ignoreAlreadyGone(err)
}
type iamPolicyReaper struct{ svc iamdomain.IAMService }
type iamPolicyReaper struct{ svc handlers_iam.IAMService }
func (r *iamPolicyReaper) Kind() string { return "iam-policy" }
func (r *iamPolicyReaper) Stage() Stage { return StageIdentity }
@@ -302,7 +302,7 @@ func (r *iamPolicyReaper) Delete(_ context.Context, accountID string, resource R
return ignoreAlreadyGone(err)
}
type instanceProfileReaper struct{ svc iamdomain.IAMService }
type instanceProfileReaper struct{ svc handlers_iam.IAMService }
func (r *instanceProfileReaper) Kind() string { return "instance-profile" }
func (r *instanceProfileReaper) Stage() Stage { return StageIdentity }
+4 -4
View File
@@ -11,7 +11,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -21,7 +21,7 @@ import (
// stay unimplemented — calling one is a test failure by panic, which is the
// honest outcome for a reaper reaching somewhere it was not meant to.
type fakeIAM struct {
iamdomain.IAMService
handlers_iam.IAMService
calls []string
@@ -396,9 +396,9 @@ func TestPolicyReaperAsksOnlyForLocalPolicies(t *testing.T) {
// DeletePolicy refuses it until they are gone.
func TestPolicyReaperDeletesAPolicyWithSeveralVersions(t *testing.T) {
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := iamdomain.NewIAMServiceImpl(t.Context(), nc, masterKey)
svc, err := handlers_iam.NewIAMServiceImpl(t.Context(), nc, masterKey)
require.NoError(t, err)
const accountID = "000000000042"
@@ -7,8 +7,8 @@ import (
"github.com/mulgadc/spinifex/internal/testkit"
"github.com/mulgadc/spinifex/spinifex/domains/admission/quota"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -36,7 +36,7 @@ func TestCurrentBehaviour_AccountTermination_QuotaOverridesSurviveTeardown(t *te
require.NoError(t, svc.PutAccountQuota(ctx, testAccountID, quota.Overrides{VCPUs: &unlimited}, "operator"))
accounts := newFakeAccounts()
iam := &accountIAM{account: &iamdomain.Account{AccountID: testAccountID, AccountName: testAccountName}}
iam := &accountIAM{account: &handlers_iam.Account{AccountID: testAccountID, AccountName: testAccountName}}
engine := quietEngine(accounts, AccountReapers(iam, nil, usage)...)
result, err := engine.Teardown(ctx, request())
+4 -4
View File
@@ -63,7 +63,6 @@ import (
ec2volume "github.com/mulgadc/spinifex/spinifex/domains/ec2/volume"
ec2vpc "github.com/mulgadc/spinifex/spinifex/domains/ec2/vpc"
"github.com/mulgadc/spinifex/spinifex/domains/ecr"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/domains/network/external"
"github.com/mulgadc/spinifex/spinifex/domains/network/external/dhcp"
"github.com/mulgadc/spinifex/spinifex/domains/network/external/exonet"
@@ -79,6 +78,7 @@ import (
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
"github.com/mulgadc/spinifex/spinifex/providers/ebs"
"github.com/mulgadc/spinifex/spinifex/providers/objectstore"
@@ -297,7 +297,7 @@ type Daemon struct {
// iamEnsurerMu guards the lazily-built system-role IAM service (systemRoleEnsurer).
iamEnsurerMu sync.Mutex
iamEnsurerCached iamdomain.SystemInstanceRoleEnsurer
iamEnsurerCached handlers_iam.SystemInstanceRoleEnsurer
mu sync.Mutex
}
@@ -2006,8 +2006,8 @@ func (d *Daemon) startCluster() error {
if err != nil {
return fmt.Errorf("jetstream handle for account status reader: %w", err)
}
var _ ec2account.AccountTerminationChecker = (*iamdomain.AccountStatusReader)(nil)
d.accountService.SetAccountTerminationChecker(iamdomain.NewAccountStatusReader(accountStatusJS))
var _ ec2account.AccountTerminationChecker = (*handlers_iam.AccountStatusReader)(nil)
d.accountService.SetAccountTerminationChecker(handlers_iam.NewAccountStatusReader(accountStatusJS))
// ELBv2 resolves listener certificate private keys through its own ACM
// Store over the same JetStream bucket the ACM service writes, so it needs
@@ -16,11 +16,11 @@ import (
ec2eip "github.com/mulgadc/spinifex/spinifex/domains/ec2/eip"
"github.com/mulgadc/spinifex/spinifex/domains/ec2/systeminstance"
ec2vpc "github.com/mulgadc/spinifex/spinifex/domains/ec2/vpc"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/domains/network/external"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/tags"
"github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/mulgadc/spinifex/spinifex/runtime/compute/vm"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
@@ -623,7 +623,7 @@ func newRefreshTestDaemon(t *testing.T) (*Daemon, *nats.Conn, string) {
tmpDir := t.TempDir()
t.Setenv("XDG_RUNTIME_DIR", tmpDir)
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := handlers_elbv2.NewELBv2ServiceImplWithNATS(nil, nc, masterKey)
require.NoError(t, err)
+3 -3
View File
@@ -7,8 +7,8 @@ import (
"github.com/mulgadc/spinifex/internal/testkit"
"github.com/mulgadc/spinifex/spinifex/bootstrap/config"
ec2eip "github.com/mulgadc/spinifex/spinifex/domains/ec2/eip"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -20,7 +20,7 @@ func newWireLBTestDaemon(t *testing.T, cfg *config.Config) *Daemon {
t.Helper()
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := handlers_elbv2.NewELBv2ServiceImplWithNATS(nil, nc, masterKey)
require.NoError(t, err)
@@ -203,7 +203,7 @@ func TestWireLBAgentConfig_GatewayURL_SingleNodeBindMatchesAdvertise(t *testing.
// newSubscribeTestDaemon creates a minimal Daemon wired enough for subscribeAll.
func newSubscribeTestDaemon(t *testing.T, nc *nats.Conn, gatewayURL, accessKey string) *Daemon {
t.Helper()
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := handlers_elbv2.NewELBv2ServiceImplWithNATS(nil, nc, masterKey)
require.NoError(t, err)
+2 -2
View File
@@ -9,10 +9,10 @@ import (
"testing"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvstore"
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
handlers_elbv2 "github.com/mulgadc/spinifex/spinifex/handlers/elbv2"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -81,7 +81,7 @@ func TestElbv2WatchBuckets_NoStoreIsNilSafe(t *testing.T) {
func TestElbv2WatchBuckets_WithStoreReturnsWatchBucket(t *testing.T) {
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := handlers_elbv2.NewELBv2ServiceImplWithNATS(nil, nc, masterKey)
require.NoError(t, err)
+3 -3
View File
@@ -6,8 +6,8 @@ import (
"path/filepath"
"github.com/mulgadc/bluebottle/pkg/masterkey"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_ecs "github.com/mulgadc/spinifex/spinifex/handlers/ecs"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// buildECSServiceDeps assembles the ECS service Deps for ProvisionCapacity: the
@@ -47,8 +47,8 @@ func (d *Daemon) buildECSServiceDeps() handlers_ecs.Deps {
// to have responders. A daemon that started first got one attempt, failed
// it, and had capacity provisioning off for the life of the process with no
// symptom but a 500 from ProvisionCapacity.
iamSvc, iamErr := initServiceWithRetry("ECS IAM service", func() (*iamdomain.IAMServiceImpl, error) {
return iamdomain.NewIAMServiceImpl(d.ctx, d.natsConn, masterKey)
iamSvc, iamErr := initServiceWithRetry("ECS IAM service", func() (*handlers_iam.IAMServiceImpl, error) {
return handlers_iam.NewIAMServiceImpl(d.ctx, d.natsConn, masterKey)
})
if iamErr != nil {
// Off rather than fatal: ECS CRUD, EKS and EC2 do not need this, and a
+3 -3
View File
@@ -10,8 +10,8 @@ import (
"github.com/mulgadc/bluebottle/pkg/masterkey"
"github.com/mulgadc/spinifex/spinifex/bootstrap/config"
ec2placementgroup "github.com/mulgadc/spinifex/spinifex/domains/ec2/placementgroup"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/mulgadc/spinifex/spinifex/providers/objectstore"
)
@@ -24,7 +24,7 @@ import (
// first-use (an LB/cluster create) the backend is up. Returns nil (caller falls
// back to static creds) until a build succeeds; retried on each call until then,
// cached once it works.
func (d *Daemon) systemRoleEnsurer() iamdomain.SystemInstanceRoleEnsurer {
func (d *Daemon) systemRoleEnsurer() handlers_iam.SystemInstanceRoleEnsurer {
d.iamEnsurerMu.Lock()
defer d.iamEnsurerMu.Unlock()
if d.iamEnsurerCached != nil {
@@ -36,7 +36,7 @@ func (d *Daemon) systemRoleEnsurer() iamdomain.SystemInstanceRoleEnsurer {
"err", err)
return nil
}
iamSvc, iamErr := iamdomain.NewIAMServiceImpl(d.ctx, d.natsConn, masterKey)
iamSvc, iamErr := handlers_iam.NewIAMServiceImpl(d.ctx, d.natsConn, masterKey)
if iamErr != nil {
slog.Warn("System role ensurer: IAM service init failed (retried on next launch); system VMs fall back to baked static creds",
"err", iamErr)
+4 -4
View File
@@ -21,9 +21,9 @@ import (
"github.com/aws/aws-sdk-go/private/protocol/json/jsonutil"
"github.com/aws/aws-sdk-go/service/acm"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -33,7 +33,7 @@ const testAccountID = "000000000001"
func setupACMService(t *testing.T) *ACMServiceImpl {
t.Helper()
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := NewACMServiceImplWithNATS(t.Context(), nil, nc, masterKey)
require.NoError(t, err)
@@ -307,7 +307,7 @@ func TestImportCertificate_PrivateKeyEncryptedAtRest(t *testing.T) {
// TestDescribeCertificate_DoesNotLeakPrivateKey asserts the public
// CertificateDetail response can never carry key material, however the
// record is stored. Precedent: TestSensitiveDataNotLogged_MasterKey in
// domains/iam/service_impl_test.go.
// handlers/iam/service_impl_test.go.
func TestDescribeCertificate_DoesNotLeakPrivateKey(t *testing.T) {
t.Parallel()
svc := setupACMService(t)
@@ -331,7 +331,7 @@ func TestDescribeCertificate_DoesNotLeakPrivateKey(t *testing.T) {
// TestSensitiveDataNotLogged_ACMPrivateKey asserts ImportCertificate and
// DescribeCertificate never place the raw private key PEM in log output.
// Precedent: TestSensitiveDataNotLogged_MasterKey in
// domains/iam/service_impl_test.go.
// handlers/iam/service_impl_test.go.
// fakeCertAuthority is a minimal CertAuthority for testing RequestCertificate's
// PRIVATE_CA path without the real tenant CA implementation (domains/acm/privateca.go
// lives on a separate branch).
+4 -4
View File
@@ -11,10 +11,10 @@ import (
"strings"
"time"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvstore"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
@@ -161,7 +161,7 @@ type CertRecord struct {
type Store struct {
store *kvstore.Store[CertRecord]
// masterKey encrypts/decrypts CertRecord.PrivateKey at rest (AES-256-GCM via
// iamdomain.EncryptSecret/DecryptSecret). Every Store over the ACM bucket
// handlers_iam.EncryptSecret/DecryptSecret). Every Store over the ACM bucket
// — the ACM service and ELBv2's independent read-only Store alike — must be
// constructed with the same deployment key: a keyed writer and an unkeyed (or
// differently keyed) reader of the same bucket disagree silently, with the
@@ -221,7 +221,7 @@ func certKey(certArn string) string {
// via the legacy-passthrough path in decryptPrivateKey is therefore
// re-encrypted the next time it is put.
func (s *Store) PutCert(ctx context.Context, rec *CertRecord) error {
ciphertext, err := iamdomain.EncryptSecret(rec.PrivateKey, s.masterKey)
ciphertext, err := handlers_iam.EncryptSecret(rec.PrivateKey, s.masterKey)
if err != nil {
return fmt.Errorf("encrypt private key: %w", err)
}
@@ -324,7 +324,7 @@ func isPlaintextPrivateKeyPEM(raw string) bool {
// ciphertext, garbage) is a hard error rather than a silent plaintext
// fallback.
func (s *Store) decryptPrivateKey(rec *CertRecord) error {
if plaintext, err := iamdomain.DecryptSecret(rec.PrivateKey, s.masterKey); err == nil {
if plaintext, err := handlers_iam.DecryptSecret(rec.PrivateKey, s.masterKey); err == nil {
rec.PrivateKey = plaintext
return nil
}
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"time"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvstore"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -21,7 +21,7 @@ import (
func setupACMStore(t *testing.T) *Store {
t.Helper()
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
store, err := NewStore(t.Context(), nc, masterKey)
require.NoError(t, err)
@@ -14,8 +14,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
)
@@ -477,7 +477,7 @@ func judgeIdentityCompleteness(ctx context.Context, frames []natsmsg.Frame, sum
// that carries only an ARN. Results are cached per ARN to avoid repeated RPCs.
// Misses are warn-logged and leave Id empty (graceful degradation for deleted profiles).
// Safe to call with a nil output or nil IAMService (no-op).
func EnrichInstanceProfileIDs(out *ec2.DescribeInstancesOutput, iamSvc iamdomain.IAMService, accountID string) {
func EnrichInstanceProfileIDs(out *ec2.DescribeInstancesOutput, iamSvc handlers_iam.IAMService, accountID string) {
if out == nil || iamSvc == nil {
return
}
@@ -11,8 +11,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -751,9 +751,9 @@ func TestEnrichInstanceProfileIDs_CachesPerARN(t *testing.T) {
arn := "arn:aws:iam::123456789012:instance-profile/shared"
var calls int
svc := &fakeIAMService{
resolveFn: func(_, nameOrARN string) (*iamdomain.InstanceProfile, error) {
resolveFn: func(_, nameOrARN string) (*handlers_iam.InstanceProfile, error) {
calls++
return &iamdomain.InstanceProfile{ARN: nameOrARN, InstanceProfileID: "AIPAEXAMPLE"}, nil
return &handlers_iam.InstanceProfile{ARN: nameOrARN, InstanceProfileID: "AIPAEXAMPLE"}, nil
},
}
@@ -771,11 +771,11 @@ func TestEnrichInstanceProfileIDs_ResolveErrorLeavesIdEmpty(t *testing.T) {
failingARN := "arn:aws:iam::123456789012:instance-profile/missing"
okARN := "arn:aws:iam::123456789012:instance-profile/present"
svc := &fakeIAMService{
resolveFn: func(_, nameOrARN string) (*iamdomain.InstanceProfile, error) {
resolveFn: func(_, nameOrARN string) (*handlers_iam.InstanceProfile, error) {
if nameOrARN == failingARN {
return nil, errors.New(awserrors.ErrorIAMNoSuchEntity)
}
return &iamdomain.InstanceProfile{ARN: nameOrARN, InstanceProfileID: "AIPAOK"}, nil
return &handlers_iam.InstanceProfile{ARN: nameOrARN, InstanceProfileID: "AIPAOK"}, nil
},
}
@@ -12,9 +12,9 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/spinifex/contracts/ec2/v1"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
)
@@ -24,7 +24,7 @@ const fanOutTimeout = 3 * time.Second
// resolveAndAuthorizeProfile resolves a profile (name or ARN) and enforces iam:PassRole.
// Profiles without a role skip PassRole; missing profile → InvalidIamInstanceProfile.NotFound.
func resolveAndAuthorizeProfile(spec *ec2.IamInstanceProfileSpecification, iamSvc iamdomain.IAMService, accountID string, passRoleCheck PassRoleChecker) (*iamdomain.InstanceProfile, error) {
func resolveAndAuthorizeProfile(spec *ec2.IamInstanceProfileSpecification, iamSvc handlers_iam.IAMService, accountID string, passRoleCheck PassRoleChecker) (*handlers_iam.InstanceProfile, error) {
nameOrARN := profileNameOrARN(spec)
if nameOrARN == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
@@ -55,7 +55,7 @@ func resolveAndAuthorizeProfile(spec *ec2.IamInstanceProfileSpecification, iamSv
// AssociateIamInstanceProfile attaches a profile to a running instance.
// The gateway resolves the profile and enforces PassRole; the daemon validates
// no existing profile and generates a fresh iip-assoc-… ID.
func AssociateIamInstanceProfile(ctx context.Context, input *ec2.AssociateIamInstanceProfileInput, natsConn *nats.Conn, iamSvc iamdomain.IAMService, accountID string, passRoleCheck PassRoleChecker) (*ec2.AssociateIamInstanceProfileOutput, error) {
func AssociateIamInstanceProfile(ctx context.Context, input *ec2.AssociateIamInstanceProfileInput, natsConn *nats.Conn, iamSvc handlers_iam.IAMService, accountID string, passRoleCheck PassRoleChecker) (*ec2.AssociateIamInstanceProfileOutput, error) {
if input == nil || input.InstanceId == nil || *input.InstanceId == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -107,7 +107,7 @@ func DisassociateIamInstanceProfile(ctx context.Context, input *ec2.Disassociate
// ReplaceIamInstanceProfileAssociation swaps the profile for an existing association.
// The gateway resolves the new profile and enforces PassRole; the daemon swaps atomically.
func ReplaceIamInstanceProfileAssociation(ctx context.Context, input *ec2.ReplaceIamInstanceProfileAssociationInput, natsConn *nats.Conn, iamSvc iamdomain.IAMService, expectedNodes int, accountID string, passRoleCheck PassRoleChecker) (*ec2.ReplaceIamInstanceProfileAssociationOutput, error) {
func ReplaceIamInstanceProfileAssociation(ctx context.Context, input *ec2.ReplaceIamInstanceProfileAssociationInput, natsConn *nats.Conn, iamSvc handlers_iam.IAMService, expectedNodes int, accountID string, passRoleCheck PassRoleChecker) (*ec2.ReplaceIamInstanceProfileAssociationOutput, error) {
if input == nil || input.AssociationId == nil || *input.AssociationId == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -173,7 +173,7 @@ func DescribeIamInstanceProfileAssociations(ctx context.Context, input *ec2.Desc
// enrichProfileID fills IamInstanceProfile.Id from the resolved profile.
// Daemons have no IAM access. Safe to call with a nil association.
func enrichProfileID(assoc *ec2.IamInstanceProfileAssociation, profile *iamdomain.InstanceProfile) {
func enrichProfileID(assoc *ec2.IamInstanceProfileAssociation, profile *handlers_iam.InstanceProfile) {
if assoc == nil || profile == nil {
return
}
@@ -11,10 +11,10 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/spinifex/contracts/ec2/v1"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -35,13 +35,13 @@ const (
// fakeIAMService implements profile and canonical role lookups.
// Other methods nil-panic to surface unexpected calls.
type fakeIAMService struct {
iamdomain.IAMService
handlers_iam.IAMService
resolveFn func(accountID, nameOrARN string) (*iamdomain.InstanceProfile, error)
resolveFn func(accountID, nameOrARN string) (*handlers_iam.InstanceProfile, error)
canonicalFn func(accountID string, kind arn.IAMResourceType, name string) (string, error)
}
func (f *fakeIAMService) ResolveInstanceProfile(accountID, nameOrARN string) (*iamdomain.InstanceProfile, error) {
func (f *fakeIAMService) ResolveInstanceProfile(accountID, nameOrARN string) (*handlers_iam.InstanceProfile, error) {
if f.resolveFn != nil {
return f.resolveFn(accountID, nameOrARN)
}
@@ -58,11 +58,11 @@ func (f *fakeIAMService) CanonicalResourceARN(accountID string, kind arn.IAMReso
return "", errors.New(awserrors.ErrorIAMNoSuchEntity)
}
var _ iamdomain.IAMService = (*fakeIAMService)(nil)
var _ handlers_iam.IAMService = (*fakeIAMService)(nil)
// profileWithRole builds a resolved profile fixture pointing at the test role.
func profileWithRole() *iamdomain.InstanceProfile {
return &iamdomain.InstanceProfile{
func profileWithRole() *handlers_iam.InstanceProfile {
return &handlers_iam.InstanceProfile{
InstanceProfileName: testProfileNameApp,
InstanceProfileID: testProfileIDApp,
AccountID: testGwAccountID,
@@ -73,8 +73,8 @@ func profileWithRole() *iamdomain.InstanceProfile {
// profileNoRole builds a resolved profile fixture with no role attached.
// resolveAndAuthorizeProfile must NOT invoke PassRole for this profile.
func profileNoRole() *iamdomain.InstanceProfile {
return &iamdomain.InstanceProfile{
func profileNoRole() *handlers_iam.InstanceProfile {
return &handlers_iam.InstanceProfile{
InstanceProfileName: testProfileNameOther,
InstanceProfileID: testProfileIDOther,
AccountID: testGwAccountID,
@@ -88,7 +88,7 @@ func profileNoRole() *iamdomain.InstanceProfile {
func TestResolveAndAuthorizeProfile_NameForm(t *testing.T) {
t.Parallel()
gotNameOrARN := ""
svc := &fakeIAMService{resolveFn: func(_, nameOrARN string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(_, nameOrARN string) (*handlers_iam.InstanceProfile, error) {
gotNameOrARN = nameOrARN
return profileWithRole(), nil
}}
@@ -112,7 +112,7 @@ func TestResolveAndAuthorizeProfile_NameForm(t *testing.T) {
func TestResolveAndAuthorizeProfile_ArnForm(t *testing.T) {
t.Parallel()
gotNameOrARN := ""
svc := &fakeIAMService{resolveFn: func(_, nameOrARN string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(_, nameOrARN string) (*handlers_iam.InstanceProfile, error) {
gotNameOrARN = nameOrARN
return profileWithRole(), nil
}}
@@ -144,7 +144,7 @@ func TestResolveAndAuthorizeProfile_NilIAMService(t *testing.T) {
func TestResolveAndAuthorizeProfile_NotFoundMapsToEC2Error(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return nil, errors.New(awserrors.ErrorIAMNoSuchEntity)
}}
_, err := resolveAndAuthorizeProfile(
@@ -157,7 +157,7 @@ func TestResolveAndAuthorizeProfile_NotFoundMapsToEC2Error(t *testing.T) {
func TestResolveAndAuthorizeProfile_PassthroughOtherIAMError(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return nil, errors.New(awserrors.ErrorAccessDenied)
}}
_, err := resolveAndAuthorizeProfile(
@@ -170,7 +170,7 @@ func TestResolveAndAuthorizeProfile_PassthroughOtherIAMError(t *testing.T) {
func TestResolveAndAuthorizeProfile_PassRoleDenied(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileWithRole(), nil
}}
check := func(string) error { return errors.New(awserrors.ErrorAccessDenied) }
@@ -183,7 +183,7 @@ func TestResolveAndAuthorizeProfile_PassRoleDenied(t *testing.T) {
func TestResolveAndAuthorizeProfile_NoRoleSkipsPassRole(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileNoRole(), nil
}}
checkCalled := false
@@ -265,7 +265,7 @@ func TestAssociateIamInstanceProfile_ProfileNotFound(t *testing.T) {
func TestAssociateIamInstanceProfile_PassRoleDenied(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileWithRole(), nil
}}
in := &ec2.AssociateIamInstanceProfileInput{
@@ -281,7 +281,7 @@ func TestAssociateIamInstanceProfile_PassRoleDenied(t *testing.T) {
func TestAssociateIamInstanceProfile_NoResponders(t *testing.T) {
t.Parallel()
_, nc := startTestNATSServer(t)
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileNoRole(), nil
}}
in := &ec2.AssociateIamInstanceProfileInput{
@@ -299,7 +299,7 @@ func TestAssociateIamInstanceProfile_NoResponders(t *testing.T) {
func TestAssociateIamInstanceProfile_Success(t *testing.T) {
t.Parallel()
_, nc := startTestNATSServer(t)
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileWithRole(), nil
}}
const instanceID = "i-assoc-success"
@@ -345,7 +345,7 @@ func TestAssociateIamInstanceProfile_Success(t *testing.T) {
func TestAssociateIamInstanceProfile_DaemonAlreadyAssociated(t *testing.T) {
t.Parallel()
_, nc := startTestNATSServer(t)
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileNoRole(), nil
}}
const instanceID = "i-already-bound"
@@ -474,7 +474,7 @@ func TestReplaceIamInstanceProfileAssociation_MissingProfileSpec(t *testing.T) {
func TestReplaceIamInstanceProfileAssociation_PassRoleDeniedBeforeBroadcast(t *testing.T) {
t.Parallel()
// nc=nil proves the call is rejected before any NATS dispatch.
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileWithRole(), nil
}}
check := func(string) error { return errors.New(awserrors.ErrorAccessDenied) }
@@ -489,7 +489,7 @@ func TestReplaceIamInstanceProfileAssociation_PassRoleDeniedBeforeBroadcast(t *t
func TestReplaceIamInstanceProfileAssociation_Success(t *testing.T) {
t.Parallel()
_, nc := startTestNATSServer(t)
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileWithRole(), nil
}}
const oldID = "iip-assoc-old00000000001"
@@ -530,7 +530,7 @@ func TestReplaceIamInstanceProfileAssociation_Success(t *testing.T) {
func TestReplaceIamInstanceProfileAssociation_NoSuchAssociation(t *testing.T) {
t.Parallel()
_, nc := startTestNATSServer(t)
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileNoRole(), nil
}}
_, err := nc.Subscribe("ec2.IamProfileAssociation.replace", func(msg *nats.Msg) {
@@ -700,7 +700,7 @@ func TestResolveAndAuthorizeProfile_CanonicalRoleARN(t *testing.T) {
t.Parallel()
storedARN := "arn:aws:iam::111122223333:role" + path + "app-role"
svc := &fakeIAMService{
resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileWithRole(), nil
},
canonicalFn: func(accountID string, kind arn.IAMResourceType, name string) (string, error) {
@@ -729,7 +729,7 @@ func TestResolveAndAuthorizeProfile_CanonicalLookupError(t *testing.T) {
t.Run(lookupErr.Error(), func(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{
resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileWithRole(), nil
},
canonicalFn: func(string, arn.IAMResourceType, string) (string, error) {
@@ -20,8 +20,8 @@ import (
ec2key "github.com/mulgadc/spinifex/spinifex/domains/ec2/key"
ec2launchtemplate "github.com/mulgadc/spinifex/spinifex/domains/ec2/launchtemplate"
ec2placementgroup "github.com/mulgadc/spinifex/spinifex/domains/ec2/placementgroup"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
)
@@ -131,7 +131,7 @@ func hasPinnedPrivateIP(input *ec2.RunInstancesInput) bool {
// passRoleCheck may be nil to skip PassRole enforcement.
// launchQuotaCheck may be nil to skip quota enforcement; when present it runs
// after validation and PassRole authorization, but before any launch dispatch.
func RunInstances(ctx context.Context, input *ec2.RunInstancesInput, natsConn *nats.Conn, iamSvc iamdomain.IAMService, accountID string, passRoleCheck PassRoleChecker, launchQuotaCheck LaunchQuotaChecker, expectedNodes int) (reservation ec2.Reservation, err error) {
func RunInstances(ctx context.Context, input *ec2.RunInstancesInput, natsConn *nats.Conn, iamSvc handlers_iam.IAMService, accountID string, passRoleCheck PassRoleChecker, launchQuotaCheck LaunchQuotaChecker, expectedNodes int) (reservation ec2.Reservation, err error) {
// Expand a referenced launch template before validation so validation, quota,
// and downstream routing all see the effective (post-merge) parameters.
if err = ExpandLaunchTemplate(ctx, natsConn, input, accountID); err != nil {
@@ -163,7 +163,7 @@ func RunInstances(ctx context.Context, input *ec2.RunInstancesInput, natsConn *n
// runInstancesInner performs the actual launch: profile resolution, placement
// routing, and capacity-aware distribution. Wrapped by RunInstances for idempotency.
func runInstancesInner(ctx context.Context, input *ec2.RunInstancesInput, natsConn *nats.Conn, iamSvc iamdomain.IAMService, accountID string, passRoleCheck PassRoleChecker, launchQuotaCheck LaunchQuotaChecker, expectedNodes int) (reservation ec2.Reservation, err error) {
func runInstancesInner(ctx context.Context, input *ec2.RunInstancesInput, natsConn *nats.Conn, iamSvc handlers_iam.IAMService, accountID string, passRoleCheck PassRoleChecker, launchQuotaCheck LaunchQuotaChecker, expectedNodes int) (reservation ec2.Reservation, err error) {
resolvedProfile, err := resolveAndAuthorizeInstanceProfile(input, iamSvc, accountID, passRoleCheck)
if err != nil {
return reservation, err
@@ -236,7 +236,7 @@ func runInstancesInner(ctx context.Context, input *ec2.RunInstancesInput, natsCo
// resolveAndAuthorizeInstanceProfile resolves an optional instance profile,
// enforces PassRole, and normalises input to the canonical ARN only.
func resolveAndAuthorizeInstanceProfile(input *ec2.RunInstancesInput, iamSvc iamdomain.IAMService, accountID string, passRoleCheck PassRoleChecker) (*iamdomain.InstanceProfile, error) {
func resolveAndAuthorizeInstanceProfile(input *ec2.RunInstancesInput, iamSvc handlers_iam.IAMService, accountID string, passRoleCheck PassRoleChecker) (*handlers_iam.InstanceProfile, error) {
if input.IamInstanceProfile == nil {
return nil, nil
}
@@ -261,7 +261,7 @@ func profileNameOrARN(spec *ec2.IamInstanceProfileSpecification) string {
// enrichReservationWithProfileID fills IamInstanceProfile.Id on every instance.
// Daemons emit Arn only (no IAM access); the gateway adds Id from the resolved profile.
func enrichReservationWithProfileID(reservation *ec2.Reservation, profile *iamdomain.InstanceProfile) {
func enrichReservationWithProfileID(reservation *ec2.Reservation, profile *handlers_iam.InstanceProfile) {
if reservation == nil || profile == nil {
return
}
@@ -9,8 +9,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/mulgadc/spinifex/spinifex/domains/ec2/ebs/metadata"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -385,7 +385,7 @@ func TestResolveAndAuthorizeInstanceProfile_Absent(t *testing.T) {
func TestResolveAndAuthorizeInstanceProfile_NameForm_NormalisesToARN(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{resolveFn: func(_, nameOrARN string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(_, nameOrARN string) (*handlers_iam.InstanceProfile, error) {
assert.Equal(t, testProfileNameApp, nameOrARN)
return profileWithRole(), nil
}}
@@ -404,7 +404,7 @@ func TestResolveAndAuthorizeInstanceProfile_NameForm_NormalisesToARN(t *testing.
func TestResolveAndAuthorizeInstanceProfile_ArnForm(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{resolveFn: func(_, nameOrARN string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(_, nameOrARN string) (*handlers_iam.InstanceProfile, error) {
assert.Equal(t, testProfileARNApp, nameOrARN)
return profileWithRole(), nil
}}
@@ -433,7 +433,7 @@ func TestResolveAndAuthorizeInstanceProfile_NilIAMService(t *testing.T) {
func TestResolveAndAuthorizeInstanceProfile_NotFoundMapsToEC2Error(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return nil, errors.New(awserrors.ErrorIAMNoSuchEntity)
}}
in := runValidInputWithProfile(&ec2.IamInstanceProfileSpecification{Name: aws.String("ghost")})
@@ -445,7 +445,7 @@ func TestResolveAndAuthorizeInstanceProfile_NotFoundMapsToEC2Error(t *testing.T)
func TestResolveAndAuthorizeInstanceProfile_CrossAccountARNPassthrough(t *testing.T) {
t.Parallel()
// IAM service rejects cross-account; gateway surfaces the AccessDenied verbatim.
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return nil, errors.New(awserrors.ErrorAccessDenied)
}}
in := runValidInputWithProfile(&ec2.IamInstanceProfileSpecification{Arn: aws.String(testCrossAccountARN)})
@@ -456,7 +456,7 @@ func TestResolveAndAuthorizeInstanceProfile_CrossAccountARNPassthrough(t *testin
func TestResolveAndAuthorizeInstanceProfile_PassRoleDenied(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileWithRole(), nil
}}
check := func(roleARN string) error {
@@ -471,7 +471,7 @@ func TestResolveAndAuthorizeInstanceProfile_PassRoleDenied(t *testing.T) {
func TestResolveAndAuthorizeInstanceProfile_NoRoleSkipsPassRole(t *testing.T) {
t.Parallel()
svc := &fakeIAMService{resolveFn: func(string, string) (*iamdomain.InstanceProfile, error) {
svc := &fakeIAMService{resolveFn: func(string, string) (*handlers_iam.InstanceProfile, error) {
return profileNoRole(), nil
}}
checkCalled := false
@@ -17,10 +17,10 @@ import (
admissionquota "github.com/mulgadc/spinifex/spinifex/domains/admission/quota"
ec2instanceapi "github.com/mulgadc/spinifex/spinifex/domains/ec2/awsapi/instance"
ec2spotinstance "github.com/mulgadc/spinifex/spinifex/domains/ec2/spotinstance"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers"
awstags "github.com/mulgadc/spinifex/spinifex/foundation/aws/tags"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
)
@@ -64,7 +64,7 @@ func ValidateRequestSpotInstancesInput(input *ec2.RequestSpotInstancesInput) err
// ClientToken passed through, same vCPU quota gate), then builds and persists one
// active/fulfilled SpotInstanceRequest per launched instance. On a launch failure
// (including InsufficientInstanceCapacity) it returns the error and persists nothing.
func RequestSpotInstances(ctx context.Context, input *ec2.RequestSpotInstancesInput, natsConn *nats.Conn, iamSvc iamdomain.IAMService, accountID, az string, passRoleCheck ec2instanceapi.PassRoleChecker, quota *admissionquota.Service, expectedNodes int) (ec2.RequestSpotInstancesOutput, error) {
func RequestSpotInstances(ctx context.Context, input *ec2.RequestSpotInstancesInput, natsConn *nats.Conn, iamSvc handlers_iam.IAMService, accountID, az string, passRoleCheck ec2instanceapi.PassRoleChecker, quota *admissionquota.Service, expectedNodes int) (ec2.RequestSpotInstancesOutput, error) {
var output ec2.RequestSpotInstancesOutput
if err := ValidateRequestSpotInstancesInput(input); err != nil {
@@ -13,8 +13,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -62,13 +62,13 @@ func (f *fakeResolver) resolveVPCCIDR(_ context.Context, _, _ string) (string, e
}
type fakeIAM struct {
profile *iamdomain.InstanceProfile
profile *handlers_iam.InstanceProfile
profileErr error
roleARN string
roleErr error
}
func (f *fakeIAM) ResolveInstanceProfile(_ context.Context, _, _ string) (*iamdomain.InstanceProfile, error) {
func (f *fakeIAM) ResolveInstanceProfile(_ context.Context, _, _ string) (*handlers_iam.InstanceProfile, error) {
return f.profile, f.profileErr
}
@@ -991,8 +991,8 @@ func TestHTTP_PublicKeysInstanceBackendError500(t *testing.T) {
// ----- IAM / credentials -------------------------------------------------
func profileFixture() *iamdomain.InstanceProfile {
return &iamdomain.InstanceProfile{
func profileFixture() *handlers_iam.InstanceProfile {
return &handlers_iam.InstanceProfile{
InstanceProfileName: "app-profile",
InstanceProfileID: "AIPAEXAMPLE",
AccountID: "111122223333",
@@ -10,7 +10,7 @@ import (
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"
ec2key "github.com/mulgadc/spinifex/spinifex/domains/ec2/key"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
"github.com/nats-io/nats.go"
)
@@ -50,7 +50,7 @@ func (a *NATSSTSAssumer) AssumeRoleForInstance(ctx context.Context, accountID, r
// NATSProfileLookup is the NATS-backed profileLookup with a short-TTL cache.
type NATSProfileLookup struct {
nc *nats.Conn
profiles *ttlCache[*iamdomain.InstanceProfile]
profiles *ttlCache[*handlers_iam.InstanceProfile]
roles *ttlCache[*iam.GetRoleOutput]
}
@@ -60,17 +60,17 @@ var _ profileLookup = (*NATSProfileLookup)(nil)
func NewNATSProfileLookup(nc *nats.Conn) *NATSProfileLookup {
return &NATSProfileLookup{
nc: nc,
profiles: newTTLCache[*iamdomain.InstanceProfile](profileCacheTTL),
profiles: newTTLCache[*handlers_iam.InstanceProfile](profileCacheTTL),
roles: newTTLCache[*iam.GetRoleOutput](profileCacheTTL),
}
}
func (p *NATSProfileLookup) ResolveInstanceProfile(ctx context.Context, accountID, nameOrARN string) (*iamdomain.InstanceProfile, error) {
func (p *NATSProfileLookup) ResolveInstanceProfile(ctx context.Context, accountID, nameOrARN string) (*handlers_iam.InstanceProfile, error) {
key := accountID + "\x00" + nameOrARN
if v, ok := p.profiles.get(key); ok {
return v, nil
}
out, err := natsmsg.NATSRequest[iamdomain.InstanceProfile](ctx, p.nc, iamdomain.SubjectResolveInstanceProfile, iamdomain.ResolveInstanceProfileRequest{
out, err := natsmsg.NATSRequest[handlers_iam.InstanceProfile](ctx, p.nc, handlers_iam.SubjectResolveInstanceProfile, handlers_iam.ResolveInstanceProfileRequest{
AccountID: accountID,
NameOrARN: nameOrARN,
}, imdsRPCTimeout, accountID)
@@ -86,7 +86,7 @@ func (p *NATSProfileLookup) GetRole(ctx context.Context, accountID string, input
if v, ok := p.roles.get(key); ok {
return v, nil
}
out, err := natsmsg.NATSRequest[iam.GetRoleOutput](ctx, p.nc, iamdomain.SubjectGetRole, iamdomain.GetRoleRequest{
out, err := natsmsg.NATSRequest[iam.GetRoleOutput](ctx, p.nc, handlers_iam.SubjectGetRole, handlers_iam.GetRoleRequest{
AccountID: accountID,
Input: input,
}, imdsRPCTimeout, accountID)
@@ -13,8 +13,8 @@ import (
"github.com/aws/aws-sdk-go/service/sts"
"github.com/mulgadc/spinifex/internal/testkit"
ec2key "github.com/mulgadc/spinifex/spinifex/domains/ec2/key"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
@@ -72,9 +72,9 @@ func TestNATSSTSAssumer_RoundTrip(t *testing.T) {
func TestNATSProfileLookup_CachesProfile(t *testing.T) {
_, nc := testutil.StartTestNATS(t)
calls := serveCounted(t, nc, iamdomain.SubjectResolveInstanceProfile,
func(req *iamdomain.ResolveInstanceProfileRequest) (*iamdomain.InstanceProfile, error) {
return &iamdomain.InstanceProfile{
calls := serveCounted(t, nc, handlers_iam.SubjectResolveInstanceProfile,
func(req *handlers_iam.ResolveInstanceProfileRequest) (*handlers_iam.InstanceProfile, error) {
return &handlers_iam.InstanceProfile{
InstanceProfileName: "app",
AccountID: req.AccountID,
RoleName: "app-role",
@@ -94,8 +94,8 @@ func TestNATSProfileLookup_CachesProfile(t *testing.T) {
func TestNATSProfileLookup_CachesRole(t *testing.T) {
_, nc := testutil.StartTestNATS(t)
calls := serveCounted(t, nc, iamdomain.SubjectGetRole,
func(req *iamdomain.GetRoleRequest) (*iam.GetRoleOutput, error) {
calls := serveCounted(t, nc, handlers_iam.SubjectGetRole,
func(req *handlers_iam.GetRoleRequest) (*iam.GetRoleOutput, error) {
return &iam.GetRoleOutput{Role: &iam.Role{
RoleName: req.Input.RoleName,
Arn: aws.String("arn:aws:iam::123456789012:role/app-role"),
@@ -114,8 +114,8 @@ func TestNATSProfileLookup_CachesRole(t *testing.T) {
func TestNATSProfileLookup_PropagatesError(t *testing.T) {
_, nc := testutil.StartTestNATS(t)
serveCounted(t, nc, iamdomain.SubjectResolveInstanceProfile,
func(_ *iamdomain.ResolveInstanceProfileRequest) (*iamdomain.InstanceProfile, error) {
serveCounted(t, nc, handlers_iam.SubjectResolveInstanceProfile,
func(_ *handlers_iam.ResolveInstanceProfileRequest) (*handlers_iam.InstanceProfile, error) {
return nil, assert.AnError
})
@@ -11,8 +11,8 @@ import (
"testing"
"time"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -47,7 +47,7 @@ func TestProfileForReportsWhyTheRoleIsMissing(t *testing.T) {
{
name: "role resolved",
res: &fakeResolver{eni: testENI(), inst: &instanceFacts{iamInstanceProfileArn: "arn:aws:iam::111122223333:instance-profile/app"}},
iam: &fakeIAM{profile: &iamdomain.InstanceProfile{ARN: "arn", InstanceProfileID: "AIPA", RoleName: "app"}},
iam: &fakeIAM{profile: &handlers_iam.InstanceProfile{ARN: "arn", InstanceProfileID: "AIPA", RoleName: "app"}},
want: roleMissNone,
},
}
@@ -13,9 +13,9 @@ import (
"time"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvstore"
"github.com/mulgadc/spinifex/spinifex/foundation/telemetry"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
@@ -35,7 +35,7 @@ type listTapsFunc func(ctx context.Context) (map[string]string, error)
// profileLookup is the IAMService slice needed for credential + iam/* paths.
type profileLookup interface {
ResolveInstanceProfile(ctx context.Context, accountID, nameOrARN string) (*iamdomain.InstanceProfile, error)
ResolveInstanceProfile(ctx context.Context, accountID, nameOrARN string) (*handlers_iam.InstanceProfile, error)
GetRole(ctx context.Context, accountID string, input *iam.GetRoleInput) (*iam.GetRoleOutput, error)
}
+3 -3
View File
@@ -20,8 +20,8 @@ import (
"strings"
"time"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go/jetstream"
)
@@ -176,7 +176,7 @@ func deleteSigningKey(ctx context.Context, kv jetstream.KeyValue, kid string) er
// decodeSigningKey decrypts and parses one stored signing key's encrypted PKCS#8
// PEM bytes into a SigningKey under kid.
func decodeSigningKey(kid string, ciphertext []byte, masterKey []byte) (*SigningKey, error) {
pemStr, err := iamdomain.DecryptSecret(string(ciphertext), masterKey)
pemStr, err := handlers_iam.DecryptSecret(string(ciphertext), masterKey)
if err != nil {
return nil, fmt.Errorf("decrypt signing key %s: %w", kid, err)
}
@@ -203,7 +203,7 @@ func generateSigningKey(ctx context.Context, kv jetstream.KeyValue, masterKey []
return nil, fmt.Errorf("marshal pkcs8 private key: %w", err)
}
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})
ciphertext, err := iamdomain.EncryptSecret(string(pemBytes), masterKey)
ciphertext, err := handlers_iam.EncryptSecret(string(pemBytes), masterKey)
if err != nil {
return nil, fmt.Errorf("encrypt signing key: %w", err)
}
+4 -4
View File
@@ -11,8 +11,8 @@ import (
"sync"
"time"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvstore"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go/jetstream"
)
@@ -73,7 +73,7 @@ const appliancePasswordAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs
// ApplianceRecord is the persisted singleton record for the platform Postgres
// appliance. EncryptedPassword is AES-256-GCM ciphertext (base64, via
// iamdomain.EncryptSecret/DecryptSecret) — the plaintext master password
// handlers_iam.EncryptSecret/DecryptSecret) — the plaintext master password
// is never stored, logged, or held anywhere outside a launch/Connect call.
//
// No exported Appliance method ever returns an ApplianceRecord: Ensure
@@ -496,7 +496,7 @@ func (a *Appliance) getRecord(ctx context.Context) (*ApplianceRecord, uint64, er
// decryptPassword resolves rec's encrypted master password to plaintext. The
// error path never includes the ciphertext or any decrypted material.
func (a *Appliance) decryptPassword(rec *ApplianceRecord) (string, error) {
plaintext, err := iamdomain.DecryptSecret(rec.EncryptedPassword, a.masterKey)
plaintext, err := handlers_iam.DecryptSecret(rec.EncryptedPassword, a.masterKey)
if err != nil {
return "", fmt.Errorf("ochrevector: decrypt appliance master password: %w", err)
}
@@ -511,7 +511,7 @@ func newApplianceRecord(masterKey []byte) (ApplianceRecord, string, error) {
if err != nil {
return ApplianceRecord{}, "", err
}
encrypted, err := iamdomain.EncryptSecret(password, masterKey)
encrypted, err := handlers_iam.EncryptSecret(password, masterKey)
if err != nil {
return ApplianceRecord{}, "", fmt.Errorf("ochrevector: encrypt appliance master password: %w", err)
}
@@ -15,7 +15,7 @@ import (
"time"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -110,7 +110,7 @@ func TestCredentialRoundTrip(t *testing.T) {
require.NoError(t, err)
assert.Len(t, password, appliancePasswordLength)
encrypted, err := iamdomain.EncryptSecret(password, masterKey)
encrypted, err := handlers_iam.EncryptSecret(password, masterKey)
require.NoError(t, err)
assert.NotEqual(t, password, encrypted)
assert.NotContains(t, encrypted, password)
@@ -196,7 +196,7 @@ func TestEnsure_StaleProvisioningIsResumed(t *testing.T) {
seedPassword, err := generateMasterPassword()
require.NoError(t, err)
encrypted, err := iamdomain.EncryptSecret(seedPassword, masterKey)
encrypted, err := handlers_iam.EncryptSecret(seedPassword, masterKey)
require.NoError(t, err)
stale := time.Now().UTC().Add(-2 * applianceStaleAfter)
@@ -236,7 +236,7 @@ func TestEnsure_FreshProvisioningIsWaitedNotResumed(t *testing.T) {
seedPassword, err := generateMasterPassword()
require.NoError(t, err)
encrypted, err := iamdomain.EncryptSecret(seedPassword, masterKey)
encrypted, err := handlers_iam.EncryptSecret(seedPassword, masterKey)
require.NoError(t, err)
fresh := time.Now().UTC()
@@ -328,7 +328,7 @@ func TestResume_LaunchFailureLeavesRecordProvisioning(t *testing.T) {
seedPassword, err := generateMasterPassword()
require.NoError(t, err)
encrypted, err := iamdomain.EncryptSecret(seedPassword, masterKey)
encrypted, err := handlers_iam.EncryptSecret(seedPassword, masterKey)
require.NoError(t, err)
stale := time.Now().UTC().Add(-2 * applianceStaleAfter)
@@ -379,7 +379,7 @@ func TestConnect_NotAvailable(t *testing.T) {
password, err := generateMasterPassword()
require.NoError(t, err)
encrypted, err := iamdomain.EncryptSecret(password, masterKey)
encrypted, err := handlers_iam.EncryptSecret(password, masterKey)
require.NoError(t, err)
now := time.Now().UTC()
@@ -418,7 +418,7 @@ func seedAvailableAppliance(t *testing.T, appliance *Appliance, masterKey []byte
t.Helper()
password, err := generateMasterPassword()
require.NoError(t, err)
encrypted, err := iamdomain.EncryptSecret(password, masterKey)
encrypted, err := handlers_iam.EncryptSecret(password, masterKey)
require.NoError(t, err)
now := time.Now().UTC()
@@ -800,7 +800,7 @@ func TestDecryptPassword_WrongKeyErrors(t *testing.T) {
password, err := generateMasterPassword()
require.NoError(t, err)
encrypted, err := iamdomain.EncryptSecret(password, rightKey)
encrypted, err := handlers_iam.EncryptSecret(password, rightKey)
require.NoError(t, err)
appliance := &Appliance{masterKey: wrongKey}
@@ -5,22 +5,22 @@ import (
"testing"
"github.com/mulgadc/bluebottle/pkg/iampolicy"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// doc builds a single-statement policy document.
func doc(effect, action, resource string) iamdomain.PolicyDocument {
return iamdomain.PolicyDocument{
func doc(effect, action, resource string) handlers_iam.PolicyDocument {
return handlers_iam.PolicyDocument{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: effect, Action: iamdomain.StringOrArr{action}, Resource: iamdomain.StringOrArr{resource}},
Statement: []handlers_iam.Statement{
{Effect: effect, Action: handlers_iam.StringOrArr{action}, Resource: handlers_iam.StringOrArr{resource}},
},
}
}
// The deny-wins algorithm and wildcard matcher are unit-tested in predastore's
// pkg/iampolicy. These tests pin spinifex's own action-string conventions end to
// end through iampolicy.Evaluate, exercising the aliased domains/iam DTOs.
// end through iampolicy.Evaluate, exercising the aliased handlers_iam DTOs.
// TestEvaluate_IAMInstanceProfileActionStrings pins the action strings for
// iam:PassRole + the four EC2 instance-profile association actions so a rename of
@@ -34,7 +34,7 @@ func TestEvaluate_IAMInstanceProfileActionStrings(t *testing.T) {
"ec2:ReplaceIamInstanceProfileAssociation",
"ec2:DescribeIamInstanceProfileAssociations",
}
policies := []iamdomain.PolicyDocument{
policies := []handlers_iam.PolicyDocument{
doc("Allow", "*", "*"),
}
for _, a := range actions {
@@ -43,7 +43,7 @@ func TestEvaluate_IAMInstanceProfileActionStrings(t *testing.T) {
}
}
scoped := []iamdomain.PolicyDocument{
scoped := []handlers_iam.PolicyDocument{
doc("Allow", "ec2:*IamInstanceProfile*", "*"),
doc("Allow", "iam:PassRole", "arn:aws:iam::*:role/*"),
}
@@ -80,8 +80,8 @@ func TestEvaluate_STSActionStrings(t *testing.T) {
"sts:GetCallerIdentity",
}
wildcard := []iamdomain.PolicyDocument{doc("Allow", "*", "*")}
scoped := []iamdomain.PolicyDocument{doc("Allow", "sts:*", "*")}
wildcard := []handlers_iam.PolicyDocument{doc("Allow", "*", "*")}
scoped := []handlers_iam.PolicyDocument{doc("Allow", "sts:*", "*")}
for _, a := range actions {
if got := iampolicy.EvaluateWithKeys(a, "*", wildcard, nil); got != iampolicy.Allow {
@@ -119,7 +119,7 @@ func TestEvaluate_ResourceScopedStatementsNeedARealARN(t *testing.T) {
const prod = "arn:aws:ec2:ap-southeast-2:123456789012:instance/i-prod"
const dev = "arn:aws:ec2:ap-southeast-2:123456789012:instance/i-dev"
fenced := []iamdomain.PolicyDocument{
fenced := []handlers_iam.PolicyDocument{
doc("Allow", "ec2:*", "*"),
doc("Deny", "ec2:TerminateInstances", "arn:aws:ec2:*:*:instance/i-prod"),
}
@@ -133,7 +133,7 @@ func TestEvaluate_ResourceScopedStatementsNeedARealARN(t *testing.T) {
t.Errorf("fenced policy against an unfenced instance: got %v, want Allow", got)
}
scopedAllow := []iamdomain.PolicyDocument{
scopedAllow := []handlers_iam.PolicyDocument{
doc("Allow", "ec2:*", "arn:aws:ec2:*:*:instance/i-dev"),
}
if got := iampolicy.EvaluateWithKeys("ec2:TerminateInstances", "*", scopedAllow, nil); got != iampolicy.Deny {
@@ -161,7 +161,7 @@ func TestEvaluate_IAMResourceARNs(t *testing.T) {
"arn:aws:iam::123456789012:oidc-provider/issuer.example/id/cluster",
}
for _, resource := range resources {
policies := []iamdomain.PolicyDocument{doc("Allow", "iam:Delete*", resource)}
policies := []handlers_iam.PolicyDocument{doc("Allow", "iam:Delete*", resource)}
if got := iampolicy.EvaluateWithKeys("iam:DeleteRole", resource, policies, nil); got != iampolicy.Allow {
t.Errorf("IAM scoped policy against %q: got %v, want Allow", resource, got)
}
@@ -176,7 +176,7 @@ func TestEvaluate_PassingARealARNCannotWithdrawAccess(t *testing.T) {
"arn:aws:ec2:ap-southeast-2:123456789012:instance/i-abc/admin",
"not-an-arn",
}
policies := [][]iamdomain.PolicyDocument{
policies := [][]handlers_iam.PolicyDocument{
{doc("Allow", "ec2:*", "*")},
{doc("Allow", "*", "*")},
}
@@ -287,7 +287,7 @@ func TestNATSRequest_ErrorResponse(t *testing.T) {
}
// TestNATSRequest_DecodedErrorContainsCode pins the shape strings.Contains(err.Error(), code)
// classifiers depend on (e.g. domains/iam/provision.go's isAlreadyExists,
// classifiers depend on (e.g. handlers/iam/provision.go's isAlreadyExists,
// gateway/auth.go's IAM-not-found branch): the decoded error's Error() text
// must still contain the wire code even when a message rides alongside it.
func TestNATSRequest_DecodedErrorContainsCode(t *testing.T) {
+13 -13
View File
@@ -17,8 +17,8 @@ import (
"github.com/mulgadc/bluebottle/pkg/sigv4"
"github.com/mulgadc/spinifex/internal/testkit"
acmawsapi "github.com/mulgadc/spinifex/spinifex/domains/acm/awsapi"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/telemetry"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/envelope"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
@@ -31,9 +31,9 @@ import (
// acmPipelineApp wires SigV4AuthMiddleware -> Request as SetupRoutes does, on
// live test NATS, with docs as every principal's policy. The region is
// testRegion because signTestRequestAs always signs for it.
func acmPipelineApp(t *testing.T, docs []iamdomain.PolicyDocument) (*GatewayConfig, http.Handler) {
func acmPipelineApp(t *testing.T, docs []handlers_iam.PolicyDocument) (*GatewayConfig, http.Handler) {
t.Helper()
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
_, nc := testutil.StartTestNATS(t)
@@ -44,7 +44,7 @@ func acmPipelineApp(t *testing.T, docs []iamdomain.PolicyDocument) (*GatewayConf
IAMService: &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -54,7 +54,7 @@ func acmPipelineApp(t *testing.T, docs []iamdomain.PolicyDocument) (*GatewayConf
},
},
},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
},
}
@@ -68,11 +68,11 @@ func acmPipelineApp(t *testing.T, docs []iamdomain.PolicyDocument) (*GatewayConf
// allowAllACMDocs is the policy acmPipelineApp uses when the test is not
// exercising authorization itself.
func allowAllACMDocs() []iamdomain.PolicyDocument {
return []iamdomain.PolicyDocument{{
func allowAllACMDocs() []handlers_iam.PolicyDocument {
return []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{"*"}, Resource: iamdomain.StringOrArr{"*"}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"*"}, Resource: handlers_iam.StringOrArr{"*"}},
},
}}
}
@@ -209,13 +209,13 @@ func TestACMPipeline_OversizedBody_413OverHTTP(t *testing.T) {
// dispatch runs, and denies, before the NATS-backed action handler is ever
// invoked, through the full pipeline.
func TestACMPipeline_AuthzGateBlocksAction(t *testing.T) {
scopedDocs := []iamdomain.PolicyDocument{{
scopedDocs := []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
Statement: []handlers_iam.Statement{
{
Effect: "Allow",
Action: iamdomain.StringOrArr{"acm:DeleteCertificate"},
Resource: iamdomain.StringOrArr{"arn:aws:acm:" + testRegion + ":" + authzAccountID + ":certificate/other"},
Action: handlers_iam.StringOrArr{"acm:DeleteCertificate"},
Resource: handlers_iam.StringOrArr{"arn:aws:acm:" + testRegion + ":" + authzAccountID + ":certificate/other"},
},
},
}}
+3 -3
View File
@@ -9,8 +9,8 @@ import (
"github.com/mulgadc/spinifex/internal/testkit"
admissionquota "github.com/mulgadc/spinifex/spinifex/domains/admission/quota"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/require"
)
@@ -25,11 +25,11 @@ type quotaIAMService struct {
known string
}
func (m *quotaIAMService) GetAccount(accountID string) (*iamdomain.Account, error) {
func (m *quotaIAMService) GetAccount(accountID string) (*handlers_iam.Account, error) {
if accountID != m.known {
return nil, errors.New(awserrors.ErrorIAMNoSuchEntity)
}
return &iamdomain.Account{AccountID: accountID, Status: iamdomain.AccountStatusActive}, nil
return &handlers_iam.Account{AccountID: accountID, Status: handlers_iam.AccountStatusActive}, nil
}
// quotaTestGateway wires a real quota Service over a real JetStream KV bucket,
+11 -11
View File
@@ -13,9 +13,9 @@ import (
"time"
"github.com/mulgadc/spinifex/contracts/ec2/v1"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go/jetstream"
)
@@ -141,7 +141,7 @@ func (gw *GatewayConfig) adminCreateAccount(ctx context.Context, body []byte) (a
record, err := json.Marshal(adminIdemRecord{
State: adminIdemDone,
Name: iamdomain.NormalizeAccountName(req.Name),
Name: handlers_iam.NormalizeAccountName(req.Name),
Response: &stored,
StartedAt: time.Now().UTC().Format(time.RFC3339),
})
@@ -163,12 +163,12 @@ func (gw *GatewayConfig) adminCreateAccount(ctx context.Context, body []byte) (a
// admin identity and waits for the default VPC.
func (gw *GatewayConfig) provisionNewAccount(
ctx context.Context,
names *iamdomain.AccountNameIndex,
names *handlers_iam.AccountNameIndex,
req *CreateAccountRequest,
) (*CreateAccountResponse, error) {
// Accounts that predate the reservation index have no entry to collide
// with, so they are found by name scan. The same listing bounds the cap.
existing, err := iamdomain.FindAccountByName(gw.IAMService, req.Name)
existing, err := handlers_iam.FindAccountByName(gw.IAMService, req.Name)
if err != nil {
slog.Error("CreateAccount: failed to list accounts", "err", err)
return nil, errors.New(awserrors.ErrorInternalError)
@@ -181,16 +181,16 @@ func (gw *GatewayConfig) provisionNewAccount(
}
switch err := names.Reserve(ctx, req.Name, req.ClientToken); {
case errors.Is(err, iamdomain.ErrNameTaken):
case errors.Is(err, handlers_iam.ErrNameTaken):
return nil, errors.New(awserrors.ErrorAccountAlreadyExists)
case errors.Is(err, iamdomain.ErrNameInFlight):
case errors.Is(err, handlers_iam.ErrNameInFlight):
return nil, errors.New(awserrors.ErrorOperationInProgress)
case err != nil:
slog.Error("CreateAccount: failed to reserve account name", "err", err)
return nil, errors.New(awserrors.ErrorInternalError)
}
account, err := gw.IAMService.CreateAccount(iamdomain.NormalizeAccountName(req.Name))
account, err := gw.IAMService.CreateAccount(handlers_iam.NormalizeAccountName(req.Name))
if err != nil {
if relErr := names.Release(ctx, req.Name, req.ClientToken); relErr != nil {
slog.Error("CreateAccount: failed to release name reservation", "err", relErr)
@@ -208,7 +208,7 @@ func (gw *GatewayConfig) provisionNewAccount(
return nil, errors.New(awserrors.ErrorInternalError)
}
provisioned, err := iamdomain.ProvisionAccount(gw.IAMService, account.AccountID, account.AccountName)
provisioned, err := handlers_iam.ProvisionAccount(gw.IAMService, account.AccountID, account.AccountName)
if err != nil {
slog.Error("CreateAccount: failed to provision account",
"accountID", account.AccountID, "err", err)
@@ -239,7 +239,7 @@ func (gw *GatewayConfig) provisionNewAccount(
// response when the token has already completed, meaning the caller should
// replay it rather than provision again.
func claimClientToken(ctx context.Context, idem jetstream.KeyValue, clientToken, name string) (*CreateAccountResponse, error) {
normalized := iamdomain.NormalizeAccountName(name)
normalized := handlers_iam.NormalizeAccountName(name)
claim, err := json.Marshal(adminIdemRecord{
State: adminIdemInProgress,
Name: normalized,
@@ -340,14 +340,14 @@ func (gw *GatewayConfig) ensureDefaultVPC(ctx context.Context, accountID string)
// adminStores opens the name index and the idempotency bucket. Both are opened
// per request rather than cached on the gateway so a bucket created after
// startup (a cluster upgraded into this feature) is picked up without a restart.
func (gw *GatewayConfig) adminStores(ctx context.Context) (*iamdomain.AccountNameIndex, jetstream.KeyValue, error) {
func (gw *GatewayConfig) adminStores(ctx context.Context) (*handlers_iam.AccountNameIndex, jetstream.KeyValue, error) {
js, err := jetstream.New(gw.NATSConn)
if err != nil {
slog.Error("CreateAccount: failed to get JetStream context", "err", err)
return nil, nil, errors.New(awserrors.ErrorServiceUnavailable)
}
names, err := iamdomain.NewAccountNameIndex(ctx, js)
names, err := handlers_iam.NewAccountNameIndex(ctx, js)
if err != nil {
slog.Error("CreateAccount: failed to open account name index", "err", err)
return nil, nil, errors.New(awserrors.ErrorServiceUnavailable)
+7 -7
View File
@@ -14,9 +14,9 @@ import (
"github.com/mulgadc/spinifex/contracts/ec2/v1"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
@@ -27,17 +27,17 @@ const testClientToken = "0123456789abcdef0123456789abcdef"
// capMockIAMService reports a fixed account count for the cap check.
type capMockIAMService struct {
iamdomain.IAMService
handlers_iam.IAMService
accounts int
err error
}
func (m *capMockIAMService) ListAccounts() ([]*iamdomain.Account, error) {
func (m *capMockIAMService) ListAccounts() ([]*handlers_iam.Account, error) {
if m.err != nil {
return nil, m.err
}
return make([]*iamdomain.Account, m.accounts), nil
return make([]*handlers_iam.Account, m.accounts), nil
}
func TestValidateCreateAccountRequest(t *testing.T) {
@@ -239,9 +239,9 @@ func newCreateAccountGateway(t *testing.T) *GatewayConfig {
t.Helper()
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := iamdomain.NewIAMServiceImpl(t.Context(), nc, masterKey)
svc, err := handlers_iam.NewIAMServiceImpl(t.Context(), nc, masterKey)
require.NoError(t, err)
sub, err := nc.Subscribe(ec2v1.EnsureDefaultVpcSubject, func(msg *nats.Msg) {
@@ -276,7 +276,7 @@ func TestAdminCreateAccountProvisionsAndReplays(t *testing.T) {
assert.NotEmpty(t, created.AccountID)
assert.Equal(t, "ben@example.com", created.AccountName)
assert.Equal(t, iamdomain.AdminUserName, created.AdminUser)
assert.Equal(t, handlers_iam.AdminUserName, created.AdminUser)
assert.NotEmpty(t, created.AccessKeyID)
assert.NotEmpty(t, created.SecretAccessKey)
assert.Equal(t, "vpc-0a1b2c3d", created.DefaultVpcID)
@@ -13,7 +13,7 @@ import (
"testing"
"github.com/mulgadc/spinifex/spinifex/accountteardown"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -23,14 +23,14 @@ const dryRunShadowMessage = "Account teardown dry-run EC2 platform shadow compar
// deleteAccountGrant is the real IAM service with a policy granting
// spinifex:DeleteAccount, so a request clears the admin gates.
type deleteAccountGrant struct {
iamdomain.IAMService
handlers_iam.IAMService
}
func (deleteAccountGrant) GetUserPolicies(string, string) ([]iamdomain.PolicyDocument, error) {
return []iamdomain.PolicyDocument{{
func (deleteAccountGrant) GetUserPolicies(string, string) ([]handlers_iam.PolicyDocument, error) {
return []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{{
Effect: "Allow", Action: iamdomain.StringOrArr{"spinifex:DeleteAccount"}, Resource: iamdomain.StringOrArr{"*"},
Statement: []handlers_iam.Statement{{
Effect: "Allow", Action: handlers_iam.StringOrArr{"spinifex:DeleteAccount"}, Resource: handlers_iam.StringOrArr{"*"},
}},
}}, nil
}
@@ -15,9 +15,9 @@ import (
ec2v1 "github.com/mulgadc/spinifex/contracts/ec2/v1"
"github.com/mulgadc/spinifex/internal/testkit"
"github.com/mulgadc/spinifex/spinifex/accountteardown"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
"github.com/mulgadc/spinifex/spinifex/foundation/messaging/nats"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/mulgadc/spinifex/spinifex/providers/objectstore"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
@@ -44,9 +44,9 @@ func newDeleteAccountGateway(t *testing.T) *GatewayConfig {
t.Helper()
_, nc, _ := testutil.StartTestJetStream(t)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
svc, err := iamdomain.NewIAMServiceImpl(t.Context(), nc, masterKey)
svc, err := handlers_iam.NewIAMServiceImpl(t.Context(), nc, masterKey)
require.NoError(t, err)
for _, subject := range teardownSubjects {
@@ -139,7 +139,7 @@ func (emptyBucketStore) DeleteBucket(context.Context, string) error { return nil
// newTenantAccount creates an account that is safe to delete. Account ids are
// sequential and the first belongs to the super admin, which is protected, so
// the tenant under test can never be the first one created.
func newTenantAccount(t *testing.T, gw *GatewayConfig, name string) *iamdomain.Account {
func newTenantAccount(t *testing.T, gw *GatewayConfig, name string) *handlers_iam.Account {
t.Helper()
if accounts, err := gw.IAMService.ListAccounts(); err == nil && len(accounts) == 0 {
_, err := gw.IAMService.CreateAccount("super-admin@example.com")
@@ -207,7 +207,7 @@ func TestAdminDeleteAccountDryRunChangesNothing(t *testing.T) {
stored, err := gw.IAMService.GetAccount(account.AccountID)
require.NoError(t, err)
assert.Equal(t, iamdomain.AccountStatusActive, stored.Status)
assert.Equal(t, handlers_iam.AccountStatusActive, stored.Status)
// No job record means a later real delete is not answered as a replay.
_, err = gw.adminDescribeAccountDeletion(t.Context(),
@@ -428,7 +428,7 @@ func TestAdminDeleteAccountRefusesAMismatchedName(t *testing.T) {
stored, err := gw.IAMService.GetAccount(account.AccountID)
require.NoError(t, err)
assert.Equal(t, iamdomain.AccountStatusActive, stored.Status)
assert.Equal(t, handlers_iam.AccountStatusActive, stored.Status)
}
func TestAdminDeleteAccountRejectsAnUnknownAccount(t *testing.T) {
+8 -8
View File
@@ -13,8 +13,8 @@ import (
"time"
"github.com/mulgadc/spinifex/spinifex/accountteardown"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -23,11 +23,11 @@ import (
type listAccountsIAMService struct {
policyMockIAMService
accounts []*iamdomain.Account
accounts []*handlers_iam.Account
err error
}
func (m *listAccountsIAMService) ListAccounts() ([]*iamdomain.Account, error) {
func (m *listAccountsIAMService) ListAccounts() ([]*handlers_iam.Account, error) {
return m.accounts, m.err
}
@@ -177,10 +177,10 @@ func TestTeardownErrorMapping(t *testing.T) {
// that is how a stuck TERMINATING account is noticed — and no key material.
func TestAdminListAccountsReportsStatus(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, IAMService: &listAccountsIAMService{
accounts: []*iamdomain.Account{
{AccountID: "000000000042", AccountName: "t@example.com", Status: iamdomain.AccountStatusActive},
accounts: []*handlers_iam.Account{
{AccountID: "000000000042", AccountName: "t@example.com", Status: handlers_iam.AccountStatusActive},
nil,
{AccountID: "000000000043", AccountName: "u@example.com", Status: iamdomain.AccountStatusTerminating},
{AccountID: "000000000043", AccountName: "u@example.com", Status: handlers_iam.AccountStatusTerminating},
},
}}
@@ -190,8 +190,8 @@ func TestAdminListAccountsReportsStatus(t *testing.T) {
response, ok := output.(*ListAccountsResponse)
require.True(t, ok)
assert.Equal(t, 2, response.Count)
assert.Equal(t, iamdomain.AccountStatusActive, response.Accounts[0].Status)
assert.Equal(t, iamdomain.AccountStatusTerminating, response.Accounts[1].Status)
assert.Equal(t, handlers_iam.AccountStatusActive, response.Accounts[0].Status)
assert.Equal(t, handlers_iam.AccountStatusTerminating, response.Accounts[1].Status)
}
// Failing closed matters here: an empty listing reads as "no accounts", which
+7 -7
View File
@@ -14,9 +14,9 @@ import (
"testing"
"github.com/go-chi/chi/v5"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -47,19 +47,19 @@ func TestAdminPathMethod(t *testing.T) {
// createAccountPolicy grants exactly the action the signup principal holds.
func createAccountPolicy() *policyMockIAMService {
allow := []iamdomain.PolicyDocument{{
allow := []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
Statement: []handlers_iam.Statement{
{
Effect: "Allow",
Action: iamdomain.StringOrArr{"spinifex:CreateAccount"},
Resource: iamdomain.StringOrArr{"*"},
Action: handlers_iam.StringOrArr{"spinifex:CreateAccount"},
Resource: handlers_iam.StringOrArr{"*"},
},
},
}}
return &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return allow, nil },
getRolePoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return allow, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return allow, nil },
getRolePoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return allow, nil },
}
}
+2 -2
View File
@@ -13,9 +13,9 @@ import (
"uuid"
"github.com/mulgadc/bluebottle/pkg/sigv4"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
telemetry "github.com/mulgadc/spinifex/spinifex/foundation/telemetry"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/dispatch"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/envelope"
@@ -383,7 +383,7 @@ func (gw *GatewayConfig) resolveLongLivedAKID(accessKeyID string, src authSource
slog.Error("IAM lookup failed", "accessKeyID", accessKeyID, "err", err)
return "", principalContext{}, awserrors.ErrorInternalError
}
if ak.Status != iamdomain.AccessKeyStatusActive {
if ak.Status != handlers_iam.AccessKeyStatusActive {
slog.Warn("Auth failure: access key inactive", "accessKeyID", accessKeyID, "sourceIP", src.logIP)
gw.RateLimiter.RecordFailure(src.limitKey, authlimit.Fingerprint("akid-inactive", accessKeyID))
return "", principalContext{}, awserrors.ErrorInvalidClientTokenId
+2 -2
View File
@@ -5,8 +5,8 @@ import (
"sync"
"time"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// accountActiveTTL bounds how long an account is trusted to still be ACTIVE
@@ -86,7 +86,7 @@ func (gw *GatewayConfig) checkAccountActive(accountID string, src authSource) st
return awserrors.ErrorInternalError
}
if account.Status != iamdomain.AccountStatusActive {
if account.Status != handlers_iam.AccountStatusActive {
slog.Warn("Auth failure: account is not active",
"accountID", accountID, "status", account.Status, "sourceIP", src.logIP)
return awserrors.ErrorAccessDenied
+11 -11
View File
@@ -11,12 +11,12 @@ import (
"time"
"github.com/go-chi/chi/v5"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// statusGateRouter builds a signed-request harness whose only job is to report
// whether the auth middleware let the request through.
func statusGateRouter(t *testing.T, svc iamdomain.IAMService) (chi.Router, *http.Request) {
func statusGateRouter(t *testing.T, svc handlers_iam.IAMService) (chi.Router, *http.Request) {
t.Helper()
gw := &GatewayConfig{DisableLogging: true, Region: testRegion, IAMService: svc}
@@ -35,29 +35,29 @@ func statusGateRouter(t *testing.T, svc iamdomain.IAMService) (chi.Router, *http
func statusGateService(t *testing.T, accountID, status string) *mockIAMService {
t.Helper()
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("encrypt secret: %v", err)
}
return &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
UserName: "alice",
AccountID: accountID,
Status: iamdomain.AccessKeyStatusActive,
Status: handlers_iam.AccessKeyStatusActive,
},
},
accounts: map[string]*iamdomain.Account{
accounts: map[string]*handlers_iam.Account{
accountID: {AccountID: accountID, Status: status},
},
}
}
func TestAuthAllowsAnActiveAccount(t *testing.T) {
r, req := statusGateRouter(t, statusGateService(t, "123456789012", iamdomain.AccountStatusActive))
r, req := statusGateRouter(t, statusGateService(t, "123456789012", handlers_iam.AccountStatusActive))
if resp := doRequest(r, req); resp.StatusCode != http.StatusOK {
t.Fatalf("expected 200 for an ACTIVE account, got %d", resp.StatusCode)
@@ -67,7 +67,7 @@ func TestAuthAllowsAnActiveAccount(t *testing.T) {
// Until this gate existed, account status was enforced only in ECR's principal
// resolution, so suspending an account left EC2, S3 and STS wide open.
func TestAuthDeniesASuspendedAccount(t *testing.T) {
r, req := statusGateRouter(t, statusGateService(t, "123456789012", iamdomain.AccountStatusSuspended))
r, req := statusGateRouter(t, statusGateService(t, "123456789012", handlers_iam.AccountStatusSuspended))
if resp := doRequest(r, req); resp.StatusCode != http.StatusForbidden {
t.Fatalf("expected 403 for a SUSPENDED account, got %d", resp.StatusCode)
@@ -77,7 +77,7 @@ func TestAuthDeniesASuspendedAccount(t *testing.T) {
// The quiesce that makes teardown converge: a terminating account must not be
// able to create resources the cascade has already walked past.
func TestAuthDeniesATerminatingAccount(t *testing.T) {
r, req := statusGateRouter(t, statusGateService(t, "123456789012", iamdomain.AccountStatusTerminating))
r, req := statusGateRouter(t, statusGateService(t, "123456789012", handlers_iam.AccountStatusTerminating))
if resp := doRequest(r, req); resp.StatusCode != http.StatusForbidden {
t.Fatalf("expected 403 for a TERMINATING account, got %d", resp.StatusCode)
@@ -89,13 +89,13 @@ type unreadableAccountService struct {
*mockIAMService
}
func (s *unreadableAccountService) GetAccount(string) (*iamdomain.Account, error) {
func (s *unreadableAccountService) GetAccount(string) (*handlers_iam.Account, error) {
return nil, errors.New("account store unavailable")
}
// An account we cannot read is not evidence of an account in good standing.
func TestAuthFailsClosedWhenTheAccountCannotBeRead(t *testing.T) {
svc := &unreadableAccountService{statusGateService(t, "123456789012", iamdomain.AccountStatusActive)}
svc := &unreadableAccountService{statusGateService(t, "123456789012", handlers_iam.AccountStatusActive)}
r, req := statusGateRouter(t, svc)
if resp := doRequest(r, req); resp.StatusCode == http.StatusOK {
+18 -18
View File
@@ -28,11 +28,11 @@ import (
"github.com/stretchr/testify/require"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
awserrors "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
awsidentifiers "github.com/mulgadc/spinifex/spinifex/foundation/aws/identifiers"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
)
@@ -56,7 +56,7 @@ type cbCreds struct {
// cbDecryptCounter is the real IAM service with DecryptSecret counted, so a test
// can observe whether the door decrypted a secret before rejecting a request.
type cbDecryptCounter struct {
*iamdomain.IAMServiceImpl
*handlers_iam.IAMServiceImpl
decrypts atomic.Int32
}
@@ -79,7 +79,7 @@ type cbEnv struct {
nc *nats.Conn // the gateway's connection
iamNC *nats.Conn // IAM/STS connection; == nc unless separateIAMConn
js jetstream.JetStream
iam *iamdomain.IAMServiceImpl
iam *handlers_iam.IAMServiceImpl
sts *handlers_sts.STSServiceImpl
counter *cbDecryptCounter
h http.Handler
@@ -100,9 +100,9 @@ func newCBEnv(t *testing.T, opts cbEnvOpts) *cbEnv {
e.iamNC = iamNC
}
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
e.iam, err = iamdomain.NewIAMServiceImpl(t.Context(), e.iamNC, masterKey)
e.iam, err = handlers_iam.NewIAMServiceImpl(t.Context(), e.iamNC, masterKey)
require.NoError(t, err)
iamJS := testutil.NewJetStream(t, e.iamNC)
e.sts, err = handlers_sts.NewSTSServiceImpl(t.Context(), e.iamNC, e.iam, masterKey, handlers_eks.NewOIDCJWKSReader(iamJS))
@@ -111,9 +111,9 @@ func newCBEnv(t *testing.T, opts cbEnvOpts) *cbEnv {
e.iam.SetSessionRevoker(e.sts)
}
encrypted, err := iamdomain.EncryptSecret(cbRootSecret, masterKey)
encrypted, err := handlers_iam.EncryptSecret(cbRootSecret, masterKey)
require.NoError(t, err)
require.NoError(t, e.iam.SeedBootstrap(&iamdomain.BootstrapData{
require.NoError(t, e.iam.SeedBootstrap(&handlers_iam.BootstrapData{
AccessKeyID: cbRootAKID, EncryptedSecret: encrypted, AccountID: awsidentifiers.GlobalAccountID,
}))
@@ -365,7 +365,7 @@ func TestCurrentBehaviour_Auth_LongLivedRejections(t *testing.T) {
creds := e.newKey(t, "alice")
require.Equal(t, http.StatusOK, e.callerIdentity(t, creds).Code)
_, err := e.iam.UpdateAccessKey(e.accountID, &iam.UpdateAccessKeyInput{
UserName: aws.String("alice"), AccessKeyId: aws.String(creds.akid), Status: aws.String(iamdomain.AccessKeyStatusInactive),
UserName: aws.String("alice"), AccessKeyId: aws.String(creds.akid), Status: aws.String(handlers_iam.AccessKeyStatusInactive),
})
require.NoError(t, err)
assert.Equal(t, cbRegistry(awserrors.ErrorInvalidClientTokenId), cbOutcomeOf(e.callerIdentity(t, creds)))
@@ -462,7 +462,7 @@ func TestCurrentBehaviour_Session_GetSessionTokenOutlivesMintingKey(t *testing.T
assert.Equal(t, e.aliceID, cbXML(rec.Body.String(), "UserId"))
_, err := e.iam.UpdateAccessKey(e.accountID, &iam.UpdateAccessKeyInput{
UserName: aws.String("alice"), AccessKeyId: aws.String(mintKey.akid), Status: aws.String(iamdomain.AccessKeyStatusInactive),
UserName: aws.String("alice"), AccessKeyId: aws.String(mintKey.akid), Status: aws.String(handlers_iam.AccessKeyStatusInactive),
})
require.NoError(t, err)
_, err = e.iam.DeleteAccessKey(e.accountID, &iam.DeleteAccessKeyInput{
@@ -486,9 +486,9 @@ func TestCurrentBehaviour_Session_WebIdentityCredentials(t *testing.T) {
issuer := testDiscIssuer
iamKV, err := kvutil.GetOrCreateBucket(t.Context(), e.js,
iamdomain.IAMAccountBucketName(e.accountID), iamdomain.KVBucketIAMAccountVersion)
handlers_iam.IAMAccountBucketName(e.accountID), handlers_iam.KVBucketIAMAccountVersion)
require.NoError(t, err)
_, err = iamKV.Put(t.Context(), iamdomain.OIDCProviderKey(issuer), []byte(`{"registered":true}`))
_, err = iamKV.Put(t.Context(), handlers_iam.OIDCProviderKey(issuer), []byte(`{"registered":true}`))
require.NoError(t, err)
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@@ -506,7 +506,7 @@ func TestCurrentBehaviour_Session_WebIdentityCredentials(t *testing.T) {
_, err = eksKV.Put(t.Context(), handlers_eks.OIDCJWKSKey(testDiscCluster), jwks)
require.NoError(t, err)
federated := iamdomain.OIDCProviderARN(e.accountID, strings.TrimPrefix(issuer, "https://"))
federated := handlers_iam.OIDCProviderARN(e.accountID, strings.TrimPrefix(issuer, "https://"))
role, err := e.iam.CreateRole(e.accountID, &iam.CreateRoleInput{
RoleName: aws.String("irsa"),
AssumeRolePolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":"` +
@@ -640,11 +640,11 @@ func TestCurrentBehaviour_Revocation_OrphanedKeyAuthenticatesAsRecreatedUser(t *
// access-key records in place, the state an interrupted DeleteAccessKey leaves.
func (e *cbEnv) orphanKeys(t *testing.T, user string) {
t.Helper()
usersKV, err := e.js.KeyValue(t.Context(), iamdomain.KVBucketUsers)
usersKV, err := e.js.KeyValue(t.Context(), handlers_iam.KVBucketUsers)
require.NoError(t, err)
entry, err := usersKV.Get(t.Context(), e.accountID+"."+user)
require.NoError(t, err)
var u iamdomain.User
var u handlers_iam.User
require.NoError(t, json.Unmarshal(entry.Value(), &u))
u.AccessKeys = []string{}
data, err := json.Marshal(u)
@@ -744,7 +744,7 @@ func TestCurrentBehaviour_Auth_BackingStateUnavailable(t *testing.T) {
t.Run("access_key_bucket_deleted", func(t *testing.T) {
e := newCBEnv(t, cbEnvOpts{})
require.NoError(t, e.js.DeleteKeyValue(t.Context(), iamdomain.KVBucketAccessKeys))
require.NoError(t, e.js.DeleteKeyValue(t.Context(), handlers_iam.KVBucketAccessKeys))
for name, c := range map[string]cbCreds{"known": e.alice, "unknown": {akid: cbUnknownKey, secret: cbBadSecret}} {
assert.Equal(t, internal, cbOutcomeOf(e.request(t, e.h, c, "sts", gci)), name)
}
@@ -760,7 +760,7 @@ func TestCurrentBehaviour_Auth_BackingStateUnavailable(t *testing.T) {
t.Run("accounts_bucket_deleted", func(t *testing.T) {
e := newCBEnv(t, cbEnvOpts{})
require.NoError(t, e.js.DeleteKeyValue(t.Context(), iamdomain.KVBucketAccounts))
require.NoError(t, e.js.DeleteKeyValue(t.Context(), handlers_iam.KVBucketAccounts))
// Fails closed only once the signature holds; a bad one still reads as such.
assert.Equal(t, internal, cbOutcomeOf(e.callerIdentity(t, e.alice)))
assert.Equal(t, cbRegistry(awserrors.ErrorSignatureDoesNotMatch),
@@ -812,7 +812,7 @@ func TestCurrentBehaviour_AntiEnumeration_RejectionShapes(t *testing.T) {
e := newCBEnv(t, cbEnvOpts{})
bobKey, _ := e.newUser(t, "bob")
_, err := e.iam.UpdateAccessKey(e.accountID, &iam.UpdateAccessKeyInput{
UserName: aws.String("bob"), AccessKeyId: aws.String(bobKey.akid), Status: aws.String(iamdomain.AccessKeyStatusInactive),
UserName: aws.String("bob"), AccessKeyId: aws.String(bobKey.akid), Status: aws.String(handlers_iam.AccessKeyStatusInactive),
})
require.NoError(t, err)
carolKey, _ := e.newUser(t, "carol")
@@ -858,7 +858,7 @@ func TestCurrentBehaviour_AntiEnumeration_RejectionShapes(t *testing.T) {
func TestCurrentBehaviour_Cache_AccountStatusIsTheOnlyAuthCache(t *testing.T) {
require.Equal(t, 5*time.Second, accountActiveTTL)
for _, status := range []string{iamdomain.AccountStatusSuspended, iamdomain.AccountStatusTerminating} {
for _, status := range []string{handlers_iam.AccountStatusSuspended, handlers_iam.AccountStatusTerminating} {
t.Run(status, func(t *testing.T) {
e := newCBEnv(t, cbEnvOpts{})
require.Equal(t, http.StatusOK, e.callerIdentity(t, e.alice).Code)
+104 -104
View File
@@ -30,8 +30,8 @@ import (
"github.com/go-chi/chi/v5"
"github.com/mulgadc/bluebottle/pkg/sigv4"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
@@ -45,27 +45,27 @@ const (
testService = "ec2"
)
// mockIAMService implements iamdomain.IAMService for auth tests. It embeds
// mockIAMService implements handlers_iam.IAMService for auth tests. It embeds
// the interface so it satisfies the full contract; the SigV4 middleware drives
// LookupAccessKey, DecryptSecret and GetUser, so those are the methods wired.
// Any other method nil-panics, surfacing an unexpected call.
type mockIAMService struct {
iamdomain.IAMService
handlers_iam.IAMService
accessKeys map[string]*iamdomain.AccessKey
accessKeys map[string]*handlers_iam.AccessKey
masterKey []byte
// accounts overrides the status the auth gate reads. An empty map means
// every account is ACTIVE, which is what most tests here are asserting
// around rather than asserting about.
accounts map[string]*iamdomain.Account
accounts map[string]*handlers_iam.Account
// getUserFn overrides the synthetic user record below, for a test that cares
// what aws:userid resolves to or that it fails to resolve at all.
getUserFn func(accountID string, input *iam.GetUserInput) (*iam.GetUserOutput, error)
}
func (m *mockIAMService) LookupAccessKey(accessKeyID string) (*iamdomain.AccessKey, error) {
func (m *mockIAMService) LookupAccessKey(accessKeyID string) (*handlers_iam.AccessKey, error) {
ak, ok := m.accessKeys[accessKeyID]
if !ok {
return nil, errors.New(awserrors.ErrorIAMNoSuchEntity)
@@ -74,14 +74,14 @@ func (m *mockIAMService) LookupAccessKey(accessKeyID string) (*iamdomain.AccessK
}
func (m *mockIAMService) DecryptSecret(ciphertext string) (string, error) {
return iamdomain.DecryptSecret(ciphertext, m.masterKey)
return handlers_iam.DecryptSecret(ciphertext, m.masterKey)
}
func (m *mockIAMService) GetAccount(accountID string) (*iamdomain.Account, error) {
func (m *mockIAMService) GetAccount(accountID string) (*handlers_iam.Account, error) {
if account, ok := m.accounts[accountID]; ok {
return account, nil
}
return &iamdomain.Account{AccountID: accountID, Status: iamdomain.AccountStatusActive}, nil
return &handlers_iam.Account{AccountID: accountID, Status: handlers_iam.AccountStatusActive}, nil
}
// GetUser answers with a unique ID derived from the name, so aws:userid resolves
@@ -102,7 +102,7 @@ var testMasterKey []byte
func init() {
var err error
testMasterKey, err = iamdomain.GenerateMasterKey()
testMasterKey, err = handlers_iam.GenerateMasterKey()
if err != nil {
panic("failed to generate test master key: " + err.Error())
}
@@ -136,14 +136,14 @@ func signTestRequestAs(t *testing.T, req *http.Request, body []byte, accessKey,
}
func setupTestApp(accessKey, secretKey string) http.Handler {
encryptedSecret, err := iamdomain.EncryptSecret(secretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(secretKey, testMasterKey)
if err != nil {
panic("failed to encrypt test secret: " + err.Error())
}
mockSvc := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
accessKey: {
AccessKeyID: accessKey,
SecretAccessKey: encryptedSecret,
@@ -172,10 +172,10 @@ func setupTestApp(accessKey, secretKey string) http.Handler {
// the aws:userid branch that a user principal reaches in production.
func userIDApp(t *testing.T, svc *mockIAMService) (http.Handler, *string) {
t.Helper()
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
svc.masterKey = testMasterKey
svc.accessKeys = map[string]*iamdomain.AccessKey{
svc.accessKeys = map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -371,14 +371,14 @@ func TestSigV4Auth_ValidSignatureWithQueryString(t *testing.T) {
}
func TestSigV4Auth_InactiveAccessKey(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
mockSvc := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -480,18 +480,18 @@ func TestParseAWSQueryArgs_URLDecoding(t *testing.T) {
func TestSigV4Auth_DecryptFailure(t *testing.T) {
// Secret encrypted under a rotated master key — unverifiable credential must
// return 403 InvalidClientTokenId, not 500, so the client re-authenticates.
otherKey, err := iamdomain.GenerateMasterKey()
otherKey, err := handlers_iam.GenerateMasterKey()
if err != nil {
t.Fatalf("GenerateMasterKey() error: %v", err)
}
encryptedWithOther, err := iamdomain.EncryptSecret(testSecretKey, otherKey)
encryptedWithOther, err := handlers_iam.EncryptSecret(testSecretKey, otherKey)
if err != nil {
t.Fatalf("EncryptSecret() error: %v", err)
}
mockSvc := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedWithOther, // encrypted with wrong key
@@ -832,14 +832,14 @@ func TestSigV4Auth_S3ScopeReturnsFlatS3Error(t *testing.T) {
// --- Context Propagation Tests ---
func TestSigV4Auth_ContextPropagation(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
mockSvc := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -902,20 +902,20 @@ func TestSigV4Auth_ContextPropagation(t *testing.T) {
}
func TestSigV4Auth_ContextDoesNotLeakBetweenRequests(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
secondKey := "AKIAIOSFODNN7SECOND0"
encryptedSecret2, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret2, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
mockSvc := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -1034,14 +1034,14 @@ func TestSigV4Auth_PathWithSpecialCharacters(t *testing.T) {
func TestSigV4Auth_SignedContentType(t *testing.T) {
// Verify signature works when content-type is included in signed headers.
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
mockSvc := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -1146,18 +1146,18 @@ func TestSigV4Auth_QueryStringEdgeCases(t *testing.T) {
type policyMockIAMService struct {
mockIAMService
getUserPoliciesFn func(accountID, userName string) ([]iamdomain.PolicyDocument, error)
getRolePoliciesFn func(accountID, roleName string) ([]iamdomain.PolicyDocument, error)
getUserPoliciesFn func(accountID, userName string) ([]handlers_iam.PolicyDocument, error)
getRolePoliciesFn func(accountID, roleName string) ([]handlers_iam.PolicyDocument, error)
}
func (m *policyMockIAMService) GetUserPolicies(accountID, userName string) ([]iamdomain.PolicyDocument, error) {
func (m *policyMockIAMService) GetUserPolicies(accountID, userName string) ([]handlers_iam.PolicyDocument, error) {
if m.getUserPoliciesFn != nil {
return m.getUserPoliciesFn(accountID, userName)
}
return nil, nil
}
func (m *policyMockIAMService) GetRolePolicies(accountID, roleName string) ([]iamdomain.PolicyDocument, error) {
func (m *policyMockIAMService) GetRolePolicies(accountID, roleName string) ([]handlers_iam.PolicyDocument, error) {
if m.getRolePoliciesFn != nil {
return m.getRolePoliciesFn(accountID, roleName)
}
@@ -1171,7 +1171,7 @@ type errorLookupMockIAMService struct {
err error
}
func (m *errorLookupMockIAMService) LookupAccessKey(_ string) (*iamdomain.AccessKey, error) {
func (m *errorLookupMockIAMService) LookupAccessKey(_ string) (*handlers_iam.AccessKey, error) {
return nil, m.err
}
@@ -1190,7 +1190,7 @@ func setupPolicyTestHandler(gw *GatewayConfig) http.Handler {
}
func TestCheckPolicy_NonRootNoPolicies_Denied(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
@@ -1198,7 +1198,7 @@ func TestCheckPolicy_NonRootNoPolicies_Denied(t *testing.T) {
mockSvc := &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -1208,7 +1208,7 @@ func TestCheckPolicy_NonRootNoPolicies_Denied(t *testing.T) {
},
},
},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return nil, nil // no policies
},
}
@@ -1239,7 +1239,7 @@ func TestCheckPolicy_NonRootNoPolicies_Denied(t *testing.T) {
}
func TestCheckPolicy_NonRootWithAllow_Passes(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
@@ -1247,7 +1247,7 @@ func TestCheckPolicy_NonRootWithAllow_Passes(t *testing.T) {
mockSvc := &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -1257,15 +1257,15 @@ func TestCheckPolicy_NonRootWithAllow_Passes(t *testing.T) {
},
},
},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
return []iamdomain.PolicyDocument{
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return []handlers_iam.PolicyDocument{
{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
Statement: []handlers_iam.Statement{
{
Effect: "Allow",
Action: iamdomain.StringOrArr{"ec2:RunInstances"},
Resource: iamdomain.StringOrArr{"*"},
Action: handlers_iam.StringOrArr{"ec2:RunInstances"},
Resource: handlers_iam.StringOrArr{"*"},
},
},
},
@@ -1294,7 +1294,7 @@ func TestCheckPolicy_NonRootWithAllow_Passes(t *testing.T) {
}
func TestCheckPolicy_NonRootWithExplicitDeny_Denied(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
@@ -1302,7 +1302,7 @@ func TestCheckPolicy_NonRootWithExplicitDeny_Denied(t *testing.T) {
mockSvc := &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -1312,20 +1312,20 @@ func TestCheckPolicy_NonRootWithExplicitDeny_Denied(t *testing.T) {
},
},
},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
return []iamdomain.PolicyDocument{
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return []handlers_iam.PolicyDocument{
{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
Statement: []handlers_iam.Statement{
{
Effect: "Allow",
Action: iamdomain.StringOrArr{"ec2:*"},
Resource: iamdomain.StringOrArr{"*"},
Action: handlers_iam.StringOrArr{"ec2:*"},
Resource: handlers_iam.StringOrArr{"*"},
},
{
Effect: "Deny",
Action: iamdomain.StringOrArr{"ec2:RunInstances"},
Resource: iamdomain.StringOrArr{"*"},
Action: handlers_iam.StringOrArr{"ec2:RunInstances"},
Resource: handlers_iam.StringOrArr{"*"},
},
},
},
@@ -1359,7 +1359,7 @@ func TestCheckPolicy_NonRootWithExplicitDeny_Denied(t *testing.T) {
}
func TestCheckPolicy_RootGlobalAccount_Bypasses(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
@@ -1367,7 +1367,7 @@ func TestCheckPolicy_RootGlobalAccount_Bypasses(t *testing.T) {
mockSvc := &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -1377,16 +1377,16 @@ func TestCheckPolicy_RootGlobalAccount_Bypasses(t *testing.T) {
},
},
},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
// Return explicit deny — root should bypass anyway
return []iamdomain.PolicyDocument{
return []handlers_iam.PolicyDocument{
{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
Statement: []handlers_iam.Statement{
{
Effect: "Deny",
Action: iamdomain.StringOrArr{"*"},
Resource: iamdomain.StringOrArr{"*"},
Action: handlers_iam.StringOrArr{"*"},
Resource: handlers_iam.StringOrArr{"*"},
},
},
},
@@ -1415,7 +1415,7 @@ func TestCheckPolicy_RootGlobalAccount_Bypasses(t *testing.T) {
}
func TestCheckPolicy_MissingAccountID_InternalError(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
@@ -1424,7 +1424,7 @@ func TestCheckPolicy_MissingAccountID_InternalError(t *testing.T) {
mockSvc := &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -1457,7 +1457,7 @@ func TestCheckPolicy_MissingAccountID_InternalError(t *testing.T) {
}
func TestCheckPolicy_GetUserPoliciesError_InternalError(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
@@ -1465,7 +1465,7 @@ func TestCheckPolicy_GetUserPoliciesError_InternalError(t *testing.T) {
mockSvc := &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -1475,7 +1475,7 @@ func TestCheckPolicy_GetUserPoliciesError_InternalError(t *testing.T) {
},
},
},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return nil, errors.New("nats: timeout")
},
}
@@ -1503,7 +1503,7 @@ func TestCheckPolicy_GetUserPoliciesError_InternalError(t *testing.T) {
func TestCheckPolicy_RootNonGlobalAccount_StillEvaluated(t *testing.T) {
// A user named "root" but on a non-global account should still have
// policies evaluated (only root + GlobalAccountID bypasses).
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
if err != nil {
t.Fatalf("Failed to encrypt secret: %v", err)
}
@@ -1511,7 +1511,7 @@ func TestCheckPolicy_RootNonGlobalAccount_StillEvaluated(t *testing.T) {
mockSvc := &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -1521,7 +1521,7 @@ func TestCheckPolicy_RootNonGlobalAccount_StillEvaluated(t *testing.T) {
},
},
},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return nil, nil // no policies → should deny
},
}
@@ -1728,7 +1728,7 @@ const (
// real AES-GCM so the SigV4 verify path exercises the actual decrypt code.
func setupSessionTestApp(t *testing.T, expiresAt time.Time) (http.Handler, *mockSTSService) {
t.Helper()
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
cred := &handlers_sts.SessionCredential{
@@ -1752,7 +1752,7 @@ func setupSessionTestApp(t *testing.T, expiresAt time.Time) (http.Handler, *mock
// principal context; varies cred to drive the principal-type branch in resolveSessionAKID.
func mountSessionGateway(t *testing.T, cred *handlers_sts.SessionCredential) (http.Handler, *mockSTSService) {
t.Helper()
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
stsMock := &mockSTSService{
@@ -1762,7 +1762,7 @@ func mountSessionGateway(t *testing.T, cred *handlers_sts.SessionCredential) (ht
iamMock := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -1824,7 +1824,7 @@ func TestSigV4Auth_Session_ValidSignature(t *testing.T) {
func TestSigV4Auth_Session_UserPrincipal(t *testing.T) {
// GetSessionToken session (PrincipalType "user") must resolve back to the IAM
// user with no assumed-role fields, so buildCallerARN yields arn:aws:iam::A:user/N.
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
cred := &handlers_sts.SessionCredential{
AccessKeyID: testSessionAKID,
@@ -1856,7 +1856,7 @@ func TestSigV4Auth_Session_UserPrincipal(t *testing.T) {
func TestSigV4Auth_Session_EmptyPrincipalType_ResolvesAssumedRole(t *testing.T) {
// Pre-PrincipalType records have an empty value and must still resolve as assumed-role.
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
cred := &handlers_sts.SessionCredential{
AccessKeyID: testSessionAKID,
@@ -2111,12 +2111,12 @@ func TestSigV4Auth_UnknownAKIDPrefix_NoLookup(t *testing.T) {
func TestSigV4Auth_AKIA_PrincipalTypeUser(t *testing.T) {
// Long-lived AKIA path must set ctxPrincipalType=user for correct policy gating.
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
iamMock := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -2150,11 +2150,11 @@ func TestSigV4Auth_AKIA_PrincipalTypeUser(t *testing.T) {
func TestSigV4Auth_Session_STSServiceNil(t *testing.T) {
// Session AKID with nil STSService is a misconfiguration — must fail with InternalError.
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
iamMock := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{},
accessKeys: map[string]*handlers_iam.AccessKey{},
}
_ = encryptedSecret
gw := &GatewayConfig{
@@ -2184,7 +2184,7 @@ func TestSigV4Auth_Session_STSServiceNil(t *testing.T) {
// that grants nothing. The enforcement path must resolve the underlying role —
// never the SessionName — so the request is denied and GetUserPolicies is never called.
func TestCheckPolicy_SessionNameCollision(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
var userPoliciesCalled bool
@@ -2195,7 +2195,7 @@ func TestCheckPolicy_SessionNameCollision(t *testing.T) {
iamMock := &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -2205,11 +2205,11 @@ func TestCheckPolicy_SessionNameCollision(t *testing.T) {
},
},
},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
userPoliciesCalled = true
return allowPolicy("ec2:*"), nil
},
getRolePoliciesFn: func(_, roleName string) ([]iamdomain.PolicyDocument, error) {
getRolePoliciesFn: func(_, roleName string) ([]handlers_iam.PolicyDocument, error) {
resolvedRoleName = roleName
return nil, nil
},
@@ -2254,18 +2254,18 @@ func TestCheckPolicy_SessionNameCollision(t *testing.T) {
}
// allowPolicy returns a single-statement Allow policy granting action on "*".
func allowPolicy(action string) []iamdomain.PolicyDocument {
func allowPolicy(action string) []handlers_iam.PolicyDocument {
return allowPolicyResource(action, "*")
}
// allowPolicyResource returns a single-statement Allow policy for action on resource.
func allowPolicyResource(action, resource string) []iamdomain.PolicyDocument {
return []iamdomain.PolicyDocument{{
func allowPolicyResource(action, resource string) []handlers_iam.PolicyDocument {
return []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{{
Statement: []handlers_iam.Statement{{
Effect: "Allow",
Action: iamdomain.StringOrArr{action},
Resource: iamdomain.StringOrArr{resource},
Action: handlers_iam.StringOrArr{action},
Resource: handlers_iam.StringOrArr{resource},
}},
}}
}
@@ -2288,16 +2288,16 @@ func assumedRoleSessionCred(sessionName, underlyingRoleARN, accountID string) *h
// newAssumedRoleEnforcementGateway wires a gateway for assumed-role checkPolicy tests.
// The user resolver fails the test if called — an assumed-role must never be evaluated as a user.
func newAssumedRoleEnforcementGateway(t *testing.T, cred *handlers_sts.SessionCredential,
getRoleFn func(accountID, roleName string) ([]iamdomain.PolicyDocument, error),
getRoleFn func(accountID, roleName string) ([]handlers_iam.PolicyDocument, error),
) *GatewayConfig {
t.Helper()
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
cred.SecretEncrypted = encryptedSecret
iamMock := &policyMockIAMService{
mockIAMService: mockIAMService{masterKey: testMasterKey},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
t.Errorf("priv-esc: assumed-role principal must not consult GetUserPolicies")
return nil, nil
},
@@ -2344,7 +2344,7 @@ func doSessionPolicyRequest(t *testing.T, handler http.Handler, akid string) *ht
// predastore's assumed-role S3 enforcement.
func TestCheckPolicy_AssumedRole_AllowedByRolePolicy(t *testing.T) {
cred := assumedRoleSessionCred("app", "arn:aws:iam::123456789012:role/app-role", "123456789012")
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, roleName string) ([]iamdomain.PolicyDocument, error) {
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, roleName string) ([]handlers_iam.PolicyDocument, error) {
require.Equal(t, "app-role", roleName)
return allowPolicy("ec2:RunInstances"), nil
})
@@ -2358,7 +2358,7 @@ func TestCheckPolicy_AssumedRole_AllowedByRolePolicy(t *testing.T) {
// action nor an explicit Deny, so the default deny applies.
func TestCheckPolicy_AssumedRole_ImplicitDeny(t *testing.T) {
cred := assumedRoleSessionCred("app", "arn:aws:iam::123456789012:role/app-role", "123456789012")
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]iamdomain.PolicyDocument, error) {
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return allowPolicy("ec2:DescribeInstances"), nil // does not cover RunInstances
})
@@ -2372,12 +2372,12 @@ func TestCheckPolicy_AssumedRole_ImplicitDeny(t *testing.T) {
// overrides any Allow.
func TestCheckPolicy_AssumedRole_ExplicitDeny(t *testing.T) {
cred := assumedRoleSessionCred("app", "arn:aws:iam::123456789012:role/app-role", "123456789012")
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]iamdomain.PolicyDocument, error) {
return []iamdomain.PolicyDocument{{
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{"ec2:*"}, Resource: iamdomain.StringOrArr{"*"}},
{Effect: "Deny", Action: iamdomain.StringOrArr{"ec2:RunInstances"}, Resource: iamdomain.StringOrArr{"*"}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"ec2:*"}, Resource: handlers_iam.StringOrArr{"*"}},
{Effect: "Deny", Action: handlers_iam.StringOrArr{"ec2:RunInstances"}, Resource: handlers_iam.StringOrArr{"*"}},
},
}}, nil
})
@@ -2393,7 +2393,7 @@ func TestCheckPolicy_AssumedRole_ExplicitDeny(t *testing.T) {
// permissions.
func TestCheckPolicy_AssumedRole_ZeroPolicyRole_DenyAll(t *testing.T) {
cred := assumedRoleSessionCred("app", "arn:aws:iam::123456789012:role/app-role", "123456789012")
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]iamdomain.PolicyDocument, error) {
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return nil, nil // role exists, zero attached policies
})
@@ -2411,7 +2411,7 @@ func TestCheckPolicy_AssumedRole_SessionNamedRoot_NoBypass(t *testing.T) {
cred := assumedRoleSessionCred("root",
"arn:aws:iam::"+awsidentifiers.GlobalAccountID+":role/sneaky-role", awsidentifiers.GlobalAccountID)
var roleResolved bool
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, roleName string) ([]iamdomain.PolicyDocument, error) {
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, roleName string) ([]handlers_iam.PolicyDocument, error) {
roleResolved = true
require.Equal(t, "sneaky-role", roleName)
return nil, nil // role grants nothing
@@ -2440,7 +2440,7 @@ func TestCheckPolicy_AssumedRole_EdgeARNs_Denied(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cred := assumedRoleSessionCred("app", tc.underlyingRoleARN, "123456789012")
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]iamdomain.PolicyDocument, error) {
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
t.Errorf("role resolver must not be consulted for an edge-case ARN (%s)", tc.name)
return nil, nil
})
@@ -2457,7 +2457,7 @@ func TestCheckPolicy_AssumedRole_EdgeARNs_Denied(t *testing.T) {
// resolve error fails closed — never an allow-on-error.
func TestCheckPolicy_AssumedRole_ResolveError_InternalError(t *testing.T) {
cred := assumedRoleSessionCred("app", "arn:aws:iam::123456789012:role/app-role", "123456789012")
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]iamdomain.PolicyDocument, error) {
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return nil, errors.New(awserrors.ErrorIAMNoSuchEntity) // role deleted after mint
})
@@ -2473,7 +2473,7 @@ func TestCheckPolicy_AssumedRole_TransientNATS_RetriesThenFails(t *testing.T) {
shortenPolicyResolveRetry(t)
cred := assumedRoleSessionCred("app", "arn:aws:iam::123456789012:role/app-role", "123456789012")
var calls int
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]iamdomain.PolicyDocument, error) {
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
calls++
return nil, nats.ErrNoResponders
})
@@ -2491,7 +2491,7 @@ func TestCheckPolicy_AssumedRole_TransientNATS_RetriesThenFails(t *testing.T) {
func TestCheckPolicy_AssumedRole_PassRole_ScopedToTargetARN_Allowed(t *testing.T) {
const targetRoleARN = "arn:aws:iam::123456789012:role/instance-role"
cred := assumedRoleSessionCred("app", "arn:aws:iam::123456789012:role/launcher-role", "123456789012")
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]iamdomain.PolicyDocument, error) {
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return allowPolicyResource("iam:PassRole", targetRoleARN), nil
})
@@ -2506,7 +2506,7 @@ func TestCheckPolicy_AssumedRole_PassRole_ScopedToTargetARN_Allowed(t *testing.T
func TestCheckPolicy_AssumedRole_PassRole_ScopedToOtherARN_Denied(t *testing.T) {
const targetRoleARN = "arn:aws:iam::123456789012:role/instance-role"
cred := assumedRoleSessionCred("app", "arn:aws:iam::123456789012:role/launcher-role", "123456789012")
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]iamdomain.PolicyDocument, error) {
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return allowPolicyResource("iam:PassRole", "arn:aws:iam::123456789012:role/some-other-role"), nil
})
@@ -2523,7 +2523,7 @@ func TestCheckPolicy_AssumedRole_PassRole_ScopedToOtherARN_Denied(t *testing.T)
func TestCheckPolicy_AssumedRole_PassRole_WildcardResource_Allowed(t *testing.T) {
const targetRoleARN = "arn:aws:iam::123456789012:role/instance-role"
cred := assumedRoleSessionCred("app", "arn:aws:iam::123456789012:role/launcher-role", "123456789012")
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]iamdomain.PolicyDocument, error) {
gw := newAssumedRoleEnforcementGateway(t, cred, func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return allowPolicyResource("iam:PassRole", "*"), nil
})
@@ -2737,12 +2737,12 @@ func TestSigV4Auth_ServedScopeSignatureMismatchStillSignatureDoesNotMatch(t *tes
// so before the fix ctxAction stayed empty and every such request shared the
// throttle bucket "<account>:unknown", which no per-action limit can match.
func TestSigV4Auth_ResolvesCtxActionForNonQueryServices(t *testing.T) {
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
mockSvc := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
+3 -3
View File
@@ -5,8 +5,8 @@ import (
"errors"
"fmt"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvstore"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go/jetstream"
)
@@ -63,7 +63,7 @@ func (s *CredentialStore) Resolve(ctx context.Context, accountID, vendor string)
entry, err := kv.Get(ctx, credentialKey(accountID, vendor))
switch {
case err == nil:
plaintext, err := iamdomain.DecryptSecret(string(entry.Value()), s.masterKey)
plaintext, err := handlers_iam.DecryptSecret(string(entry.Value()), s.masterKey)
if err != nil {
return "", false, fmt.Errorf("decrypt credential for %s/%s: %w", accountID, vendor, err)
}
@@ -84,7 +84,7 @@ func (s *CredentialStore) PutCredential(ctx context.Context, accountID, vendor,
if err != nil {
return err
}
ciphertext, err := iamdomain.EncryptSecret(key, s.masterKey)
ciphertext, err := handlers_iam.EncryptSecret(key, s.masterKey)
if err != nil {
return fmt.Errorf("encrypt credential for %s/%s: %w", accountID, vendor, err)
}
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"testing"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -106,11 +106,11 @@ func TestEncryptDecryptSecret_RoundTrip(t *testing.T) {
key := fixedMasterKey()
plaintext := "sk-ant-test-secret"
ciphertext, err := iamdomain.EncryptSecret(plaintext, key)
ciphertext, err := handlers_iam.EncryptSecret(plaintext, key)
require.NoError(t, err)
assert.NotEqual(t, plaintext, ciphertext)
decrypted, err := iamdomain.DecryptSecret(ciphertext, key)
decrypted, err := handlers_iam.DecryptSecret(ciphertext, key)
require.NoError(t, err)
assert.Equal(t, plaintext, decrypted)
}
@@ -14,7 +14,7 @@ import (
"testing"
"github.com/mulgadc/bluebottle/pkg/iampolicy"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -176,7 +176,7 @@ func TestValidatePolicyDocument_AcceptsExactlyTheSupportedConditions(t *testing.
for _, key := range allConditionKeys {
for op, value := range withIfExists(operatorValues) {
doc := conditionDocument(t, op, key, value)
_, err := iamdomain.ValidatePolicyDocument(doc)
_, err := handlers_iam.ValidatePolicyDocument(doc)
if iampolicy.SupportedCondition(op, key) {
assert.NoError(t, err,
@@ -204,7 +204,7 @@ func TestValidatePolicyDocument_AcceptsExactlyTheSubstitutableVariables(t *testi
resourceDocument(t, "arn:aws:s3:::reports/"+reference+"/*"),
conditionDocument(t, iampolicy.OpStringLike, iampolicy.KeyS3Prefix, reference+"/*"),
} {
_, err := iamdomain.ValidatePolicyDocument(doc)
_, err := handlers_iam.ValidatePolicyDocument(doc)
if unresolvable {
assert.Error(t, err,
"the write path accepts %s, which the evaluator cannot resolve: the policy would "+
@@ -220,7 +220,7 @@ func TestValidatePolicyDocument_AcceptsExactlyTheSubstitutableVariables(t *testi
// A malformed reference resolves nowhere and is rejected on every path.
func TestValidatePolicyDocument_RejectsUnterminatedVariable(t *testing.T) {
_, err := iamdomain.ValidatePolicyDocument(
_, err := handlers_iam.ValidatePolicyDocument(
resourceDocument(t, "arn:aws:s3:::reports/"+iampolicy.VariablePrefix+iampolicy.KeyUsername))
assert.ErrorContains(t, err, "unterminated policy variable reference")
}
+11 -11
View File
@@ -16,8 +16,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/mulgadc/bluebottle/pkg/iampolicy"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -279,15 +279,15 @@ func TestRequestConditionKeys_SourceIPWithoutAuthMiddleware(t *testing.T) {
// conditionedPolicyIAMService serves one statement carrying cond, so a test can
// drive a real condition through checkPolicyResources.
func conditionedPolicyIAMService(effect string, cond map[string]map[string]iamdomain.ConditionValue) *policyMockIAMService {
docs := []iamdomain.PolicyDocument{{
func conditionedPolicyIAMService(effect string, cond map[string]map[string]handlers_iam.ConditionValue) *policyMockIAMService {
docs := []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{"*"}, Resource: iamdomain.StringOrArr{"*"}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"*"}, Resource: handlers_iam.StringOrArr{"*"}},
{
Effect: effect,
Action: iamdomain.StringOrArr{"ec2:*"},
Resource: iamdomain.StringOrArr{"*"},
Action: handlers_iam.StringOrArr{"ec2:*"},
Resource: handlers_iam.StringOrArr{"*"},
Condition: cond,
},
},
@@ -296,8 +296,8 @@ func conditionedPolicyIAMService(effect string, cond map[string]map[string]iamdo
docs[0].Statement = docs[0].Statement[1:]
}
return &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getRolePoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
getRolePoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
}
}
@@ -307,7 +307,7 @@ func conditionedPolicyIAMService(effect string, cond map[string]map[string]iamdo
func TestCheckPolicyResources_ConditionalAllowHonoursSourceIP(t *testing.T) {
gw := &GatewayConfig{
DisableLogging: true,
IAMService: conditionedPolicyIAMService("Allow", map[string]map[string]iamdomain.ConditionValue{
IAMService: conditionedPolicyIAMService("Allow", map[string]map[string]handlers_iam.ConditionValue{
iampolicy.OpIPAddress: {iampolicy.KeySourceIP: {"10.0.0.0/8"}},
}),
}
@@ -346,7 +346,7 @@ func TestCheckPolicyResources_ConditionalAllowHonoursSourceIP(t *testing.T) {
func TestCheckPolicyResources_ConditionalDenyHonoursSourceIP(t *testing.T) {
gw := &GatewayConfig{
DisableLogging: true,
IAMService: conditionedPolicyIAMService("Deny", map[string]map[string]iamdomain.ConditionValue{
IAMService: conditionedPolicyIAMService("Deny", map[string]map[string]handlers_iam.ConditionValue{
iampolicy.OpIPAddress: {iampolicy.KeySourceIP: {"203.0.113.0/24"}},
}),
}
+13 -13
View File
@@ -18,9 +18,9 @@ import (
"github.com/go-chi/chi/v5"
"github.com/mulgadc/bluebottle/pkg/iampolicy"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
"github.com/mulgadc/spinifex/spinifex/foundation/telemetry"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/dispatch"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/envelope"
"github.com/stretchr/testify/assert"
@@ -135,9 +135,9 @@ func fakeNoResolverRegistration(c *dispatchCapture) dispatch.Registration {
// dispatchSeamApp wires SigV4AuthMiddleware -> traceActionEnricher -> Request
// on live test NATS, with regs registered through dispatch.NewBuilder, so a
// fake service's full pipeline mirrors ecrPipelineApp for a real one.
func dispatchSeamApp(t *testing.T, docs []iamdomain.PolicyDocument, regs ...dispatch.Registration) (*GatewayConfig, http.Handler) {
func dispatchSeamApp(t *testing.T, docs []handlers_iam.PolicyDocument, regs ...dispatch.Registration) (*GatewayConfig, http.Handler) {
t.Helper()
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
_, nc := testutil.StartTestNATS(t)
@@ -154,7 +154,7 @@ func dispatchSeamApp(t *testing.T, docs []iamdomain.PolicyDocument, regs ...disp
IAMService: &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -164,7 +164,7 @@ func dispatchSeamApp(t *testing.T, docs []iamdomain.PolicyDocument, regs ...disp
},
},
},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
},
}
@@ -257,10 +257,10 @@ func TestDispatchSeam_AuthorizeDeniesOnMismatchAllowsOnMatch(t *testing.T) {
t.Run("scoped to a different resource is denied", func(t *testing.T) {
capt := &dispatchCapture{authorizeResource: allowedResource}
scopedDocs := []iamdomain.PolicyDocument{{
scopedDocs := []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{"testsvc:FakeAction"}, Resource: iamdomain.StringOrArr{otherResource}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"testsvc:FakeAction"}, Resource: handlers_iam.StringOrArr{otherResource}},
},
}}
_, app := dispatchSeamApp(t, scopedDocs, fakeJSONRegistration(capt))
@@ -275,10 +275,10 @@ func TestDispatchSeam_AuthorizeDeniesOnMismatchAllowsOnMatch(t *testing.T) {
t.Run("matching policy allows", func(t *testing.T) {
capt := &dispatchCapture{authorizeResource: allowedResource}
matchingDocs := []iamdomain.PolicyDocument{{
matchingDocs := []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{"testsvc:FakeAction"}, Resource: iamdomain.StringOrArr{allowedResource}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"testsvc:FakeAction"}, Resource: handlers_iam.StringOrArr{allowedResource}},
},
}}
_, app := dispatchSeamApp(t, matchingDocs, fakeJSONRegistration(capt))
@@ -365,7 +365,7 @@ func TestDispatchSeam_UnregisteredUnknownServiceStillInvalidAction(t *testing.T)
b := dispatch.NewBuilder()
require.NoError(t, b.Register(fakeJSONRegistration(&dispatchCapture{})))
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
gw := &GatewayConfig{
DisableLogging: true,
@@ -373,7 +373,7 @@ func TestDispatchSeam_UnregisteredUnknownServiceStillInvalidAction(t *testing.T)
Services: b.Build(),
IAMService: &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {AccessKeyID: testAccessKey, SecretAccessKey: encryptedSecret, UserName: "root", Status: "Active"},
},
},
+14 -14
View File
@@ -10,9 +10,9 @@ import (
"strings"
"testing"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/query"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -24,23 +24,23 @@ const (
)
// scopedPolicyGateway serves one policy document to every principal.
func scopedPolicyGateway(statements ...iamdomain.Statement) *GatewayConfig {
docs := []iamdomain.PolicyDocument{{Version: "2012-10-17", Statement: statements}}
func scopedPolicyGateway(statements ...handlers_iam.Statement) *GatewayConfig {
docs := []handlers_iam.PolicyDocument{{Version: "2012-10-17", Statement: statements}}
return &GatewayConfig{
DisableLogging: true,
Region: authzRegion,
IAMService: &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getRolePoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
getRolePoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
},
}
}
func statement(effect, action, resource string) iamdomain.Statement {
return iamdomain.Statement{
func statement(effect, action, resource string) handlers_iam.Statement {
return handlers_iam.Statement{
Effect: effect,
Action: iamdomain.StringOrArr{action},
Resource: iamdomain.StringOrArr{resource},
Action: handlers_iam.StringOrArr{action},
Resource: handlers_iam.StringOrArr{resource},
}
}
@@ -250,7 +250,7 @@ func TestEC2Request_RejectsOversizedResourceList(t *testing.T) {
// per-service beads rest on: passing a real ARN where "*" was passed cannot
// withdraw access that works today.
func TestEC2Request_AccountWideAllowIsUnaffected(t *testing.T) {
for _, policy := range []iamdomain.Statement{
for _, policy := range []handlers_iam.Statement{
statement("Allow", "ec2:*", "*"),
statement("Allow", "*", "*"),
} {
@@ -318,11 +318,11 @@ type profileIAMService struct {
resolveErr error
}
func (s *profileIAMService) ResolveInstanceProfile(accountID, _ string) (*iamdomain.InstanceProfile, error) {
func (s *profileIAMService) ResolveInstanceProfile(accountID, _ string) (*handlers_iam.InstanceProfile, error) {
if s.resolveErr != nil {
return nil, s.resolveErr
}
return &iamdomain.InstanceProfile{AccountID: accountID, RoleName: "app"}, nil
return &handlers_iam.InstanceProfile{AccountID: accountID, RoleName: "app"}, nil
}
func (s *profileIAMService) CanonicalResourceARN(accountID string, _ arn.IAMResourceType, name string) (string, error) {
@@ -331,7 +331,7 @@ func (s *profileIAMService) CanonicalResourceARN(accountID string, _ arn.IAMReso
func passRoleDeniedGateway(t *testing.T, resolveErr error) *GatewayConfig {
t.Helper()
docs := []iamdomain.PolicyDocument{{Version: "2012-10-17", Statement: []iamdomain.Statement{
docs := []handlers_iam.PolicyDocument{{Version: "2012-10-17", Statement: []handlers_iam.Statement{
statement("Allow", "ec2:*", "*"),
statement("Deny", "iam:PassRole", "*"),
}}}
@@ -341,7 +341,7 @@ func passRoleDeniedGateway(t *testing.T, resolveErr error) *GatewayConfig {
NATSConn: startTestNATS(t),
IAMService: &profileIAMService{
policyMockIAMService: policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
},
resolveErr: resolveErr,
},
+14 -14
View File
@@ -7,8 +7,8 @@ import (
"net/http/httptest"
"testing"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
)
@@ -22,19 +22,19 @@ const (
// auth_test.go's allowPolicy/allowPolicyResource helpers because those are
// fixed to a single action string; ECR route classification can require
// multiple actions per operation (e.g. cross-repo mount).
func ecrAllowPolicy(actions, resources []string) iamdomain.PolicyDocument {
return iamdomain.PolicyDocument{
func ecrAllowPolicy(actions, resources []string) handlers_iam.PolicyDocument {
return handlers_iam.PolicyDocument{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: actions, Resource: resources},
},
}
}
func ecrDenyPolicy(actions, resources []string) iamdomain.PolicyDocument {
return iamdomain.PolicyDocument{
func ecrDenyPolicy(actions, resources []string) handlers_iam.PolicyDocument {
return handlers_iam.PolicyDocument{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
Statement: []handlers_iam.Statement{
{Effect: "Deny", Action: actions, Resource: resources},
},
}
@@ -43,7 +43,7 @@ func ecrDenyPolicy(actions, resources []string) iamdomain.PolicyDocument {
// ecrAuthzTestGateway builds a GatewayConfig wired for ecrOperationAuthorization
// tests: a non-nil ECRTokenVerifier (so the middleware is active) and an
// ecrMockIAMService seeded with ecrAuthzTestUser's policy documents.
func ecrAuthzTestGateway(t *testing.T, policies []iamdomain.PolicyDocument) (*GatewayConfig, *ecrMockIAMService) {
func ecrAuthzTestGateway(t *testing.T, policies []handlers_iam.PolicyDocument) (*GatewayConfig, *ecrMockIAMService) {
t.Helper()
_, verify := newECRAuth(t)
iamSvc := newECRMockIAMService()
@@ -76,7 +76,7 @@ func TestECROperationAuthorization_PullOnlyMatrix(t *testing.T) {
[]string{"ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability"},
[]string{repoARN},
)
gw, _ := ecrAuthzTestGateway(t, []iamdomain.PolicyDocument{pullOnly})
gw, _ := ecrAuthzTestGateway(t, []handlers_iam.PolicyDocument{pullOnly})
allowed := []struct{ method, path string }{
{http.MethodHead, "/v2/team/app/blobs/sha256:abc"},
@@ -126,7 +126,7 @@ func TestECROperationAuthorization_ReadOnlyMatrix(t *testing.T) {
},
[]string{repoARN, wildcardARN},
)
gw, _ := ecrAuthzTestGateway(t, []iamdomain.PolicyDocument{readOnly})
gw, _ := ecrAuthzTestGateway(t, []handlers_iam.PolicyDocument{readOnly})
allowed := []struct{ method, path string }{
{http.MethodGet, "/v2/_catalog"},
@@ -166,7 +166,7 @@ func TestECROperationAuthorization_ReadOnlyMatrix(t *testing.T) {
func TestECROperationAuthorization_RepoScopedAllowDoesNotCrossRepos(t *testing.T) {
repoAARN := "arn:aws:ecr:" + ecrTestRegion + ":" + ecrAuthzTestAccount + ":repository/team/a"
policy := ecrAllowPolicy([]string{"ecr:BatchGetImage"}, []string{repoAARN})
gw, _ := ecrAuthzTestGateway(t, []iamdomain.PolicyDocument{policy})
gw, _ := ecrAuthzTestGateway(t, []handlers_iam.PolicyDocument{policy})
t.Run("repo A allowed", func(t *testing.T) {
next, called := okHandler()
@@ -191,7 +191,7 @@ func TestECROperationAuthorization_ExplicitDenyWins(t *testing.T) {
repoARN := "arn:aws:ecr:" + ecrTestRegion + ":" + ecrAuthzTestAccount + ":repository/team/app"
allow := ecrAllowPolicy([]string{"ecr:BatchGetImage"}, []string{"arn:aws:ecr:" + ecrTestRegion + ":" + ecrAuthzTestAccount + ":repository/*"})
deny := ecrDenyPolicy([]string{"ecr:BatchGetImage"}, []string{repoARN})
gw, _ := ecrAuthzTestGateway(t, []iamdomain.PolicyDocument{allow, deny})
gw, _ := ecrAuthzTestGateway(t, []handlers_iam.PolicyDocument{allow, deny})
next, called := okHandler()
w := httptest.NewRecorder()
@@ -207,7 +207,7 @@ func TestECROperationAuthorization_CrossRepoMount(t *testing.T) {
sourceARN := "arn:aws:ecr:" + ecrTestRegion + ":" + ecrAuthzTestAccount + ":repository/team/base"
t.Run("destination write only: denied, never dispatches", func(t *testing.T) {
gw, _ := ecrAuthzTestGateway(t, []iamdomain.PolicyDocument{
gw, _ := ecrAuthzTestGateway(t, []handlers_iam.PolicyDocument{
ecrAllowPolicy([]string{"ecr:InitiateLayerUpload"}, []string{destARN}),
})
next, called := okHandler()
@@ -219,7 +219,7 @@ func TestECROperationAuthorization_CrossRepoMount(t *testing.T) {
})
t.Run("destination write + source read: allowed", func(t *testing.T) {
gw, _ := ecrAuthzTestGateway(t, []iamdomain.PolicyDocument{
gw, _ := ecrAuthzTestGateway(t, []handlers_iam.PolicyDocument{
ecrAllowPolicy([]string{"ecr:InitiateLayerUpload"}, []string{destARN}),
ecrAllowPolicy([]string{"ecr:BatchCheckLayerAvailability"}, []string{sourceARN}),
})
+13 -13
View File
@@ -21,9 +21,9 @@ import (
"github.com/mulgadc/spinifex/internal/testkit"
"github.com/mulgadc/spinifex/spinifex/domains/ecr"
awsapi "github.com/mulgadc/spinifex/spinifex/domains/ecr/awsapi"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
"github.com/mulgadc/spinifex/spinifex/foundation/telemetry"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/envelope"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -45,9 +45,9 @@ func decodeAuthToken(token string) (string, error) {
// ecrPipelineApp wires SigV4AuthMiddleware -> Request as SetupRoutes does, on
// live test NATS so Request dispatches. docs is every principal's policy; the
// region is testRegion because signTestRequestAs always signs for it.
func ecrPipelineApp(t *testing.T, docs []iamdomain.PolicyDocument) (*GatewayConfig, http.Handler) {
func ecrPipelineApp(t *testing.T, docs []handlers_iam.PolicyDocument) (*GatewayConfig, http.Handler) {
t.Helper()
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
_, nc := testutil.StartTestNATS(t)
@@ -59,7 +59,7 @@ func ecrPipelineApp(t *testing.T, docs []iamdomain.PolicyDocument) (*GatewayConf
IAMService: &policyMockIAMService{
mockIAMService: mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
testAccessKey: {
AccessKeyID: testAccessKey,
SecretAccessKey: encryptedSecret,
@@ -69,7 +69,7 @@ func ecrPipelineApp(t *testing.T, docs []iamdomain.PolicyDocument) (*GatewayConf
},
},
},
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
},
}
@@ -83,11 +83,11 @@ func ecrPipelineApp(t *testing.T, docs []iamdomain.PolicyDocument) (*GatewayConf
// allowAllECRDocs is the policy ecrPipelineApp uses when the test is not
// exercising authorization itself.
func allowAllECRDocs() []iamdomain.PolicyDocument {
return []iamdomain.PolicyDocument{{
func allowAllECRDocs() []handlers_iam.PolicyDocument {
return []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{"*"}, Resource: iamdomain.StringOrArr{"*"}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"*"}, Resource: handlers_iam.StringOrArr{"*"}},
},
}}
}
@@ -362,13 +362,13 @@ func (s *recordingRepoStore) DeleteRepo(context.Context, string, string) error {
// ECR dispatch runs, and denies, before a composed capability (here
// RepositoryActionService backing CreateRepository) is ever invoked.
func TestECRPipeline_AuthzGateBlocksComposedAction(t *testing.T) {
scopedDocs := []iamdomain.PolicyDocument{{
scopedDocs := []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
Statement: []handlers_iam.Statement{
{
Effect: "Allow",
Action: iamdomain.StringOrArr{"ecr:CreateRepository"},
Resource: iamdomain.StringOrArr{"arn:aws:ecr:" + testRegion + ":" + ecrTestAccount + ":repository/other"},
Action: handlers_iam.StringOrArr{"ecr:CreateRepository"},
Resource: handlers_iam.StringOrArr{"arn:aws:ecr:" + testRegion + ":" + ecrTestAccount + ":repository/other"},
},
},
}}
+3 -3
View File
@@ -9,8 +9,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
ecrauth "github.com/mulgadc/spinifex/spinifex/domains/ecr/auth"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
)
@@ -103,7 +103,7 @@ func (gw *GatewayConfig) resolveECRLongLivedPrincipal(claims *ecrauth.Claims) (p
if err != nil {
return principalContext{}, classifyIAMLookupErr(err, "access key")
}
if ak.Status != iamdomain.AccessKeyStatusActive {
if ak.Status != handlers_iam.AccessKeyStatusActive {
return principalContext{}, ecrInvalidPrincipal("access key %q is inactive", claims.AccessKeyID)
}
if ak.AccountID != claims.AccountID {
@@ -238,7 +238,7 @@ func (gw *GatewayConfig) requireActiveECRAccount(accountID string) error {
if err != nil {
return classifyIAMLookupErr(err, "account")
}
if account.Status != iamdomain.AccountStatusActive {
if account.Status != handlers_iam.AccountStatusActive {
return ecrInvalidPrincipal("account %q is not active", accountID)
}
return nil
+22 -22
View File
@@ -10,26 +10,26 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
ecrauth "github.com/mulgadc/spinifex/spinifex/domains/ecr/auth"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ecrMockIAMService implements iamdomain.IAMService for ECR principal
// ecrMockIAMService implements handlers_iam.IAMService for ECR principal
// rehydration tests. Only the methods resolveECRPrincipal calls are wired;
// any other method nil-panics on the embedded nil interface, surfacing an
// unexpected call rather than silently returning a zero value.
type ecrMockIAMService struct {
iamdomain.IAMService
handlers_iam.IAMService
accessKeys map[string]*iamdomain.AccessKey
accessKeys map[string]*handlers_iam.AccessKey
users map[string]*iam.User
roles map[string]*iam.Role
accounts map[string]*iamdomain.Account
userPolicies map[string][]iamdomain.PolicyDocument
rolePolicies map[string][]iamdomain.PolicyDocument
accounts map[string]*handlers_iam.Account
userPolicies map[string][]handlers_iam.PolicyDocument
rolePolicies map[string][]handlers_iam.PolicyDocument
// genericErr, when set, is returned by LookupAccessKey/GetUser/GetRole/
// GetAccount/GetUserPolicies/GetRolePolicies instead of the usual
@@ -40,16 +40,16 @@ type ecrMockIAMService struct {
func newECRMockIAMService() *ecrMockIAMService {
return &ecrMockIAMService{
accessKeys: map[string]*iamdomain.AccessKey{},
accessKeys: map[string]*handlers_iam.AccessKey{},
users: map[string]*iam.User{},
roles: map[string]*iam.Role{},
accounts: map[string]*iamdomain.Account{},
userPolicies: map[string][]iamdomain.PolicyDocument{},
rolePolicies: map[string][]iamdomain.PolicyDocument{},
accounts: map[string]*handlers_iam.Account{},
userPolicies: map[string][]handlers_iam.PolicyDocument{},
rolePolicies: map[string][]handlers_iam.PolicyDocument{},
}
}
func (m *ecrMockIAMService) LookupAccessKey(accessKeyID string) (*iamdomain.AccessKey, error) {
func (m *ecrMockIAMService) LookupAccessKey(accessKeyID string) (*handlers_iam.AccessKey, error) {
if m.genericErr != nil {
return nil, m.genericErr
}
@@ -82,7 +82,7 @@ func (m *ecrMockIAMService) GetRole(accountID string, input *iam.GetRoleInput) (
return &iam.GetRoleOutput{Role: r}, nil
}
func (m *ecrMockIAMService) GetAccount(accountID string) (*iamdomain.Account, error) {
func (m *ecrMockIAMService) GetAccount(accountID string) (*handlers_iam.Account, error) {
if m.genericErr != nil {
return nil, m.genericErr
}
@@ -93,14 +93,14 @@ func (m *ecrMockIAMService) GetAccount(accountID string) (*iamdomain.Account, er
return a, nil
}
func (m *ecrMockIAMService) GetUserPolicies(accountID, userName string) ([]iamdomain.PolicyDocument, error) {
func (m *ecrMockIAMService) GetUserPolicies(accountID, userName string) ([]handlers_iam.PolicyDocument, error) {
if m.genericErr != nil {
return nil, m.genericErr
}
return m.userPolicies[accountID+"|"+userName], nil
}
func (m *ecrMockIAMService) GetRolePolicies(accountID, roleName string) ([]iamdomain.PolicyDocument, error) {
func (m *ecrMockIAMService) GetRolePolicies(accountID, roleName string) ([]handlers_iam.PolicyDocument, error) {
if m.genericErr != nil {
return nil, m.genericErr
}
@@ -150,18 +150,18 @@ const (
)
func seedECRTestUser(iamSvc *ecrMockIAMService, accountID, userName, akid string) {
iamSvc.accessKeys[akid] = &iamdomain.AccessKey{
iamSvc.accessKeys[akid] = &handlers_iam.AccessKey{
AccessKeyID: akid,
UserName: userName,
AccountID: accountID,
Status: iamdomain.AccessKeyStatusActive,
Status: handlers_iam.AccessKeyStatusActive,
}
iamSvc.users[accountID+"|"+userName] = &iam.User{
UserName: aws.String(userName),
UserId: aws.String("AIDA" + strings.ToUpper(userName)),
Arn: aws.String("arn:aws:iam::" + accountID + ":user/" + userName),
}
iamSvc.accounts[accountID] = &iamdomain.Account{AccountID: accountID, Status: iamdomain.AccountStatusActive}
iamSvc.accounts[accountID] = &handlers_iam.Account{AccountID: accountID, Status: handlers_iam.AccountStatusActive}
}
func TestResolveECRPrincipal_LongLivedUser(t *testing.T) {
@@ -278,7 +278,7 @@ func TestResolveECRPrincipal_LongLivedUser_Rejections(t *testing.T) {
t.Run("suspended account", func(t *testing.T) {
iamSvc := newECRMockIAMService()
seedECRTestUser(iamSvc, ecrPrincipalTestAccount, "dev", ecrPrincipalTestAKID)
iamSvc.accounts[ecrPrincipalTestAccount].Status = iamdomain.AccountStatusSuspended
iamSvc.accounts[ecrPrincipalTestAccount].Status = handlers_iam.AccountStatusSuspended
gw := &GatewayConfig{IAMService: iamSvc}
_, err := gw.resolveECRPrincipal(validClaims())
require.Error(t, err)
@@ -290,7 +290,7 @@ func TestResolveECRPrincipal_LongLivedUser_Rejections(t *testing.T) {
t.Run("terminating account", func(t *testing.T) {
iamSvc := newECRMockIAMService()
seedECRTestUser(iamSvc, ecrPrincipalTestAccount, "dev", ecrPrincipalTestAKID)
iamSvc.accounts[ecrPrincipalTestAccount].Status = iamdomain.AccountStatusTerminating
iamSvc.accounts[ecrPrincipalTestAccount].Status = handlers_iam.AccountStatusTerminating
gw := &GatewayConfig{IAMService: iamSvc}
_, err := gw.resolveECRPrincipal(validClaims())
require.Error(t, err)
@@ -340,7 +340,7 @@ func seedECRSessionUser(iamSvc *ecrMockIAMService, stsSvc *ecrMockSTSService, ac
UserId: aws.String("AIDA" + strings.ToUpper(userName)),
Arn: aws.String("arn:aws:iam::" + accountID + ":user/" + userName),
}
iamSvc.accounts[accountID] = &iamdomain.Account{AccountID: accountID, Status: iamdomain.AccountStatusActive}
iamSvc.accounts[accountID] = &handlers_iam.Account{AccountID: accountID, Status: handlers_iam.AccountStatusActive}
stsSvc.sessions[asid] = &handlers_sts.SessionCredential{
AccessKeyID: asid,
AccountID: accountID,
@@ -464,7 +464,7 @@ func seedECRAssumedRole(iamSvc *ecrMockIAMService, stsSvc *ecrMockSTSService, ac
Arn: aws.String(roleARN),
RoleId: aws.String(roleID),
}
iamSvc.accounts[accountID] = &iamdomain.Account{AccountID: accountID, Status: iamdomain.AccountStatusActive}
iamSvc.accounts[accountID] = &handlers_iam.Account{AccountID: accountID, Status: handlers_iam.AccountStatusActive}
// AssumeRole session ARNs use the "assumed-role" resource type; the
// underlying role (resolved via GetRole) stays a role/ ARN, matching the
// production shape.
+10 -10
View File
@@ -9,7 +9,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecs"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -44,19 +44,19 @@ func registerTaskDefinitionBody(t *testing.T, taskRoleARN string) string {
// unconditionally (the action-level check gateway/ecs.go:39 always runs
// first) plus iam:PassRole scoped to passRoleResource, so tests can isolate
// the resource-level PassRole decision from the action-level one.
func allowRegisterTaskDefAndPassRole(passRoleResource string) []iamdomain.PolicyDocument {
return []iamdomain.PolicyDocument{{
func allowRegisterTaskDefAndPassRole(passRoleResource string) []handlers_iam.PolicyDocument {
return []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{"ecs:RegisterTaskDefinition"}, Resource: iamdomain.StringOrArr{"*"}},
{Effect: "Allow", Action: iamdomain.StringOrArr{"iam:PassRole"}, Resource: iamdomain.StringOrArr{passRoleResource}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"ecs:RegisterTaskDefinition"}, Resource: handlers_iam.StringOrArr{"*"}},
{Effect: "Allow", Action: handlers_iam.StringOrArr{"iam:PassRole"}, Resource: handlers_iam.StringOrArr{passRoleResource}},
},
}}
}
func TestECSRegisterTaskDefinition_DeniedWithoutPassRole(t *testing.T) {
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
// ecs:RegisterTaskDefinition is granted; iam:PassRole is scoped to a
// different role, so the target ARN is specifically denied.
return allowRegisterTaskDefAndPassRole("arn:aws:iam::123456789012:role/some-other-role"), nil
@@ -73,7 +73,7 @@ func TestECSRegisterTaskDefinition_DeniedWithoutPassRole(t *testing.T) {
func TestECSRegisterTaskDefinition_AllowedWithPassRole(t *testing.T) {
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return allowRegisterTaskDefAndPassRole(testECSPassRoleTargetARN), nil
},
}
@@ -91,7 +91,7 @@ func TestECSRegisterTaskDefinition_AllowedWithPassRole(t *testing.T) {
func TestECSRegisterTaskDefinition_NoRoleSkipsPassRole(t *testing.T) {
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
// No iam:PassRole grant at all — if the gateway still consulted
// PassRole for a roleless task definition this would deny it.
return allowPolicyResource("ecs:RegisterTaskDefinition", "*"), nil
@@ -110,7 +110,7 @@ func TestECSRegisterTaskDefinition_NoRoleSkipsPassRole(t *testing.T) {
// policy denials stay opaque while retaining the service's 403 envelope.
func TestECSRegisterTaskDefinition_DenialRendersAccessDenied403(t *testing.T) {
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return allowPolicyResource("ecs:RegisterTaskDefinition", "*"), nil
},
}
+5 -5
View File
@@ -5,9 +5,9 @@ import (
"net/http"
"testing"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -37,11 +37,11 @@ func (s *roleStoreIAMService) CanonicalResourceARN(account string, _ arn.IAMReso
// The foreign account holds a role of the same name, so only the gateway's
// account check stops its ARN from resolving.
func eksPassRoleService(statements ...iamdomain.Statement) *roleStoreIAMService {
docs := []iamdomain.PolicyDocument{{Version: "2012-10-17", Statement: statements}}
func eksPassRoleService(statements ...handlers_iam.Statement) *roleStoreIAMService {
docs := []handlers_iam.PolicyDocument{{Version: "2012-10-17", Statement: statements}}
return &roleStoreIAMService{
policyMockIAMService: policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
},
roles: map[string]map[string]string{
authzAccountID: {"node-role": eksNodeRoleARN},
@@ -50,7 +50,7 @@ func eksPassRoleService(statements ...iamdomain.Statement) *roleStoreIAMService
}
}
func eksPassRoleGateway(statements ...iamdomain.Statement) *GatewayConfig {
func eksPassRoleGateway(statements ...handlers_iam.Statement) *GatewayConfig {
return eksGatewayWithIAM(eksPassRoleService(statements...))
}
+6 -6
View File
@@ -33,7 +33,6 @@ import (
ec2instanceapi "github.com/mulgadc/spinifex/spinifex/domains/ec2/awsapi/instance"
ecrauth "github.com/mulgadc/spinifex/spinifex/domains/ecr/auth"
ecrregistry "github.com/mulgadc/spinifex/spinifex/domains/ecr/registry"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
ochrevector "github.com/mulgadc/spinifex/spinifex/domains/ochre/vector"
"github.com/mulgadc/spinifex/spinifex/domains/tagging"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
@@ -41,6 +40,7 @@ import (
"github.com/mulgadc/spinifex/spinifex/foundation/telemetry"
gateway_bedrock "github.com/mulgadc/spinifex/spinifex/gateway/bedrock"
gateway_sts "github.com/mulgadc/spinifex/spinifex/gateway/sts"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/dispatch"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/envelope"
@@ -147,7 +147,7 @@ type GatewayConfig struct {
// stopped answering, which would otherwise drop out of the answer entirely.
// Zero value keeps the pre-existing fan-out-only behaviour.
InstanceStatus ec2instanceapi.StatusSynthesis
IAMService iamdomain.IAMService
IAMService handlers_iam.IAMService
// BucketStore reaps a tenant's S3 buckets during account teardown. It
// signs with the config service credential, which predastore already
// trusts to reach any bucket, so this grants enumeration, not access.
@@ -824,7 +824,7 @@ func (gw *GatewayConfig) evaluatePrincipalPolicyResources(
// type. Identity-sensitive decisions (root bypass, resolver selection) are
// fully inside their branch so an assumed-role SessionName of "root" cannot
// reach the user-branch root short-circuit.
var resolve func() ([]iamdomain.PolicyDocument, error)
var resolve func() ([]handlers_iam.PolicyDocument, error)
var logIdentity string
switch principal.principalType {
@@ -833,7 +833,7 @@ func (gw *GatewayConfig) evaluatePrincipalPolicyResources(
// Global root bypass — user branch only.
return nil
}
resolve = func() ([]iamdomain.PolicyDocument, error) {
resolve = func() ([]handlers_iam.PolicyDocument, error) {
return gw.IAMService.GetUserPolicies(principal.accountID, principal.identity)
}
logIdentity = principal.identity
@@ -849,7 +849,7 @@ func (gw *GatewayConfig) evaluatePrincipalPolicyResources(
"err", perr)
return errors.New(awserrors.ErrorAccessDenied)
}
resolve = func() ([]iamdomain.PolicyDocument, error) {
resolve = func() ([]handlers_iam.PolicyDocument, error) {
return gw.IAMService.GetRolePolicies(principal.accountID, roleName)
}
logIdentity = principal.assumedRoleARN
@@ -859,7 +859,7 @@ func (gw *GatewayConfig) evaluatePrincipalPolicyResources(
}
// Resolve policies, retrying transient NATS errors. Fail-closed on non-transient errors.
var policies []iamdomain.PolicyDocument
var policies []handlers_iam.PolicyDocument
var err error
for attempt := range 3 {
policies, err = resolve()
+18 -18
View File
@@ -23,8 +23,8 @@ import (
"github.com/mulgadc/bluebottle/pkg/ratelimit"
clusterv1 "github.com/mulgadc/spinifex/contracts/cluster/v1"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/dispatch"
"github.com/mulgadc/spinifex/spinifex/ingress/aws/envelope"
"github.com/nats-io/nats.go"
@@ -1106,7 +1106,7 @@ func TestCheckPolicy_FailsClosed(t *testing.T) {
// resolved from storage. STS opts into detailed wording at its dispatcher.
func TestCheckPolicyResources_DenialIsOpaque(t *testing.T) {
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return nil, nil
},
}
@@ -1173,12 +1173,12 @@ func TestCheckPolicy_AssumedRoleSessionNamedRoot(t *testing.T) {
func TestCheckPolicy_NonRootAllowPolicy(t *testing.T) {
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
return []iamdomain.PolicyDocument{
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return []handlers_iam.PolicyDocument{
{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{"ec2:*"}, Resource: iamdomain.StringOrArr{"*"}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"ec2:*"}, Resource: handlers_iam.StringOrArr{"*"}},
},
},
}, nil
@@ -1197,12 +1197,12 @@ func TestCheckPolicy_NonRootAllowPolicy(t *testing.T) {
func TestCheckPolicy_NonRootDenyPolicy(t *testing.T) {
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
return []iamdomain.PolicyDocument{
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return []handlers_iam.PolicyDocument{
{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Deny", Action: iamdomain.StringOrArr{"ec2:*"}, Resource: iamdomain.StringOrArr{"*"}},
Statement: []handlers_iam.Statement{
{Effect: "Deny", Action: handlers_iam.StringOrArr{"ec2:*"}, Resource: handlers_iam.StringOrArr{"*"}},
},
},
}, nil
@@ -1222,7 +1222,7 @@ func TestCheckPolicy_NonRootDenyPolicy(t *testing.T) {
func TestCheckPolicy_NonRootNoPolicies(t *testing.T) {
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return nil, nil
},
}
@@ -1240,7 +1240,7 @@ func TestCheckPolicy_NonRootNoPolicies(t *testing.T) {
func TestCheckPolicy_GetUserPoliciesError(t *testing.T) {
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return nil, errors.New("db connection failed")
},
}
@@ -1281,7 +1281,7 @@ func TestCheckPolicy_NATSTransientRetriesAllAttempts(t *testing.T) {
shortenPolicyResolveRetry(t)
calls := 0
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
calls++
return nil, fmt.Errorf("get user: %w", nats.ErrNoResponders)
},
@@ -1303,16 +1303,16 @@ func TestCheckPolicy_NATSTransientRetriesThenSucceeds(t *testing.T) {
shortenPolicyResolveRetry(t)
calls := 0
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
calls++
if calls < 3 {
return nil, fmt.Errorf("get user: %w", nats.ErrNoResponders)
}
return []iamdomain.PolicyDocument{
return []handlers_iam.PolicyDocument{
{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{"ec2:*"}, Resource: iamdomain.StringOrArr{"*"}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"ec2:*"}, Resource: handlers_iam.StringOrArr{"*"}},
},
},
}, nil
@@ -1332,7 +1332,7 @@ func TestCheckPolicy_NATSTransientRetriesThenSucceeds(t *testing.T) {
func TestCheckPolicy_NonTransientErrorStillFails(t *testing.T) {
mock := &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) {
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return nil, errors.New("database corruption")
},
}
+3 -3
View File
@@ -3,18 +3,18 @@ package gateway_iam
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// GetAccountSummary forwards to the service. The input has no required fields,
// so this is a straight pass-through.
func GetAccountSummary(accountID string, input *iam.GetAccountSummaryInput, svc iamdomain.IAMService) (*iam.GetAccountSummaryOutput, error) {
func GetAccountSummary(accountID string, input *iam.GetAccountSummaryInput, svc handlers_iam.IAMService) (*iam.GetAccountSummaryOutput, error) {
return svc.GetAccountSummary(accountID, input)
}
// ListAccountAliases implements the IAM ListAccountAliases action. Aliases are paged here by
// Marker and MaxItems; a bad Marker or MaxItems is a ValidationError.
func ListAccountAliases(accountID string, input *iam.ListAccountAliasesInput, svc iamdomain.IAMService) (*iam.ListAccountAliasesOutput, error) {
func ListAccountAliases(accountID string, input *iam.ListAccountAliasesInput, svc handlers_iam.IAMService) (*iam.ListAccountAliasesOutput, error) {
p, err := newPager(input.Marker, input.MaxItems)
if err != nil {
return nil, err
+6 -6
View File
@@ -9,9 +9,9 @@ import (
"slices"
"strings"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
const anyResource = "*"
@@ -153,7 +153,7 @@ func ScopedActions() []string {
// ResourceARNs resolves the resource an IAM request authorizes against from
// the same parsed SDK input that dispatch receives.
func ResourceARNs(action, accountID string, input any, svc iamdomain.IAMService) ([]string, error) {
func ResourceARNs(action, accountID string, input any, svc handlers_iam.IAMService) ([]string, error) {
scope, ok := iamScopes[action]
if !ok {
slog.Error("IAM authz: action is served but absent from the scope table", "action", action)
@@ -169,7 +169,7 @@ func ResourceARNs(action, accountID string, input any, svc iamdomain.IAMService)
return []string{resource}, nil
}
func (s resourceScope) resolve(action, accountID string, input any, svc iamdomain.IAMService) (string, error) {
func (s resourceScope) resolve(action, accountID string, input any, svc handlers_iam.IAMService) (string, error) {
if s.source == sourceAccount {
return anyResource, nil
}
@@ -209,7 +209,7 @@ func (s resourceScope) resolve(action, accountID string, input any, svc iamdomai
}
return arn.FormatIAMResource(arn.IAMPolicy, accountID, pathAndName), nil
case sourceOIDCCreate:
hostPath, err := iamdomain.OIDCProviderHostPathFromURL(name)
hostPath, err := handlers_iam.OIDCProviderHostPathFromURL(name)
if hostPath == "" {
slog.Warn("IAM authz: issuer URL unresolvable, authorizing account-wide",
"action", action, "url", name, "account_id", accountID, "err", err)
@@ -217,7 +217,7 @@ func (s resourceScope) resolve(action, accountID string, input any, svc iamdomai
}
return arn.FormatIAMResource(arn.IAMOIDCProvider, accountID, hostPath), nil
case sourceOIDCARN:
hostPath, err := iamdomain.OIDCProviderHostPathFromARN(name)
hostPath, err := handlers_iam.OIDCProviderHostPathFromARN(name)
if hostPath == "" {
slog.Warn("IAM authz: provider ARN unresolvable, authorizing account-wide",
"action", action, "provider_arn", name, "account_id", accountID, "err", err)
@@ -267,7 +267,7 @@ type lookupTarget struct {
accountID string
}
func canonicalARN(t lookupTarget, kind arn.IAMResourceType, svc iamdomain.IAMService) (string, error) {
func canonicalARN(t lookupTarget, kind arn.IAMResourceType, svc handlers_iam.IAMService) (string, error) {
resource, err := svc.CanonicalResourceARN(t.accountID, kind, t.name)
return lookupResult(t, resource, err)
}
+5 -5
View File
@@ -6,10 +6,10 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
gateway_iam "github.com/mulgadc/spinifex/spinifex/gateway/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -17,11 +17,11 @@ import (
const testAccount = "123456789012"
type resolverService struct {
iamdomain.IAMService
handlers_iam.IAMService
arns map[arn.IAMResourceType]map[string]string
lookupErr error
key *iamdomain.AccessKey
key *handlers_iam.AccessKey
gotKind arn.IAMResourceType
gotName string
}
@@ -36,7 +36,7 @@ func (s *resolverService) CanonicalResourceARN(_ string, kind arn.IAMResourceTyp
return "", errors.New(awserrors.ErrorIAMNoSuchEntity)
}
func (s *resolverService) LookupAccessKey(_ string) (*iamdomain.AccessKey, error) {
func (s *resolverService) LookupAccessKey(_ string) (*handlers_iam.AccessKey, error) {
if s.lookupErr != nil {
return nil, s.lookupErr
}
@@ -113,7 +113,7 @@ func TestResourceARNs_UpdateAccessKeyUsesRecordedOwner(t *testing.T) {
arns: map[arn.IAMResourceType]map[string]string{
arn.IAMUser: {"owner": "arn:aws:iam::123456789012:user/team/owner"},
},
key: &iamdomain.AccessKey{AccountID: testAccount, UserName: "owner"},
key: &handlers_iam.AccessKey{AccountID: testAccount, UserName: "owner"},
}
input := &iam.UpdateAccessKeyInput{
AccessKeyId: aws.String("AKIAEXAMPLE"),
+15 -15
View File
@@ -5,13 +5,13 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// CreateGroup implements the IAM CreateGroup action, creating an IAM group. It returns
// MissingParameter when GroupName is absent, then calls the IAM service for accountID.
func CreateGroup(accountID string, input *iam.CreateGroupInput, svc iamdomain.IAMService) (*iam.CreateGroupOutput, error) {
func CreateGroup(accountID string, input *iam.CreateGroupInput, svc handlers_iam.IAMService) (*iam.CreateGroupOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -20,7 +20,7 @@ func CreateGroup(accountID string, input *iam.CreateGroupInput, svc iamdomain.IA
// GetGroup implements the IAM GetGroup action, returning the group and its members. It requires
// GroupName; the Users list is paged here by Marker and MaxItems.
func GetGroup(accountID string, input *iam.GetGroupInput, svc iamdomain.IAMService) (*iam.GetGroupOutput, error) {
func GetGroup(accountID string, input *iam.GetGroupInput, svc handlers_iam.IAMService) (*iam.GetGroupOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -39,7 +39,7 @@ func GetGroup(accountID string, input *iam.GetGroupInput, svc iamdomain.IAMServi
// ListGroups implements the IAM ListGroups action, listing the account's groups. It validates
// PathPrefix; results are paged here by Marker and MaxItems.
func ListGroups(accountID string, input *iam.ListGroupsInput, svc iamdomain.IAMService) (*iam.ListGroupsOutput, error) {
func ListGroups(accountID string, input *iam.ListGroupsInput, svc handlers_iam.IAMService) (*iam.ListGroupsOutput, error) {
if err := validatePathPrefix(input.PathPrefix, identityPathPrefix); err != nil {
return nil, err
}
@@ -58,7 +58,7 @@ func ListGroups(accountID string, input *iam.ListGroupsInput, svc iamdomain.IAMS
// DeleteGroup implements the IAM DeleteGroup action, deleting an IAM group. It returns
// MissingParameter when GroupName is absent, then calls the IAM service for accountID.
func DeleteGroup(accountID string, input *iam.DeleteGroupInput, svc iamdomain.IAMService) (*iam.DeleteGroupOutput, error) {
func DeleteGroup(accountID string, input *iam.DeleteGroupInput, svc handlers_iam.IAMService) (*iam.DeleteGroupOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -68,7 +68,7 @@ func DeleteGroup(accountID string, input *iam.DeleteGroupInput, svc iamdomain.IA
// AddUserToGroup implements the IAM AddUserToGroup action, adding a user to a group. It returns
// MissingParameter when GroupName or UserName is absent, then calls the IAM service for
// accountID.
func AddUserToGroup(accountID string, input *iam.AddUserToGroupInput, svc iamdomain.IAMService) (*iam.AddUserToGroupOutput, error) {
func AddUserToGroup(accountID string, input *iam.AddUserToGroupInput, svc handlers_iam.IAMService) (*iam.AddUserToGroupOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -81,7 +81,7 @@ func AddUserToGroup(accountID string, input *iam.AddUserToGroupInput, svc iamdom
// RemoveUserFromGroup implements the IAM RemoveUserFromGroup action, removing a user from a
// group. It returns MissingParameter when GroupName or UserName is absent, then calls the IAM
// service for accountID.
func RemoveUserFromGroup(accountID string, input *iam.RemoveUserFromGroupInput, svc iamdomain.IAMService) (*iam.RemoveUserFromGroupOutput, error) {
func RemoveUserFromGroup(accountID string, input *iam.RemoveUserFromGroupInput, svc handlers_iam.IAMService) (*iam.RemoveUserFromGroupOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -93,7 +93,7 @@ func RemoveUserFromGroup(accountID string, input *iam.RemoveUserFromGroupInput,
// ListGroupsForUser implements the IAM ListGroupsForUser action, listing the groups a user
// belongs to. It requires UserName; results are paged here by Marker and MaxItems.
func ListGroupsForUser(accountID string, input *iam.ListGroupsForUserInput, svc iamdomain.IAMService) (*iam.ListGroupsForUserOutput, error) {
func ListGroupsForUser(accountID string, input *iam.ListGroupsForUserInput, svc handlers_iam.IAMService) (*iam.ListGroupsForUserOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -113,7 +113,7 @@ func ListGroupsForUser(accountID string, input *iam.ListGroupsForUserInput, svc
// AttachGroupPolicy implements the IAM AttachGroupPolicy action, attaching a managed policy to a
// group. It returns MissingParameter when GroupName or PolicyArn is absent, then calls the IAM
// service for accountID.
func AttachGroupPolicy(accountID string, input *iam.AttachGroupPolicyInput, svc iamdomain.IAMService) (*iam.AttachGroupPolicyOutput, error) {
func AttachGroupPolicy(accountID string, input *iam.AttachGroupPolicyInput, svc handlers_iam.IAMService) (*iam.AttachGroupPolicyOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -126,7 +126,7 @@ func AttachGroupPolicy(accountID string, input *iam.AttachGroupPolicyInput, svc
// DetachGroupPolicy implements the IAM DetachGroupPolicy action, detaching a managed policy from
// a group. It returns MissingParameter when GroupName or PolicyArn is absent, then calls the IAM
// service for accountID.
func DetachGroupPolicy(accountID string, input *iam.DetachGroupPolicyInput, svc iamdomain.IAMService) (*iam.DetachGroupPolicyOutput, error) {
func DetachGroupPolicy(accountID string, input *iam.DetachGroupPolicyInput, svc handlers_iam.IAMService) (*iam.DetachGroupPolicyOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -139,7 +139,7 @@ func DetachGroupPolicy(accountID string, input *iam.DetachGroupPolicyInput, svc
// ListAttachedGroupPolicies implements the IAM ListAttachedGroupPolicies action, listing a
// group's managed policies. It requires GroupName and validates PathPrefix; results are paged
// here by Marker and MaxItems.
func ListAttachedGroupPolicies(accountID string, input *iam.ListAttachedGroupPoliciesInput, svc iamdomain.IAMService) (*iam.ListAttachedGroupPoliciesOutput, error) {
func ListAttachedGroupPolicies(accountID string, input *iam.ListAttachedGroupPoliciesInput, svc handlers_iam.IAMService) (*iam.ListAttachedGroupPoliciesOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -162,7 +162,7 @@ func ListAttachedGroupPolicies(accountID string, input *iam.ListAttachedGroupPol
// PutGroupPolicy implements the IAM PutGroupPolicy action, adding or replacing a group inline
// policy. It returns MissingParameter when GroupName, PolicyName or PolicyDocument is absent,
// then calls the IAM service for accountID.
func PutGroupPolicy(accountID string, input *iam.PutGroupPolicyInput, svc iamdomain.IAMService) (*iam.PutGroupPolicyOutput, error) {
func PutGroupPolicy(accountID string, input *iam.PutGroupPolicyInput, svc handlers_iam.IAMService) (*iam.PutGroupPolicyOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -178,7 +178,7 @@ func PutGroupPolicy(accountID string, input *iam.PutGroupPolicyInput, svc iamdom
// GetGroupPolicy implements the IAM GetGroupPolicy action, reading a group inline policy. It
// requires GroupName or PolicyName and returns the document percent-encoded as the IAM Query API
// does.
func GetGroupPolicy(accountID string, input *iam.GetGroupPolicyInput, svc iamdomain.IAMService) (*iam.GetGroupPolicyOutput, error) {
func GetGroupPolicy(accountID string, input *iam.GetGroupPolicyInput, svc handlers_iam.IAMService) (*iam.GetGroupPolicyOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -196,7 +196,7 @@ func GetGroupPolicy(accountID string, input *iam.GetGroupPolicyInput, svc iamdom
// DeleteGroupPolicy implements the IAM DeleteGroupPolicy action, deleting a group inline policy.
// It returns MissingParameter when GroupName or PolicyName is absent, then calls the IAM service
// for accountID.
func DeleteGroupPolicy(accountID string, input *iam.DeleteGroupPolicyInput, svc iamdomain.IAMService) (*iam.DeleteGroupPolicyOutput, error) {
func DeleteGroupPolicy(accountID string, input *iam.DeleteGroupPolicyInput, svc handlers_iam.IAMService) (*iam.DeleteGroupPolicyOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -208,7 +208,7 @@ func DeleteGroupPolicy(accountID string, input *iam.DeleteGroupPolicyInput, svc
// ListGroupPolicies implements the IAM ListGroupPolicies action, listing a group's inline policy
// names. It requires GroupName; results are paged here by Marker and MaxItems.
func ListGroupPolicies(accountID string, input *iam.ListGroupPoliciesInput, svc iamdomain.IAMService) (*iam.ListGroupPoliciesOutput, error) {
func ListGroupPolicies(accountID string, input *iam.ListGroupPoliciesInput, svc handlers_iam.IAMService) (*iam.ListGroupPoliciesOutput, error) {
if input.GroupName == nil || *input.GroupName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
+2 -2
View File
@@ -3,8 +3,8 @@ package gateway_iam
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
@@ -18,7 +18,7 @@ const testAccountID = "000000000000"
// promoted from the embedded interface and nil-panic if unexpectedly called.
// Individual tests can override a method by setting the matching function field.
type stubIAMService struct {
iamdomain.IAMService
handlers_iam.IAMService
getInstanceProfile func(string, *iam.GetInstanceProfileInput) (*iam.GetInstanceProfileOutput, error)
}
+11 -11
View File
@@ -6,8 +6,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// LiveAssociationCounter reports how many live EC2 instances reference the
@@ -17,7 +17,7 @@ type LiveAssociationCounter func(profileARN string) (int, error)
// CreateInstanceProfile implements the IAM CreateInstanceProfile action, creating an instance
// profile. It returns MissingParameter when InstanceProfileName is absent, then calls the IAM
// service for accountID.
func CreateInstanceProfile(accountID string, input *iam.CreateInstanceProfileInput, svc iamdomain.IAMService) (*iam.CreateInstanceProfileOutput, error) {
func CreateInstanceProfile(accountID string, input *iam.CreateInstanceProfileInput, svc handlers_iam.IAMService) (*iam.CreateInstanceProfileOutput, error) {
if input.InstanceProfileName == nil || *input.InstanceProfileName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -26,7 +26,7 @@ func CreateInstanceProfile(accountID string, input *iam.CreateInstanceProfileInp
// GetInstanceProfile implements the IAM GetInstanceProfile action. It requires
// InstanceProfileName and percent-encodes each role's trust policy as the IAM Query API does.
func GetInstanceProfile(accountID string, input *iam.GetInstanceProfileInput, svc iamdomain.IAMService) (*iam.GetInstanceProfileOutput, error) {
func GetInstanceProfile(accountID string, input *iam.GetInstanceProfileInput, svc handlers_iam.IAMService) (*iam.GetInstanceProfileOutput, error) {
if input.InstanceProfileName == nil || *input.InstanceProfileName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -40,7 +40,7 @@ func GetInstanceProfile(accountID string, input *iam.GetInstanceProfileInput, sv
// ListInstanceProfiles implements the IAM ListInstanceProfiles action. It validates PathPrefix,
// pages results here by Marker and MaxItems, and percent-encodes role trust policies.
func ListInstanceProfiles(accountID string, input *iam.ListInstanceProfilesInput, svc iamdomain.IAMService) (*iam.ListInstanceProfilesOutput, error) {
func ListInstanceProfiles(accountID string, input *iam.ListInstanceProfilesInput, svc handlers_iam.IAMService) (*iam.ListInstanceProfilesOutput, error) {
if err := validatePathPrefix(input.PathPrefix, identityPathPrefix); err != nil {
return nil, err
}
@@ -62,7 +62,7 @@ func ListInstanceProfiles(accountID string, input *iam.ListInstanceProfilesInput
// VM. countLive fans out to all daemons; a non-zero count returns DeleteConflict.
// The RoleName-attached guard runs inside svc.DeleteInstanceProfile afterward.
// countLive may be nil (e.g. in unit tests); the live-instance check is skipped.
func DeleteInstanceProfile(accountID string, input *iam.DeleteInstanceProfileInput, svc iamdomain.IAMService, countLive LiveAssociationCounter) (*iam.DeleteInstanceProfileOutput, error) {
func DeleteInstanceProfile(accountID string, input *iam.DeleteInstanceProfileInput, svc handlers_iam.IAMService, countLive LiveAssociationCounter) (*iam.DeleteInstanceProfileOutput, error) {
if input.InstanceProfileName == nil || *input.InstanceProfileName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -96,7 +96,7 @@ func DeleteInstanceProfile(accountID string, input *iam.DeleteInstanceProfileInp
// ListInstanceProfilesForRole implements the IAM action of the same name. It requires RoleName,
// pages results here by Marker and MaxItems, and percent-encodes role trust policies.
func ListInstanceProfilesForRole(accountID string, input *iam.ListInstanceProfilesForRoleInput, svc iamdomain.IAMService) (*iam.ListInstanceProfilesForRoleOutput, error) {
func ListInstanceProfilesForRole(accountID string, input *iam.ListInstanceProfilesForRoleInput, svc handlers_iam.IAMService) (*iam.ListInstanceProfilesForRoleOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -117,7 +117,7 @@ func ListInstanceProfilesForRole(accountID string, input *iam.ListInstanceProfil
// AddRoleToInstanceProfile implements the IAM AddRoleToInstanceProfile action, adding a role to
// an instance profile. It returns MissingParameter when InstanceProfileName or RoleName is
// absent, then calls the IAM service for accountID.
func AddRoleToInstanceProfile(accountID string, input *iam.AddRoleToInstanceProfileInput, svc iamdomain.IAMService) (*iam.AddRoleToInstanceProfileOutput, error) {
func AddRoleToInstanceProfile(accountID string, input *iam.AddRoleToInstanceProfileInput, svc handlers_iam.IAMService) (*iam.AddRoleToInstanceProfileOutput, error) {
if input.InstanceProfileName == nil || *input.InstanceProfileName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -130,7 +130,7 @@ func AddRoleToInstanceProfile(accountID string, input *iam.AddRoleToInstanceProf
// RemoveRoleFromInstanceProfile implements the IAM RemoveRoleFromInstanceProfile action, removing
// a role from an instance profile. It returns MissingParameter when InstanceProfileName or
// RoleName is absent, then calls the IAM service for accountID.
func RemoveRoleFromInstanceProfile(accountID string, input *iam.RemoveRoleFromInstanceProfileInput, svc iamdomain.IAMService) (*iam.RemoveRoleFromInstanceProfileOutput, error) {
func RemoveRoleFromInstanceProfile(accountID string, input *iam.RemoveRoleFromInstanceProfileInput, svc handlers_iam.IAMService) (*iam.RemoveRoleFromInstanceProfileOutput, error) {
if input.InstanceProfileName == nil || *input.InstanceProfileName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -142,7 +142,7 @@ func RemoveRoleFromInstanceProfile(accountID string, input *iam.RemoveRoleFromIn
// TagInstanceProfile implements the IAM TagInstanceProfile action. It returns MissingParameter
// when InstanceProfileName is absent or Tags is empty.
func TagInstanceProfile(accountID string, input *iam.TagInstanceProfileInput, svc iamdomain.IAMService) (*iam.TagInstanceProfileOutput, error) {
func TagInstanceProfile(accountID string, input *iam.TagInstanceProfileInput, svc handlers_iam.IAMService) (*iam.TagInstanceProfileOutput, error) {
if input.InstanceProfileName == nil || *input.InstanceProfileName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -154,7 +154,7 @@ func TagInstanceProfile(accountID string, input *iam.TagInstanceProfileInput, sv
// UntagInstanceProfile implements the IAM UntagInstanceProfile action. It returns
// MissingParameter when InstanceProfileName is absent or TagKeys is empty.
func UntagInstanceProfile(accountID string, input *iam.UntagInstanceProfileInput, svc iamdomain.IAMService) (*iam.UntagInstanceProfileOutput, error) {
func UntagInstanceProfile(accountID string, input *iam.UntagInstanceProfileInput, svc handlers_iam.IAMService) (*iam.UntagInstanceProfileOutput, error) {
if input.InstanceProfileName == nil || *input.InstanceProfileName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -166,7 +166,7 @@ func UntagInstanceProfile(accountID string, input *iam.UntagInstanceProfileInput
// ListInstanceProfileTags implements the IAM ListInstanceProfileTags action, listing an instance
// profile's tags. It requires InstanceProfileName; results are paged here by Marker and MaxItems.
func ListInstanceProfileTags(accountID string, input *iam.ListInstanceProfileTagsInput, svc iamdomain.IAMService) (*iam.ListInstanceProfileTagsOutput, error) {
func ListInstanceProfileTags(accountID string, input *iam.ListInstanceProfileTagsInput, svc handlers_iam.IAMService) (*iam.ListInstanceProfileTagsOutput, error) {
if input.InstanceProfileName == nil || *input.InstanceProfileName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
+5 -5
View File
@@ -5,13 +5,13 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// CreateAccessKey implements the IAM CreateAccessKey action, issuing a new key pair for UserName
// (MissingParameter when absent). The output is the only place the secret is returned.
func CreateAccessKey(accountID string, input *iam.CreateAccessKeyInput, svc iamdomain.IAMService) (*iam.CreateAccessKeyOutput, error) {
func CreateAccessKey(accountID string, input *iam.CreateAccessKeyInput, svc handlers_iam.IAMService) (*iam.CreateAccessKeyOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -20,7 +20,7 @@ func CreateAccessKey(accountID string, input *iam.CreateAccessKeyInput, svc iamd
// ListAccessKeys implements the IAM ListAccessKeys action, listing a user's access key metadata.
// It requires UserName; results are paged here by Marker and MaxItems.
func ListAccessKeys(accountID string, input *iam.ListAccessKeysInput, svc iamdomain.IAMService) (*iam.ListAccessKeysOutput, error) {
func ListAccessKeys(accountID string, input *iam.ListAccessKeysInput, svc handlers_iam.IAMService) (*iam.ListAccessKeysOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -40,7 +40,7 @@ func ListAccessKeys(accountID string, input *iam.ListAccessKeysInput, svc iamdom
// DeleteAccessKey implements the IAM DeleteAccessKey action, deleting a user's access key. It
// returns MissingParameter when UserName or AccessKeyId is absent, then calls the IAM service for
// accountID.
func DeleteAccessKey(accountID string, input *iam.DeleteAccessKeyInput, svc iamdomain.IAMService) (*iam.DeleteAccessKeyOutput, error) {
func DeleteAccessKey(accountID string, input *iam.DeleteAccessKeyInput, svc handlers_iam.IAMService) (*iam.DeleteAccessKeyOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -53,7 +53,7 @@ func DeleteAccessKey(accountID string, input *iam.DeleteAccessKeyInput, svc iamd
// UpdateAccessKey implements the IAM UpdateAccessKey action, setting an access key Active or
// Inactive. It returns MissingParameter when AccessKeyId or Status is absent, then calls the IAM
// service for accountID.
func UpdateAccessKey(accountID string, input *iam.UpdateAccessKeyInput, svc iamdomain.IAMService) (*iam.UpdateAccessKeyOutput, error) {
func UpdateAccessKey(accountID string, input *iam.UpdateAccessKeyInput, svc handlers_iam.IAMService) (*iam.UpdateAccessKeyOutput, error) {
if input.AccessKeyId == nil || *input.AccessKeyId == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
+8 -8
View File
@@ -5,14 +5,14 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// CreateOpenIDConnectProvider implements the IAM CreateOpenIDConnectProvider action, registering
// an OIDC identity provider. It returns MissingParameter when Url is absent, then calls the IAM
// service for accountID.
func CreateOpenIDConnectProvider(accountID string, input *iam.CreateOpenIDConnectProviderInput, svc iamdomain.IAMService) (*iam.CreateOpenIDConnectProviderOutput, error) {
func CreateOpenIDConnectProvider(accountID string, input *iam.CreateOpenIDConnectProviderInput, svc handlers_iam.IAMService) (*iam.CreateOpenIDConnectProviderOutput, error) {
if input.Url == nil || *input.Url == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -22,7 +22,7 @@ func CreateOpenIDConnectProvider(accountID string, input *iam.CreateOpenIDConnec
// GetOpenIDConnectProvider implements the IAM GetOpenIDConnectProvider action, describing an OIDC
// provider. It returns MissingParameter when OpenIDConnectProviderArn is absent, then calls the
// IAM service for accountID.
func GetOpenIDConnectProvider(accountID string, input *iam.GetOpenIDConnectProviderInput, svc iamdomain.IAMService) (*iam.GetOpenIDConnectProviderOutput, error) {
func GetOpenIDConnectProvider(accountID string, input *iam.GetOpenIDConnectProviderInput, svc handlers_iam.IAMService) (*iam.GetOpenIDConnectProviderOutput, error) {
if input.OpenIDConnectProviderArn == nil || *input.OpenIDConnectProviderArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -31,14 +31,14 @@ func GetOpenIDConnectProvider(accountID string, input *iam.GetOpenIDConnectProvi
// ListOpenIDConnectProviders implements the IAM ListOpenIDConnectProviders action. The action is
// unpaginated in AWS, so the service result is returned as is.
func ListOpenIDConnectProviders(accountID string, input *iam.ListOpenIDConnectProvidersInput, svc iamdomain.IAMService) (*iam.ListOpenIDConnectProvidersOutput, error) {
func ListOpenIDConnectProviders(accountID string, input *iam.ListOpenIDConnectProvidersInput, svc handlers_iam.IAMService) (*iam.ListOpenIDConnectProvidersOutput, error) {
return svc.ListOpenIDConnectProviders(accountID, input)
}
// DeleteOpenIDConnectProvider implements the IAM DeleteOpenIDConnectProvider action, deleting an
// OIDC provider. It returns MissingParameter when OpenIDConnectProviderArn is absent, then calls
// the IAM service for accountID.
func DeleteOpenIDConnectProvider(accountID string, input *iam.DeleteOpenIDConnectProviderInput, svc iamdomain.IAMService) (*iam.DeleteOpenIDConnectProviderOutput, error) {
func DeleteOpenIDConnectProvider(accountID string, input *iam.DeleteOpenIDConnectProviderInput, svc handlers_iam.IAMService) (*iam.DeleteOpenIDConnectProviderOutput, error) {
if input.OpenIDConnectProviderArn == nil || *input.OpenIDConnectProviderArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -47,7 +47,7 @@ func DeleteOpenIDConnectProvider(accountID string, input *iam.DeleteOpenIDConnec
// TagOpenIDConnectProvider implements the IAM TagOpenIDConnectProvider action. It returns
// MissingParameter when OpenIDConnectProviderArn is absent or Tags is empty.
func TagOpenIDConnectProvider(accountID string, input *iam.TagOpenIDConnectProviderInput, svc iamdomain.IAMService) (*iam.TagOpenIDConnectProviderOutput, error) {
func TagOpenIDConnectProvider(accountID string, input *iam.TagOpenIDConnectProviderInput, svc handlers_iam.IAMService) (*iam.TagOpenIDConnectProviderOutput, error) {
if input.OpenIDConnectProviderArn == nil || *input.OpenIDConnectProviderArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -59,7 +59,7 @@ func TagOpenIDConnectProvider(accountID string, input *iam.TagOpenIDConnectProvi
// UntagOpenIDConnectProvider implements the IAM UntagOpenIDConnectProvider action. It returns
// MissingParameter when OpenIDConnectProviderArn is absent or TagKeys is empty.
func UntagOpenIDConnectProvider(accountID string, input *iam.UntagOpenIDConnectProviderInput, svc iamdomain.IAMService) (*iam.UntagOpenIDConnectProviderOutput, error) {
func UntagOpenIDConnectProvider(accountID string, input *iam.UntagOpenIDConnectProviderInput, svc handlers_iam.IAMService) (*iam.UntagOpenIDConnectProviderOutput, error) {
if input.OpenIDConnectProviderArn == nil || *input.OpenIDConnectProviderArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -72,7 +72,7 @@ func UntagOpenIDConnectProvider(accountID string, input *iam.UntagOpenIDConnectP
// ListOpenIDConnectProviderTags implements the IAM ListOpenIDConnectProviderTags action, listing
// an OIDC provider's tags. It requires OpenIDConnectProviderArn; results are paged here by Marker
// and MaxItems.
func ListOpenIDConnectProviderTags(accountID string, input *iam.ListOpenIDConnectProviderTagsInput, svc iamdomain.IAMService) (*iam.ListOpenIDConnectProviderTagsOutput, error) {
func ListOpenIDConnectProviderTags(accountID string, input *iam.ListOpenIDConnectProviderTagsInput, svc handlers_iam.IAMService) (*iam.ListOpenIDConnectProviderTagsOutput, error) {
if input.OpenIDConnectProviderArn == nil || *input.OpenIDConnectProviderArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
gateway_iam "github.com/mulgadc/spinifex/spinifex/gateway/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -18,7 +18,7 @@ const testAccountID = "000000000000"
// listStub serves fixed lists in whatever order the test gives, as the KV
// bucket does, and counts service calls.
type listStub struct {
iamdomain.IAMService
handlers_iam.IAMService
users []string
versions []string
@@ -134,7 +134,7 @@ func TestListUsers_RejectsBadPagingBeforeListing(t *testing.T) {
// listAliasesStub counts calls so a rejected page proves the service never ran.
type listAliasesStub struct {
iamdomain.IAMService
handlers_iam.IAMService
calls int
}
+17 -17
View File
@@ -6,8 +6,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// The model's arnType length bounds.
@@ -19,7 +19,7 @@ const (
// CreatePolicy implements the IAM CreatePolicy action, creating a customer managed policy. It
// returns MissingParameter when PolicyName or PolicyDocument is absent, then calls the IAM
// service for accountID.
func CreatePolicy(accountID string, input *iam.CreatePolicyInput, svc iamdomain.IAMService) (*iam.CreatePolicyOutput, error) {
func CreatePolicy(accountID string, input *iam.CreatePolicyInput, svc handlers_iam.IAMService) (*iam.CreatePolicyOutput, error) {
if input.PolicyName == nil || *input.PolicyName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -31,7 +31,7 @@ func CreatePolicy(accountID string, input *iam.CreatePolicyInput, svc iamdomain.
// GetPolicy implements the IAM GetPolicy action, describing a managed policy. It returns
// MissingParameter when PolicyArn is absent, then calls the IAM service for accountID.
func GetPolicy(accountID string, input *iam.GetPolicyInput, svc iamdomain.IAMService) (*iam.GetPolicyOutput, error) {
func GetPolicy(accountID string, input *iam.GetPolicyInput, svc handlers_iam.IAMService) (*iam.GetPolicyOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -41,7 +41,7 @@ func GetPolicy(accountID string, input *iam.GetPolicyInput, svc iamdomain.IAMSer
// GetPolicyVersion implements the IAM GetPolicyVersion action, reading one version of a managed
// policy. It requires PolicyArn or VersionId and returns the document percent-encoded as the IAM
// Query API does.
func GetPolicyVersion(accountID string, input *iam.GetPolicyVersionInput, svc iamdomain.IAMService) (*iam.GetPolicyVersionOutput, error) {
func GetPolicyVersion(accountID string, input *iam.GetPolicyVersionInput, svc handlers_iam.IAMService) (*iam.GetPolicyVersionOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -60,7 +60,7 @@ func GetPolicyVersion(accountID string, input *iam.GetPolicyVersionInput, svc ia
// ListPolicyVersions implements the IAM ListPolicyVersions action. It requires PolicyArn;
// versions are paged here newest first by Marker and MaxItems.
func ListPolicyVersions(accountID string, input *iam.ListPolicyVersionsInput, svc iamdomain.IAMService) (*iam.ListPolicyVersionsOutput, error) {
func ListPolicyVersions(accountID string, input *iam.ListPolicyVersionsInput, svc handlers_iam.IAMService) (*iam.ListPolicyVersionsOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -80,7 +80,7 @@ func ListPolicyVersions(accountID string, input *iam.ListPolicyVersionsInput, sv
// CreatePolicyVersion implements the IAM CreatePolicyVersion action, adding a new version to a
// managed policy. It returns MissingParameter when PolicyArn or PolicyDocument is absent, then
// calls the IAM service for accountID.
func CreatePolicyVersion(accountID string, input *iam.CreatePolicyVersionInput, svc iamdomain.IAMService) (*iam.CreatePolicyVersionOutput, error) {
func CreatePolicyVersion(accountID string, input *iam.CreatePolicyVersionInput, svc handlers_iam.IAMService) (*iam.CreatePolicyVersionOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -93,7 +93,7 @@ func CreatePolicyVersion(accountID string, input *iam.CreatePolicyVersionInput,
// SetDefaultPolicyVersion implements the IAM SetDefaultPolicyVersion action, choosing a managed
// policy's default version. It returns MissingParameter when PolicyArn or VersionId is absent,
// then calls the IAM service for accountID.
func SetDefaultPolicyVersion(accountID string, input *iam.SetDefaultPolicyVersionInput, svc iamdomain.IAMService) (*iam.SetDefaultPolicyVersionOutput, error) {
func SetDefaultPolicyVersion(accountID string, input *iam.SetDefaultPolicyVersionInput, svc handlers_iam.IAMService) (*iam.SetDefaultPolicyVersionOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -106,7 +106,7 @@ func SetDefaultPolicyVersion(accountID string, input *iam.SetDefaultPolicyVersio
// DeletePolicyVersion implements the IAM DeletePolicyVersion action, deleting a non-default
// policy version. It returns MissingParameter when PolicyArn or VersionId is absent, then calls
// the IAM service for accountID.
func DeletePolicyVersion(accountID string, input *iam.DeletePolicyVersionInput, svc iamdomain.IAMService) (*iam.DeletePolicyVersionOutput, error) {
func DeletePolicyVersion(accountID string, input *iam.DeletePolicyVersionInput, svc handlers_iam.IAMService) (*iam.DeletePolicyVersionOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -118,7 +118,7 @@ func DeletePolicyVersion(accountID string, input *iam.DeletePolicyVersionInput,
// ListPolicies implements the IAM ListPolicies action, listing managed policies. It validates
// PathPrefix; results are paged here by Marker and MaxItems.
func ListPolicies(accountID string, input *iam.ListPoliciesInput, svc iamdomain.IAMService) (*iam.ListPoliciesOutput, error) {
func ListPolicies(accountID string, input *iam.ListPoliciesInput, svc handlers_iam.IAMService) (*iam.ListPoliciesOutput, error) {
if err := validatePathPrefix(input.PathPrefix, policyPathPrefix); err != nil {
return nil, err
}
@@ -137,7 +137,7 @@ func ListPolicies(accountID string, input *iam.ListPoliciesInput, svc iamdomain.
// DeletePolicy implements the IAM DeletePolicy action, deleting a customer managed policy. It
// returns MissingParameter when PolicyArn is absent, then calls the IAM service for accountID.
func DeletePolicy(accountID string, input *iam.DeletePolicyInput, svc iamdomain.IAMService) (*iam.DeletePolicyOutput, error) {
func DeletePolicy(accountID string, input *iam.DeletePolicyInput, svc handlers_iam.IAMService) (*iam.DeletePolicyOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -147,7 +147,7 @@ func DeletePolicy(accountID string, input *iam.DeletePolicyInput, svc iamdomain.
// ListEntitiesForPolicy implements the IAM ListEntitiesForPolicy action. It checks PolicyArn's
// length and PathPrefix as the IAM model does, then pages groups, roles and users as one
// sequence.
func ListEntitiesForPolicy(accountID string, input *iam.ListEntitiesForPolicyInput, svc iamdomain.IAMService) (*iam.ListEntitiesForPolicyOutput, error) {
func ListEntitiesForPolicy(accountID string, input *iam.ListEntitiesForPolicyInput, svc handlers_iam.IAMService) (*iam.ListEntitiesForPolicyOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -213,7 +213,7 @@ func pageEntitiesForPolicy(p pager, out *iam.ListEntitiesForPolicyOutput) {
// AttachUserPolicy implements the IAM AttachUserPolicy action, attaching a managed policy to a
// user. It returns MissingParameter when UserName or PolicyArn is absent, then calls the IAM
// service for accountID.
func AttachUserPolicy(accountID string, input *iam.AttachUserPolicyInput, svc iamdomain.IAMService) (*iam.AttachUserPolicyOutput, error) {
func AttachUserPolicy(accountID string, input *iam.AttachUserPolicyInput, svc handlers_iam.IAMService) (*iam.AttachUserPolicyOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -226,7 +226,7 @@ func AttachUserPolicy(accountID string, input *iam.AttachUserPolicyInput, svc ia
// DetachUserPolicy implements the IAM DetachUserPolicy action, detaching a managed policy from a
// user. It returns MissingParameter when UserName or PolicyArn is absent, then calls the IAM
// service for accountID.
func DetachUserPolicy(accountID string, input *iam.DetachUserPolicyInput, svc iamdomain.IAMService) (*iam.DetachUserPolicyOutput, error) {
func DetachUserPolicy(accountID string, input *iam.DetachUserPolicyInput, svc handlers_iam.IAMService) (*iam.DetachUserPolicyOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -239,7 +239,7 @@ func DetachUserPolicy(accountID string, input *iam.DetachUserPolicyInput, svc ia
// ListAttachedUserPolicies implements the IAM ListAttachedUserPolicies action, listing a user's
// managed policies. It requires UserName and validates PathPrefix; results are paged here by
// Marker and MaxItems.
func ListAttachedUserPolicies(accountID string, input *iam.ListAttachedUserPoliciesInput, svc iamdomain.IAMService) (*iam.ListAttachedUserPoliciesOutput, error) {
func ListAttachedUserPolicies(accountID string, input *iam.ListAttachedUserPoliciesInput, svc handlers_iam.IAMService) (*iam.ListAttachedUserPoliciesOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -261,7 +261,7 @@ func ListAttachedUserPolicies(accountID string, input *iam.ListAttachedUserPolic
// TagPolicy implements the IAM TagPolicy action. It returns MissingParameter when PolicyArn is
// absent or Tags is empty.
func TagPolicy(accountID string, input *iam.TagPolicyInput, svc iamdomain.IAMService) (*iam.TagPolicyOutput, error) {
func TagPolicy(accountID string, input *iam.TagPolicyInput, svc handlers_iam.IAMService) (*iam.TagPolicyOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -273,7 +273,7 @@ func TagPolicy(accountID string, input *iam.TagPolicyInput, svc iamdomain.IAMSer
// UntagPolicy implements the IAM UntagPolicy action. It returns MissingParameter when PolicyArn
// is absent or TagKeys is empty.
func UntagPolicy(accountID string, input *iam.UntagPolicyInput, svc iamdomain.IAMService) (*iam.UntagPolicyOutput, error) {
func UntagPolicy(accountID string, input *iam.UntagPolicyInput, svc handlers_iam.IAMService) (*iam.UntagPolicyOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -285,7 +285,7 @@ func UntagPolicy(accountID string, input *iam.UntagPolicyInput, svc iamdomain.IA
// ListPolicyTags implements the IAM ListPolicyTags action, listing a managed policy's tags. It
// requires PolicyArn; results are paged here by Marker and MaxItems.
func ListPolicyTags(accountID string, input *iam.ListPolicyTagsInput, svc iamdomain.IAMService) (*iam.ListPolicyTagsOutput, error) {
func ListPolicyTags(accountID string, input *iam.ListPolicyTagsInput, svc handlers_iam.IAMService) (*iam.ListPolicyTagsOutput, error) {
if input.PolicyArn == nil || *input.PolicyArn == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
gateway_iam "github.com/mulgadc/spinifex/spinifex/gateway/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -22,7 +22,7 @@ const (
// rawDocIAMService answers every document-bearing read with rawDoc, as the
// service layer stores and returns it.
type rawDocIAMService struct {
iamdomain.IAMService
handlers_iam.IAMService
}
func rawDocRole() *iam.Role {
+18 -18
View File
@@ -5,14 +5,14 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// CreateRole implements the IAM CreateRole action. It requires RoleName and
// AssumeRolePolicyDocument and returns the trust policy percent-encoded as the IAM Query API
// does.
func CreateRole(accountID string, input *iam.CreateRoleInput, svc iamdomain.IAMService) (*iam.CreateRoleOutput, error) {
func CreateRole(accountID string, input *iam.CreateRoleInput, svc handlers_iam.IAMService) (*iam.CreateRoleOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -29,7 +29,7 @@ func CreateRole(accountID string, input *iam.CreateRoleInput, svc iamdomain.IAMS
// GetRole implements the IAM GetRole action. It requires RoleName and returns the trust policy
// percent-encoded as the IAM Query API does.
func GetRole(accountID string, input *iam.GetRoleInput, svc iamdomain.IAMService) (*iam.GetRoleOutput, error) {
func GetRole(accountID string, input *iam.GetRoleInput, svc handlers_iam.IAMService) (*iam.GetRoleOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -43,7 +43,7 @@ func GetRole(accountID string, input *iam.GetRoleInput, svc iamdomain.IAMService
// ListRoles implements the IAM ListRoles action. It validates PathPrefix, pages results here by
// Marker and MaxItems, and percent-encodes each trust policy.
func ListRoles(accountID string, input *iam.ListRolesInput, svc iamdomain.IAMService) (*iam.ListRolesOutput, error) {
func ListRoles(accountID string, input *iam.ListRolesInput, svc handlers_iam.IAMService) (*iam.ListRolesOutput, error) {
if err := validatePathPrefix(input.PathPrefix, identityPathPrefix); err != nil {
return nil, err
}
@@ -63,7 +63,7 @@ func ListRoles(accountID string, input *iam.ListRolesInput, svc iamdomain.IAMSer
// DeleteRole implements the IAM DeleteRole action, deleting an IAM role. It returns
// MissingParameter when RoleName is absent, then calls the IAM service for accountID.
func DeleteRole(accountID string, input *iam.DeleteRoleInput, svc iamdomain.IAMService) (*iam.DeleteRoleOutput, error) {
func DeleteRole(accountID string, input *iam.DeleteRoleInput, svc handlers_iam.IAMService) (*iam.DeleteRoleOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -73,7 +73,7 @@ func DeleteRole(accountID string, input *iam.DeleteRoleInput, svc iamdomain.IAMS
// UpdateRole implements the IAM UpdateRole action, updating a role's description or maximum
// session duration. It returns MissingParameter when RoleName is absent, then calls the IAM
// service for accountID.
func UpdateRole(accountID string, input *iam.UpdateRoleInput, svc iamdomain.IAMService) (*iam.UpdateRoleOutput, error) {
func UpdateRole(accountID string, input *iam.UpdateRoleInput, svc handlers_iam.IAMService) (*iam.UpdateRoleOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -82,7 +82,7 @@ func UpdateRole(accountID string, input *iam.UpdateRoleInput, svc iamdomain.IAMS
// UpdateRoleDescription implements the IAM UpdateRoleDescription action. RoleName and Description
// must be present (an empty Description clears it); the trust policy is percent- encoded.
func UpdateRoleDescription(accountID string, input *iam.UpdateRoleDescriptionInput, svc iamdomain.IAMService) (*iam.UpdateRoleDescriptionOutput, error) {
func UpdateRoleDescription(accountID string, input *iam.UpdateRoleDescriptionInput, svc handlers_iam.IAMService) (*iam.UpdateRoleDescriptionOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -101,7 +101,7 @@ func UpdateRoleDescription(accountID string, input *iam.UpdateRoleDescriptionInp
// UpdateAssumeRolePolicy implements the IAM UpdateAssumeRolePolicy action, replacing a role's
// trust policy. It returns MissingParameter when RoleName or PolicyDocument is absent, then calls
// the IAM service for accountID.
func UpdateAssumeRolePolicy(accountID string, input *iam.UpdateAssumeRolePolicyInput, svc iamdomain.IAMService) (*iam.UpdateAssumeRolePolicyOutput, error) {
func UpdateAssumeRolePolicy(accountID string, input *iam.UpdateAssumeRolePolicyInput, svc handlers_iam.IAMService) (*iam.UpdateAssumeRolePolicyOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -114,7 +114,7 @@ func UpdateAssumeRolePolicy(accountID string, input *iam.UpdateAssumeRolePolicyI
// AttachRolePolicy implements the IAM AttachRolePolicy action, attaching a managed policy to a
// role. It returns MissingParameter when RoleName or PolicyArn is absent, then calls the IAM
// service for accountID.
func AttachRolePolicy(accountID string, input *iam.AttachRolePolicyInput, svc iamdomain.IAMService) (*iam.AttachRolePolicyOutput, error) {
func AttachRolePolicy(accountID string, input *iam.AttachRolePolicyInput, svc handlers_iam.IAMService) (*iam.AttachRolePolicyOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -127,7 +127,7 @@ func AttachRolePolicy(accountID string, input *iam.AttachRolePolicyInput, svc ia
// DetachRolePolicy implements the IAM DetachRolePolicy action, detaching a managed policy from a
// role. It returns MissingParameter when RoleName or PolicyArn is absent, then calls the IAM
// service for accountID.
func DetachRolePolicy(accountID string, input *iam.DetachRolePolicyInput, svc iamdomain.IAMService) (*iam.DetachRolePolicyOutput, error) {
func DetachRolePolicy(accountID string, input *iam.DetachRolePolicyInput, svc handlers_iam.IAMService) (*iam.DetachRolePolicyOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -140,7 +140,7 @@ func DetachRolePolicy(accountID string, input *iam.DetachRolePolicyInput, svc ia
// ListAttachedRolePolicies implements the IAM ListAttachedRolePolicies action, listing a role's
// managed policies. It requires RoleName and validates PathPrefix; results are paged here by
// Marker and MaxItems.
func ListAttachedRolePolicies(accountID string, input *iam.ListAttachedRolePoliciesInput, svc iamdomain.IAMService) (*iam.ListAttachedRolePoliciesOutput, error) {
func ListAttachedRolePolicies(accountID string, input *iam.ListAttachedRolePoliciesInput, svc handlers_iam.IAMService) (*iam.ListAttachedRolePoliciesOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -163,7 +163,7 @@ func ListAttachedRolePolicies(accountID string, input *iam.ListAttachedRolePolic
// PutRolePolicy implements the IAM PutRolePolicy action, adding or replacing a role inline
// policy. It returns MissingParameter when RoleName, PolicyName or PolicyDocument is absent, then
// calls the IAM service for accountID.
func PutRolePolicy(accountID string, input *iam.PutRolePolicyInput, svc iamdomain.IAMService) (*iam.PutRolePolicyOutput, error) {
func PutRolePolicy(accountID string, input *iam.PutRolePolicyInput, svc handlers_iam.IAMService) (*iam.PutRolePolicyOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -179,7 +179,7 @@ func PutRolePolicy(accountID string, input *iam.PutRolePolicyInput, svc iamdomai
// GetRolePolicy implements the IAM GetRolePolicy action, reading a role inline policy. It
// requires RoleName or PolicyName and returns the document percent-encoded as the IAM Query API
// does.
func GetRolePolicy(accountID string, input *iam.GetRolePolicyInput, svc iamdomain.IAMService) (*iam.GetRolePolicyOutput, error) {
func GetRolePolicy(accountID string, input *iam.GetRolePolicyInput, svc handlers_iam.IAMService) (*iam.GetRolePolicyOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -197,7 +197,7 @@ func GetRolePolicy(accountID string, input *iam.GetRolePolicyInput, svc iamdomai
// DeleteRolePolicy implements the IAM DeleteRolePolicy action, deleting a role inline policy. It
// returns MissingParameter when RoleName or PolicyName is absent, then calls the IAM service for
// accountID.
func DeleteRolePolicy(accountID string, input *iam.DeleteRolePolicyInput, svc iamdomain.IAMService) (*iam.DeleteRolePolicyOutput, error) {
func DeleteRolePolicy(accountID string, input *iam.DeleteRolePolicyInput, svc handlers_iam.IAMService) (*iam.DeleteRolePolicyOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -209,7 +209,7 @@ func DeleteRolePolicy(accountID string, input *iam.DeleteRolePolicyInput, svc ia
// ListRolePolicies implements the IAM ListRolePolicies action, listing a role's inline policy
// names. It requires RoleName; results are paged here by Marker and MaxItems.
func ListRolePolicies(accountID string, input *iam.ListRolePoliciesInput, svc iamdomain.IAMService) (*iam.ListRolePoliciesOutput, error) {
func ListRolePolicies(accountID string, input *iam.ListRolePoliciesInput, svc handlers_iam.IAMService) (*iam.ListRolePoliciesOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -228,7 +228,7 @@ func ListRolePolicies(accountID string, input *iam.ListRolePoliciesInput, svc ia
// TagRole implements the IAM TagRole action. It returns MissingParameter when RoleName is absent
// or Tags is empty.
func TagRole(accountID string, input *iam.TagRoleInput, svc iamdomain.IAMService) (*iam.TagRoleOutput, error) {
func TagRole(accountID string, input *iam.TagRoleInput, svc handlers_iam.IAMService) (*iam.TagRoleOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -240,7 +240,7 @@ func TagRole(accountID string, input *iam.TagRoleInput, svc iamdomain.IAMService
// UntagRole implements the IAM UntagRole action. It returns MissingParameter when RoleName is
// absent or TagKeys is empty.
func UntagRole(accountID string, input *iam.UntagRoleInput, svc iamdomain.IAMService) (*iam.UntagRoleOutput, error) {
func UntagRole(accountID string, input *iam.UntagRoleInput, svc handlers_iam.IAMService) (*iam.UntagRoleOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -252,7 +252,7 @@ func UntagRole(accountID string, input *iam.UntagRoleInput, svc iamdomain.IAMSer
// ListRoleTags implements the IAM ListRoleTags action, listing a role's tags. It requires
// RoleName; results are paged here by Marker and MaxItems.
func ListRoleTags(accountID string, input *iam.ListRoleTagsInput, svc iamdomain.IAMService) (*iam.ListRoleTagsOutput, error) {
func ListRoleTags(accountID string, input *iam.ListRoleTagsInput, svc handlers_iam.IAMService) (*iam.ListRoleTagsOutput, error) {
if input.RoleName == nil || *input.RoleName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
+12 -12
View File
@@ -5,13 +5,13 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// CreateUser implements the IAM CreateUser action, creating an IAM user. It returns
// MissingParameter when UserName is absent, then calls the IAM service for accountID.
func CreateUser(accountID string, input *iam.CreateUserInput, svc iamdomain.IAMService) (*iam.CreateUserOutput, error) {
func CreateUser(accountID string, input *iam.CreateUserInput, svc handlers_iam.IAMService) (*iam.CreateUserOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -20,7 +20,7 @@ func CreateUser(accountID string, input *iam.CreateUserInput, svc iamdomain.IAMS
// GetUser implements the IAM GetUser action. UserName is required (MissingParameter); the AWS
// default of describing the calling user is not supported.
func GetUser(accountID string, input *iam.GetUserInput, svc iamdomain.IAMService) (*iam.GetUserOutput, error) {
func GetUser(accountID string, input *iam.GetUserInput, svc handlers_iam.IAMService) (*iam.GetUserOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -29,7 +29,7 @@ func GetUser(accountID string, input *iam.GetUserInput, svc iamdomain.IAMService
// ListUsers implements the IAM ListUsers action, listing the account's users. It validates
// PathPrefix; results are paged here by Marker and MaxItems.
func ListUsers(accountID string, input *iam.ListUsersInput, svc iamdomain.IAMService) (*iam.ListUsersOutput, error) {
func ListUsers(accountID string, input *iam.ListUsersInput, svc handlers_iam.IAMService) (*iam.ListUsersOutput, error) {
if err := validatePathPrefix(input.PathPrefix, identityPathPrefix); err != nil {
return nil, err
}
@@ -48,7 +48,7 @@ func ListUsers(accountID string, input *iam.ListUsersInput, svc iamdomain.IAMSer
// DeleteUser implements the IAM DeleteUser action, deleting an IAM user. It returns
// MissingParameter when UserName is absent, then calls the IAM service for accountID.
func DeleteUser(accountID string, input *iam.DeleteUserInput, svc iamdomain.IAMService) (*iam.DeleteUserOutput, error) {
func DeleteUser(accountID string, input *iam.DeleteUserInput, svc handlers_iam.IAMService) (*iam.DeleteUserOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -58,7 +58,7 @@ func DeleteUser(accountID string, input *iam.DeleteUserInput, svc iamdomain.IAMS
// PutUserPolicy implements the IAM PutUserPolicy action, adding or replacing a user inline
// policy. It returns MissingParameter when UserName, PolicyName or PolicyDocument is absent, then
// calls the IAM service for accountID.
func PutUserPolicy(accountID string, input *iam.PutUserPolicyInput, svc iamdomain.IAMService) (*iam.PutUserPolicyOutput, error) {
func PutUserPolicy(accountID string, input *iam.PutUserPolicyInput, svc handlers_iam.IAMService) (*iam.PutUserPolicyOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -74,7 +74,7 @@ func PutUserPolicy(accountID string, input *iam.PutUserPolicyInput, svc iamdomai
// GetUserPolicy implements the IAM GetUserPolicy action, reading a user inline policy. It
// requires UserName or PolicyName and returns the document percent-encoded as the IAM Query API
// does.
func GetUserPolicy(accountID string, input *iam.GetUserPolicyInput, svc iamdomain.IAMService) (*iam.GetUserPolicyOutput, error) {
func GetUserPolicy(accountID string, input *iam.GetUserPolicyInput, svc handlers_iam.IAMService) (*iam.GetUserPolicyOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -92,7 +92,7 @@ func GetUserPolicy(accountID string, input *iam.GetUserPolicyInput, svc iamdomai
// DeleteUserPolicy implements the IAM DeleteUserPolicy action, deleting a user inline policy. It
// returns MissingParameter when UserName or PolicyName is absent, then calls the IAM service for
// accountID.
func DeleteUserPolicy(accountID string, input *iam.DeleteUserPolicyInput, svc iamdomain.IAMService) (*iam.DeleteUserPolicyOutput, error) {
func DeleteUserPolicy(accountID string, input *iam.DeleteUserPolicyInput, svc handlers_iam.IAMService) (*iam.DeleteUserPolicyOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -104,7 +104,7 @@ func DeleteUserPolicy(accountID string, input *iam.DeleteUserPolicyInput, svc ia
// ListUserPolicies implements the IAM ListUserPolicies action, listing a user's inline policy
// names. It requires UserName; results are paged here by Marker and MaxItems.
func ListUserPolicies(accountID string, input *iam.ListUserPoliciesInput, svc iamdomain.IAMService) (*iam.ListUserPoliciesOutput, error) {
func ListUserPolicies(accountID string, input *iam.ListUserPoliciesInput, svc handlers_iam.IAMService) (*iam.ListUserPoliciesOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -123,7 +123,7 @@ func ListUserPolicies(accountID string, input *iam.ListUserPoliciesInput, svc ia
// TagUser implements the IAM TagUser action. It returns MissingParameter when UserName is absent
// or Tags is empty.
func TagUser(accountID string, input *iam.TagUserInput, svc iamdomain.IAMService) (*iam.TagUserOutput, error) {
func TagUser(accountID string, input *iam.TagUserInput, svc handlers_iam.IAMService) (*iam.TagUserOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -135,7 +135,7 @@ func TagUser(accountID string, input *iam.TagUserInput, svc iamdomain.IAMService
// UntagUser implements the IAM UntagUser action. It returns MissingParameter when UserName is
// absent or TagKeys is empty.
func UntagUser(accountID string, input *iam.UntagUserInput, svc iamdomain.IAMService) (*iam.UntagUserOutput, error) {
func UntagUser(accountID string, input *iam.UntagUserInput, svc handlers_iam.IAMService) (*iam.UntagUserOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
@@ -147,7 +147,7 @@ func UntagUser(accountID string, input *iam.UntagUserInput, svc iamdomain.IAMSer
// ListUserTags implements the IAM ListUserTags action, listing a user's tags. It requires
// UserName; results are paged here by Marker and MaxItems.
func ListUserTags(accountID string, input *iam.ListUserTagsInput, svc iamdomain.IAMService) (*iam.ListUserTagsOutput, error) {
func ListUserTags(accountID string, input *iam.ListUserTagsInput, svc handlers_iam.IAMService) (*iam.ListUserTagsOutput, error) {
if input.UserName == nil || *input.UserName == "" {
return nil, errors.New(awserrors.ErrorMissingParameter)
}
+5 -5
View File
@@ -11,9 +11,9 @@ import (
"testing"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -46,12 +46,12 @@ func (s *iamAuthzPolicyService) DeleteRole(_ string, input *iam.DeleteRoleInput)
return &iam.DeleteRoleOutput{}, nil
}
func iamScopedGateway(resources map[string]string, statements ...iamdomain.Statement) (*GatewayConfig, *iamAuthzPolicyService) {
docs := []iamdomain.PolicyDocument{{Version: "2012-10-17", Statement: statements}}
func iamScopedGateway(resources map[string]string, statements ...handlers_iam.Statement) (*GatewayConfig, *iamAuthzPolicyService) {
docs := []handlers_iam.PolicyDocument{{Version: "2012-10-17", Statement: statements}}
svc := &iamAuthzPolicyService{
policyMockIAMService: policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getRolePoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
getRolePoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
},
arns: resources,
}
@@ -12,7 +12,7 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -333,4 +333,4 @@ func (svc *echoIAMService) UpdateAssumeRolePolicy(accountID string, input *iam.U
return echoIAMCall[iam.UpdateAssumeRolePolicyInput, iam.UpdateAssumeRolePolicyOutput](svc, accountID, input)
}
var _ iamdomain.IAMService = (*echoIAMService)(nil)
var _ handlers_iam.IAMService = (*echoIAMService)(nil)
+2 -2
View File
@@ -7,9 +7,9 @@ import (
"testing"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -41,7 +41,7 @@ func (s *addRoleIAMService) AddRoleToInstanceProfile(_ string, input *iam.AddRol
return &iam.AddRoleToInstanceProfileOutput{}, nil
}
func addRoleGateway(statements ...iamdomain.Statement) (*GatewayConfig, *addRoleIAMService) {
func addRoleGateway(statements ...handlers_iam.Statement) (*GatewayConfig, *addRoleIAMService) {
base, inner := iamScopedGateway(map[string]string{
"web": passRoleProfileARN,
"admin": passRoleAdminARN,
+7 -7
View File
@@ -13,9 +13,9 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/arn"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -25,7 +25,7 @@ import (
// methods below are wired, and any other method nil-panics if a test reaches
// an unmocked path.
type flexMockIAMService struct {
iamdomain.IAMService
handlers_iam.IAMService
createUserFn func(string, *iam.CreateUserInput) (*iam.CreateUserOutput, error)
getUserFn func(string, *iam.GetUserInput) (*iam.GetUserOutput, error)
@@ -36,7 +36,7 @@ type flexMockIAMService struct {
deleteAccessKeyFn func(string, *iam.DeleteAccessKeyInput) (*iam.DeleteAccessKeyOutput, error)
updateAccessKeyFn func(string, *iam.UpdateAccessKeyInput) (*iam.UpdateAccessKeyOutput, error)
canonicalResourceARNFn func(string, arn.IAMResourceType, string) (string, error)
lookupAccessKeyFn func(string) (*iamdomain.AccessKey, error)
lookupAccessKeyFn func(string) (*handlers_iam.AccessKey, error)
getAccountSummaryFn func(string, *iam.GetAccountSummaryInput) (*iam.GetAccountSummaryOutput, error)
getRoleFn func(string, *iam.GetRoleInput) (*iam.GetRoleOutput, error)
@@ -115,11 +115,11 @@ func (m *flexMockIAMService) CanonicalResourceARN(accountID string, kind arn.IAM
return arn.FormatIAMPath(kind, accountID, "/", name), nil
}
func (m *flexMockIAMService) LookupAccessKey(accessKeyID string) (*iamdomain.AccessKey, error) {
func (m *flexMockIAMService) LookupAccessKey(accessKeyID string) (*handlers_iam.AccessKey, error) {
if m.lookupAccessKeyFn != nil {
return m.lookupAccessKeyFn(accessKeyID)
}
return &iamdomain.AccessKey{AccessKeyID: accessKeyID, AccountID: "000000000000", UserName: "user"}, nil
return &handlers_iam.AccessKey{AccessKeyID: accessKeyID, AccountID: "000000000000", UserName: "user"}, nil
}
func (m *flexMockIAMService) GetAccountSummary(accountID string, input *iam.GetAccountSummaryInput) (*iam.GetAccountSummaryOutput, error) {
@@ -187,7 +187,7 @@ func (m *flexMockIAMService) ListUserPolicies(_ string, _ *iam.ListUserPoliciesI
}
// setupIAMRequestHandler wires an http.Handler for IAM_Request with injected SigV4 context values.
func setupIAMRequestHandler(svc iamdomain.IAMService) http.Handler {
func setupIAMRequestHandler(svc handlers_iam.IAMService) http.Handler {
gw := &GatewayConfig{
DisableLogging: true,
IAMService: svc,
@@ -295,7 +295,7 @@ type realAliasesIAMService struct {
}
func (realAliasesIAMService) ListAccountAliases(accountID string, input *iam.ListAccountAliasesInput) (*iam.ListAccountAliasesOutput, error) {
return (&iamdomain.IAMServiceImpl{}).ListAccountAliases(accountID, input)
return (&handlers_iam.IAMServiceImpl{}).ListAccountAliases(accountID, input)
}
// AWS answers an account with no alias with an empty AccountAliases list and
+7 -7
View File
@@ -6,15 +6,15 @@ package gateway
import (
"testing"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// The PowerUserAccess shape: an Allow on everything except the listed actions.
func TestEC2Request_AllowNotActionGrantsTheComplement(t *testing.T) {
gw := scopedPolicyGateway(iamdomain.Statement{
gw := scopedPolicyGateway(handlers_iam.Statement{
Effect: "Allow",
NotAction: iamdomain.StringOrArr{"iam:*", "ec2:TerminateInstances"},
Resource: iamdomain.StringOrArr{"*"},
NotAction: handlers_iam.StringOrArr{"iam:*", "ec2:TerminateInstances"},
Resource: handlers_iam.StringOrArr{"*"},
})
assertPermitted(t, dispatchEC2(t, gw, "Action=StopInstances&InstanceId.1=i-dev"))
@@ -25,10 +25,10 @@ func TestEC2Request_AllowNotActionGrantsTheComplement(t *testing.T) {
func TestEC2Request_DenyNotResourceFencesTheComplement(t *testing.T) {
gw := scopedPolicyGateway(
statement("Allow", "ec2:*", "*"),
iamdomain.Statement{
handlers_iam.Statement{
Effect: "Deny",
Action: iamdomain.StringOrArr{"ec2:TerminateInstances"},
NotResource: iamdomain.StringOrArr{"arn:aws:ec2:*:*:instance/i-dev"},
Action: handlers_iam.StringOrArr{"ec2:TerminateInstances"},
NotResource: handlers_iam.StringOrArr{"arn:aws:ec2:*:*:instance/i-dev"},
},
)
@@ -20,9 +20,9 @@ import (
"github.com/stretchr/testify/require"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil"
handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
)
@@ -174,20 +174,20 @@ func TestCurrentBehaviour_OIDCDiscoveryKVUnavailable(t *testing.T) {
func TestCurrentBehaviour_WebIdentityJWKSMissingWireResponse(t *testing.T) {
_, nc, _ := testutil.StartTestJetStream(t)
js := testutil.NewJetStream(t, nc)
masterKey, err := iamdomain.GenerateMasterKey()
masterKey, err := handlers_iam.GenerateMasterKey()
require.NoError(t, err)
iamSvc, err := iamdomain.NewIAMServiceImpl(t.Context(), nc, masterKey)
iamSvc, err := handlers_iam.NewIAMServiceImpl(t.Context(), nc, masterKey)
require.NoError(t, err)
stsSvc, err := handlers_sts.NewSTSServiceImpl(t.Context(), nc, iamSvc, masterKey, handlers_eks.NewOIDCJWKSReader(js))
require.NoError(t, err)
issuer := testDiscIssuer
iamKV, err := kvutil.GetOrCreateBucket(t.Context(), js,
iamdomain.IAMAccountBucketName(testDiscAccount), iamdomain.KVBucketIAMAccountVersion)
handlers_iam.IAMAccountBucketName(testDiscAccount), handlers_iam.KVBucketIAMAccountVersion)
require.NoError(t, err)
_, err = iamKV.Put(t.Context(), iamdomain.OIDCProviderKey(issuer), []byte(`{"registered":true}`))
_, err = iamKV.Put(t.Context(), handlers_iam.OIDCProviderKey(issuer), []byte(`{"registered":true}`))
require.NoError(t, err)
federated := iamdomain.OIDCProviderARN(testDiscAccount, strings.TrimPrefix(issuer, "https://"))
federated := handlers_iam.OIDCProviderARN(testDiscAccount, strings.TrimPrefix(issuer, "https://"))
role, err := iamSvc.CreateRole(testDiscAccount, &iam.CreateRoleInput{
RoleName: aws.String("irsa-wire"),
AssumeRolePolicyDocument: aws.String(`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":"` +
+8 -8
View File
@@ -6,32 +6,32 @@ import (
"testing"
"github.com/mulgadc/bluebottle/pkg/iampolicy"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// passRoleTo is a PassRole statement on every role, conditioned on the
// consuming service the way AWS's documented examples scope it.
func passRoleTo(effect, operator, service string) iamdomain.Statement {
func passRoleTo(effect, operator, service string) handlers_iam.Statement {
s := statement(effect, "iam:PassRole", "*")
s.Condition = map[string]map[string]iamdomain.ConditionValue{
s.Condition = map[string]map[string]handlers_iam.ConditionValue{
operator: {iampolicy.KeyPassedToService: {service}},
}
return s
}
func passRoleToGateway(t *testing.T, statements ...iamdomain.Statement) *GatewayConfig {
func passRoleToGateway(t *testing.T, statements ...handlers_iam.Statement) *GatewayConfig {
t.Helper()
docs := []iamdomain.PolicyDocument{{Version: "2012-10-17", Statement: statements}}
docs := []handlers_iam.PolicyDocument{{Version: "2012-10-17", Statement: statements}}
return &GatewayConfig{
DisableLogging: true,
Region: authzRegion,
NATSConn: startTestNATS(t),
IAMService: &profileIAMService{
policyMockIAMService: policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return docs, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return docs, nil },
},
},
}
@@ -122,7 +122,7 @@ func TestPassedToService_EKS(t *testing.T) {
// A Deny scoped to one service fences that service and leaves the others to
// the unconditional grant.
func TestPassedToService_DenyFencesOneService(t *testing.T) {
statements := []iamdomain.Statement{
statements := []handlers_iam.Statement{
statement("Allow", "ec2:*", "*"),
statement("Allow", "ecs:*", "*"),
statement("Allow", "iam:PassRole", "*"),
@@ -136,7 +136,7 @@ func TestPassedToService_DenyFencesOneService(t *testing.T) {
// any other action does not hold.
func TestPassedToService_AbsentOutsidePassRole(t *testing.T) {
grant := statement("Allow", "ec2:*", "*")
grant.Condition = map[string]map[string]iamdomain.ConditionValue{
grant.Condition = map[string]map[string]handlers_iam.ConditionValue{
iampolicy.OpStringEquals: {iampolicy.KeyPassedToService: {"ec2.amazonaws.com"}},
}
assertUnauthorized(t, dispatchEC2(t, scopedPolicyGateway(grant), "Action=DeleteVolume"))
+6 -6
View File
@@ -4,22 +4,22 @@ import (
"context"
"net/http"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// allowAllIAMService resolves an allow-everything policy for every principal.
// Tests that exercise handler behaviour rather than authorization use it so the
// policy gate passes on a real evaluation instead of a bypass.
func allowAllIAMService() *policyMockIAMService {
allow := []iamdomain.PolicyDocument{{
allow := []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{"*"}, Resource: iamdomain.StringOrArr{"*"}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"*"}, Resource: handlers_iam.StringOrArr{"*"}},
},
}}
return &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return allow, nil },
getRolePoliciesFn: func(_, _ string) ([]iamdomain.PolicyDocument, error) { return allow, nil },
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return allow, nil },
getRolePoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return allow, nil },
}
}
+4 -4
View File
@@ -11,8 +11,8 @@ import (
"github.com/go-chi/chi/v5"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -25,14 +25,14 @@ import (
func setupTestAppWithRateLimiter(t *testing.T, accessKey, secretKey string, rl *authlimit.AuthRateLimiter) http.Handler {
t.Helper()
encryptedSecret, err := iamdomain.EncryptSecret(secretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(secretKey, testMasterKey)
if err != nil {
panic("failed to encrypt test secret: " + err.Error())
}
mockSvc := &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{
accessKeys: map[string]*handlers_iam.AccessKey{
accessKey: {
AccessKeyID: accessKey,
SecretAccessKey: encryptedSecret,
@@ -146,7 +146,7 @@ func recordProbeFailures(rl *authlimit.AuthRateLimiter, ip, batch string, n int)
// End to end: a client whose credential will never resolve keeps getting the
// verdict that says so, and never the 503 that tells it to retry.
func TestStaleCredentialNeverLocksTheAddressOut(t *testing.T) {
handler, _ := auditRouter(t, map[string]*iamdomain.AccessKey{})
handler, _ := auditRouter(t, map[string]*handlers_iam.AccessKey{})
for range authlimit.MaxFailures * 3 {
w := httptest.NewRecorder()
+17 -17
View File
@@ -11,9 +11,9 @@ import (
"github.com/aws/aws-sdk-go/service/rds"
"github.com/mulgadc/spinifex/internal/testkit"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
natsmsg "github.com/mulgadc/spinifex/spinifex/foundation/messaging/nats"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_rds "github.com/mulgadc/spinifex/spinifex/handlers/rds"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
@@ -59,17 +59,17 @@ type rdsPolicyProbe struct{ consulted int }
// A gateway whose IAM service answers with policies, so the RDS dispatch path
// runs a real evaluation for either principal branch.
func newRDSPolicyGateway(policies []iamdomain.PolicyDocument) (*GatewayConfig, *rdsPolicyProbe) {
return newRDSGatewayWithResolver(func(_, _ string) ([]iamdomain.PolicyDocument, error) {
func newRDSPolicyGateway(policies []handlers_iam.PolicyDocument) (*GatewayConfig, *rdsPolicyProbe) {
return newRDSGatewayWithResolver(func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return policies, nil
})
}
func newRDSGatewayWithResolver(
resolve func(accountID, name string) ([]iamdomain.PolicyDocument, error),
resolve func(accountID, name string) ([]handlers_iam.PolicyDocument, error),
) (*GatewayConfig, *rdsPolicyProbe) {
probe := &rdsPolicyProbe{}
counted := func(accountID, name string) ([]iamdomain.PolicyDocument, error) {
counted := func(accountID, name string) ([]handlers_iam.PolicyDocument, error) {
probe.consulted++
return resolve(accountID, name)
}
@@ -200,14 +200,14 @@ func TestRDSRequest_SnapshotActionsAuthorizeSourceAndTarget(t *testing.T) {
name string
action string
query string
resources iamdomain.StringOrArr
resources handlers_iam.StringOrArr
}{
{
name: "create snapshot",
action: "rds:CreateDBSnapshot",
query: "Action=CreateDBSnapshot&DBInstanceIdentifier=orders-db" +
"&DBSnapshotIdentifier=orders-db-nightly",
resources: iamdomain.StringOrArr{
resources: handlers_iam.StringOrArr{
handlers_rds.FormatARN(handlers_rds.ResourceKindDBInstance, "ap-southeast-2", rdsTestAccountID, "orders-db"),
handlers_rds.FormatARN(handlers_rds.ResourceKindDBSnapshot, "ap-southeast-2", rdsTestAccountID, "orders-db-nightly"),
},
@@ -217,7 +217,7 @@ func TestRDSRequest_SnapshotActionsAuthorizeSourceAndTarget(t *testing.T) {
action: "rds:RestoreDBInstanceFromDBSnapshot",
query: "Action=RestoreDBInstanceFromDBSnapshot&DBSnapshotIdentifier=orders-db-nightly" +
"&DBInstanceIdentifier=orders-db-restored",
resources: iamdomain.StringOrArr{
resources: handlers_iam.StringOrArr{
handlers_rds.FormatARN(handlers_rds.ResourceKindDBSnapshot, "ap-southeast-2", rdsTestAccountID, "orders-db-nightly"),
handlers_rds.FormatARN(handlers_rds.ResourceKindDBInstance, "ap-southeast-2", rdsTestAccountID, "orders-db-restored"),
},
@@ -225,10 +225,10 @@ func TestRDSRequest_SnapshotActionsAuthorizeSourceAndTarget(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
policies := []iamdomain.PolicyDocument{{
policies := []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{{
Effect: "Allow", Action: iamdomain.StringOrArr{tt.action}, Resource: tt.resources,
Statement: []handlers_iam.Statement{{
Effect: "Allow", Action: handlers_iam.StringOrArr{tt.action}, Resource: tt.resources,
}},
}}
gw, probe := newRDSPolicyGateway(policies)
@@ -244,7 +244,7 @@ func TestRDSRequest_SnapshotActionsUseOnePolicySnapshot(t *testing.T) {
source := handlers_rds.FormatARN(handlers_rds.ResourceKindDBInstance, "ap-southeast-2", rdsTestAccountID, "orders-db")
target := handlers_rds.FormatARN(handlers_rds.ResourceKindDBSnapshot, "ap-southeast-2", rdsTestAccountID, "orders-db-nightly")
resolution := 0
gw, probe := newRDSGatewayWithResolver(func(_, _ string) ([]iamdomain.PolicyDocument, error) {
gw, probe := newRDSGatewayWithResolver(func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
resolution++
if resolution == 1 {
return allowPolicyResource("rds:CreateDBSnapshot", source), nil
@@ -283,11 +283,11 @@ func TestRDSRequest_SnapshotActionsHonorTargetDeny(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
policies := []iamdomain.PolicyDocument{{
policies := []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []iamdomain.Statement{
{Effect: "Allow", Action: iamdomain.StringOrArr{tt.action}, Resource: iamdomain.StringOrArr{"*"}},
{Effect: "Deny", Action: iamdomain.StringOrArr{tt.action}, Resource: iamdomain.StringOrArr{tt.target}},
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{tt.action}, Resource: handlers_iam.StringOrArr{"*"}},
{Effect: "Deny", Action: handlers_iam.StringOrArr{tt.action}, Resource: handlers_iam.StringOrArr{tt.target}},
},
}}
gw, probe := newRDSPolicyGateway(policies)
@@ -313,7 +313,7 @@ func TestRDSRequest_UnscopedActionsEvaluateAgainstAnyResource(t *testing.T) {
// snapshot sharing is out of v1, and an evaluator that sees a foreign account is
// one policy away from allowing it.
func TestRDSRequest_CrossAccountARNIsRejectedNotEvaluated(t *testing.T) {
gw, _ := newRDSGatewayWithResolver(func(_, _ string) ([]iamdomain.PolicyDocument, error) {
gw, _ := newRDSGatewayWithResolver(func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
t.Error("a cross-account ARN must never reach policy evaluation")
return nil, nil
})
+4 -4
View File
@@ -17,8 +17,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// A handler reached outside the middleware chain — a direct call from a test —
@@ -73,7 +73,7 @@ func TestRequestAuditCarriesNoSecret(t *testing.T) {
// auditRouter wires the audit middleware ahead of SigV4 auth the way
// SetupRoutes does, and hands back the record for the request just served.
func auditRouter(t *testing.T, keys map[string]*iamdomain.AccessKey) (http.Handler, func() *requestAudit) {
func auditRouter(t *testing.T, keys map[string]*handlers_iam.AccessKey) (http.Handler, func() *requestAudit) {
t.Helper()
rl := authlimit.NewAuthRateLimiter()
@@ -143,7 +143,7 @@ func signedRequestForKey(remoteAddr, accessKeyID string) *http.Request {
// A rejected caller is identifiable: the key it presented is recorded even
// though authentication failed on it.
func TestRequestAuditRecordsRejectedAccessKey(t *testing.T) {
handler, audit := auditRouter(t, map[string]*iamdomain.AccessKey{})
handler, audit := auditRouter(t, map[string]*handlers_iam.AccessKey{})
w := httptest.NewRecorder()
handler.ServeHTTP(w, signedRequest("10.15.8.12:54321"))
@@ -157,7 +157,7 @@ func TestRequestAuditRecordsRejectedAccessKey(t *testing.T) {
// The lockout that answers 503 is the case that motivated this: without the
// record, the response carries no client address and no reason at all.
func TestRequestAuditRecordsRateLimitLockout(t *testing.T) {
handler, audit := auditRouter(t, map[string]*iamdomain.AccessKey{})
handler, audit := auditRouter(t, map[string]*handlers_iam.AccessKey{})
// Distinct key ids: one key id repeated is one fault and never locks out.
send := func(accessKeyID string) int {
+3 -3
View File
@@ -10,8 +10,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
)
@@ -35,7 +35,7 @@ const (
func GetCallerIdentity(
accountID, callerARN, callerPrincipalType, identity, assumedRoleID string,
input *sts.GetCallerIdentityInput,
iamSvc iamdomain.IAMService,
iamSvc handlers_iam.IAMService,
stsSvc handlers_sts.STSService,
) (*sts.GetCallerIdentityOutput, error) {
userID, _, err := ResolveCallerUser(accountID, callerPrincipalType, identity, assumedRoleID, iamSvc)
@@ -60,7 +60,7 @@ func GetCallerIdentity(
// An error is a dependency fault; a missing value returns empty.
func ResolveCallerUser(
accountID, callerPrincipalType, identity, assumedRoleID string,
iamSvc iamdomain.IAMService,
iamSvc handlers_iam.IAMService,
) (userID, userARN string, err error) {
switch callerPrincipalType {
case PrincipalTypeRoot:
+3 -3
View File
@@ -7,8 +7,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -67,12 +67,12 @@ func (s *stubSTSService) LookupSessionCredential(akid string) (*handlers_sts.Ses
// that triggers an unexpected call fails loudly instead of silently returning
// zero values.
type stubIAMService struct {
iamdomain.IAMService
handlers_iam.IAMService
getUserFn func(accountID string, input *iam.GetUserInput) (*iam.GetUserOutput, error)
}
var _ iamdomain.IAMService = (*stubIAMService)(nil)
var _ handlers_iam.IAMService = (*stubIAMService)(nil)
func (s *stubIAMService) GetUser(accountID string, input *iam.GetUserInput) (*iam.GetUserOutput, error) {
if s.getUserFn != nil {
+11 -11
View File
@@ -16,8 +16,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -54,13 +54,13 @@ func (m *stsPolicyIAMService) GetRole(_ string, input *iam.GetRoleInput) (*iam.G
// stsIdentityPolicy builds an IAM service returning the given statements as the
// caller's only identity policy, for a user and for an assumed role alike. The
// one stored role is the pathless `app`; withRoles replaces the set.
func stsIdentityPolicy(statements ...iamdomain.Statement) *stsPolicyIAMService {
docs := []iamdomain.PolicyDocument{{Version: "2012-10-17", Statement: statements}}
func stsIdentityPolicy(statements ...handlers_iam.Statement) *stsPolicyIAMService {
docs := []handlers_iam.PolicyDocument{{Version: "2012-10-17", Statement: statements}}
svc := &stsPolicyIAMService{roles: map[string]string{stsTestRoleName: stsTestRoleARN}}
svc.getUserPoliciesFn = func(_, _ string) ([]iamdomain.PolicyDocument, error) {
svc.getUserPoliciesFn = func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return docs, nil
}
svc.getRolePoliciesFn = func(_, _ string) ([]iamdomain.PolicyDocument, error) {
svc.getRolePoliciesFn = func(_, _ string) ([]handlers_iam.PolicyDocument, error) {
return docs, nil
}
return svc
@@ -74,11 +74,11 @@ func (m *stsPolicyIAMService) withRoles(roles map[string]string) *stsPolicyIAMSe
}
// stsStatement is shorthand for a single-action, single-resource statement.
func stsStatement(effect, action, resource string) iamdomain.Statement {
return iamdomain.Statement{
func stsStatement(effect, action, resource string) handlers_iam.Statement {
return handlers_iam.Statement{
Effect: effect,
Action: iamdomain.StringOrArr{action},
Resource: iamdomain.StringOrArr{resource},
Action: handlers_iam.StringOrArr{action},
Resource: handlers_iam.StringOrArr{resource},
}
}
@@ -96,7 +96,7 @@ func deniedSTSService(t *testing.T) *flexMockSTSService {
// stsPolicyRequest dispatches an STS action as alice with the given identity
// policies.
func stsPolicyRequest(t *testing.T, iamSvc iamdomain.IAMService, stsSvc *flexMockSTSService, body string) *http.Response {
func stsPolicyRequest(t *testing.T, iamSvc handlers_iam.IAMService, stsSvc *flexMockSTSService, body string) *http.Response {
t.Helper()
if stsSvc == nil {
stsSvc = deniedSTSService(t)
@@ -166,7 +166,7 @@ func TestSTSRequest_AssumeRole_IdentityPolicyGate(t *testing.T) {
// grants a non-sts action and nothing else must not be able to assume anything.
func TestSTSRequest_AssumeRole_AssumedRoleCallerIsGated(t *testing.T) {
body := "Action=AssumeRole&RoleArn=" + stsTestRoleARN + "&RoleSessionName=s1"
params := func(iamSvc iamdomain.IAMService, stsSvc *flexMockSTSService) stsRequestParams {
params := func(iamSvc handlers_iam.IAMService, stsSvc *flexMockSTSService) stsRequestParams {
return stsRequestParams{
accountID: awsidentifiers.GlobalAccountID,
identity: "s1",
+4 -4
View File
@@ -13,8 +13,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/sts"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -87,7 +87,7 @@ type stsRequestParams struct {
// caller's policies by; SessionName never participates.
underlyingRoleARN string
stsSvc handlers_sts.STSService
iamSvc iamdomain.IAMService
iamSvc handlers_iam.IAMService
}
func setupSTSRequestHandler(p stsRequestParams) http.Handler {
@@ -447,7 +447,7 @@ func TestSTSRequest_GetCallerIdentity_User_LookupIAM(t *testing.T) {
// resolved by the SigV4 middleware rather than reformatted at "/".
func TestSTSRequest_GetCallerIdentity_PathedUserThroughMiddleware(t *testing.T) {
const pathed = "arn:aws:iam::123456789012:user/eng/alice"
encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey)
encryptedSecret, err := handlers_iam.EncryptSecret(testSecretKey, testMasterKey)
require.NoError(t, err)
gw := &GatewayConfig{
DisableLogging: true,
@@ -455,7 +455,7 @@ func TestSTSRequest_GetCallerIdentity_PathedUserThroughMiddleware(t *testing.T)
STSService: &flexMockSTSService{},
IAMService: &mockIAMService{
masterKey: testMasterKey,
accessKeys: map[string]*iamdomain.AccessKey{testAccessKey: {
accessKeys: map[string]*handlers_iam.AccessKey{testAccessKey: {
AccessKeyID: testAccessKey, SecretAccessKey: encryptedSecret,
UserName: "alice", AccountID: "123456789012", Status: "Active",
}},
+1 -1
View File
@@ -7,9 +7,9 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
iammock "github.com/mulgadc/spinifex/spinifex/domains/iam/mock"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/errors"
"github.com/mulgadc/spinifex/spinifex/foundation/aws/tags"
iammock "github.com/mulgadc/spinifex/spinifex/handlers/iam/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+3 -3
View File
@@ -3,7 +3,7 @@ package handlers_ecs
import (
"fmt"
iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
const (
@@ -35,7 +35,7 @@ func ecsInstanceRolePolicyDoc(accountID string) string {
// is written rather than created so an existing role picks up a changed
// document instead of keeping the one it was provisioned with.
func (s *Service) ensureECSInstanceProfile(accountID string) (string, error) {
return iamdomain.EnsureSystemInstanceProfile(s.deps.IAM, accountID,
return handlers_iam.EnsureSystemInstanceProfile(s.deps.IAM, accountID,
ecsInstanceRoleName, ecsInstanceRolePolicyName, ecsInstanceRolePolicyDoc(accountID))
}
@@ -43,6 +43,6 @@ func (s *Service) ensureECSInstanceProfile(accountID string) (string, error) {
// already has the role, so a running cluster picks up a changed document
// without creating IAM entities in accounts that never launched capacity.
func (s *Service) convergeECSInstanceRole(accountID string) error {
return iamdomain.ConvergeSystemRolePolicy(s.deps.IAM, accountID,
return handlers_iam.ConvergeSystemRolePolicy(s.deps.IAM, accountID,
ecsInstanceRoleName, ecsInstanceRolePolicyName, ecsInstanceRolePolicyDoc(accountID))
}

Some files were not shown because too many files have changed in this diff Show More