diff --git a/architecture/layering/allowlist_test.go b/architecture/layering/allowlist_test.go index 3d66cffd6..e0c002fb2 100644 --- a/architecture/layering/allowlist_test.go +++ b/architecture/layering/allowlist_test.go @@ -59,7 +59,7 @@ var allowlist = []allowance{ {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/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: "domains/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"}, {From: "domains/ec2/instance", To: "domains/dns", Debt: "domains/ec2/instance", Reason: "cross-domain implementation import of DNS"}, @@ -153,7 +153,7 @@ var allowlist = []allowance{ {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: "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: "domains/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: "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"}, diff --git a/architecture/layering/legacy_test.go b/architecture/layering/legacy_test.go index 09938ce1e..5be703116 100644 --- a/architecture/layering/legacy_test.go +++ b/architecture/layering/legacy_test.go @@ -25,7 +25,6 @@ var frozenLegacyPackages = []string{ "handlers/eks", "handlers/elbv2", "handlers/rds", - "handlers/sts", "lbagent", } diff --git a/docs/PACKAGE_BOUNDARY_MIGRATION.md b/docs/PACKAGE_BOUNDARY_MIGRATION.md index 07e0153eb..c44650d0c 100644 --- a/docs/PACKAGE_BOUNDARY_MIGRATION.md +++ b/docs/PACKAGE_BOUNDARY_MIGRATION.md @@ -341,6 +341,7 @@ while the branch is in flight; they do not describe the umbrella repository's | `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. | +| `spinifex#1250` | `spinifex/handlers/sts` | `spinifex/domains/sts` (`sts`, imported as `stsdomain`) | A mechanical relocation of STS's implementation to its ADR-0001 home; APIs, NATS subjects, KV buckets and behaviour are unchanged and only the package name changed from `handlers_sts`. The frozen-legacy entry is removed, and the allowlist entries for `domains/ec2/guestmetadata` → STS and STS → `domains/iam` now name `domains/sts`. | 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 @@ -464,7 +465,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: `domains/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. - `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,8 +652,8 @@ 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` | — | 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, `domains/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` | — | Done. The implementation moved to `domains/sts` (package `sts`) in `spinifex#1250` with no API, NATS subject, storage or behaviour change. ADR-0001's condition, cutting the EKS dependency, was already met through the injected `ClusterJWKSResolver`. `domains/sts` still imports `domains/iam` directly; per-consumer IAM capabilities remain dependency-rule debt, with its allowlist entry now sourced from `domains/sts`. | | `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. | diff --git a/spinifex/domains/ec2/guestmetadata/nats_clients.go b/spinifex/domains/ec2/guestmetadata/nats_clients.go index 8a3befaa4..148b68009 100644 --- a/spinifex/domains/ec2/guestmetadata/nats_clients.go +++ b/spinifex/domains/ec2/guestmetadata/nats_clients.go @@ -11,7 +11,7 @@ import ( "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_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/nats-io/nats.go" ) @@ -39,7 +39,7 @@ func NewNATSSTSAssumer(nc *nats.Conn) *NATSSTSAssumer { } func (a *NATSSTSAssumer) AssumeRoleForInstance(ctx context.Context, accountID, roleARN, instanceID string, durationSeconds int64) (*sts.AssumeRoleOutput, error) { - return natsmsg.NATSRequest[sts.AssumeRoleOutput](ctx, a.nc, handlers_sts.SubjectAssumeRoleForInstance, handlers_sts.AssumeRoleForInstanceRequest{ + return natsmsg.NATSRequest[sts.AssumeRoleOutput](ctx, a.nc, stsdomain.SubjectAssumeRoleForInstance, stsdomain.AssumeRoleForInstanceRequest{ AccountID: accountID, RoleARN: roleARN, InstanceID: instanceID, diff --git a/spinifex/domains/ec2/guestmetadata/nats_clients_test.go b/spinifex/domains/ec2/guestmetadata/nats_clients_test.go index fa793eb76..1ce9262a8 100644 --- a/spinifex/domains/ec2/guestmetadata/nats_clients_test.go +++ b/spinifex/domains/ec2/guestmetadata/nats_clients_test.go @@ -14,8 +14,8 @@ import ( "github.com/mulgadc/spinifex/internal/testkit" ec2key "github.com/mulgadc/spinifex/spinifex/domains/ec2/key" iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" "github.com/nats-io/nats.go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -41,9 +41,9 @@ func serveCounted[I any, O any](t *testing.T, nc *nats.Conn, subject string, fn func TestNATSSTSAssumer_RoundTrip(t *testing.T) { _, nc := testutil.StartTestNATS(t) - var gotReq handlers_sts.AssumeRoleForInstanceRequest - calls := serveCounted(t, nc, handlers_sts.SubjectAssumeRoleForInstance, - func(req *handlers_sts.AssumeRoleForInstanceRequest) (*sts.AssumeRoleOutput, error) { + var gotReq stsdomain.AssumeRoleForInstanceRequest + calls := serveCounted(t, nc, stsdomain.SubjectAssumeRoleForInstance, + func(req *stsdomain.AssumeRoleForInstanceRequest) (*sts.AssumeRoleOutput, error) { gotReq = *req return &sts.AssumeRoleOutput{Credentials: &sts.Credentials{ AccessKeyId: aws.String("ASIATESTKEY"), @@ -61,7 +61,7 @@ func TestNATSSTSAssumer_RoundTrip(t *testing.T) { assert.Equal(t, "ASIATESTKEY", aws.StringValue(out.Credentials.AccessKeyId)) assert.Equal(t, int32(1), atomic.LoadInt32(calls)) // The request payload must round-trip every field the responder needs. - assert.Equal(t, handlers_sts.AssumeRoleForInstanceRequest{ + assert.Equal(t, stsdomain.AssumeRoleForInstanceRequest{ AccountID: imdsTestAccountID, RoleARN: "arn:aws:iam::123456789012:role/app", InstanceID: "i-abc", diff --git a/spinifex/domains/iam/roles.go b/spinifex/domains/iam/roles.go index 996f16be1..101087182 100644 --- a/spinifex/domains/iam/roles.go +++ b/spinifex/domains/iam/roles.go @@ -738,12 +738,12 @@ func gotRoleToSDK(r *Role) *iam.Role { // STSActionAssumeRoleWithWebIdentity is one of the two actions that may carry a Condition block. const STSActionAssumeRoleWithWebIdentity = "sts:AssumeRoleWithWebIdentity" -// stsActionAssumeRole mirrors handlers_sts's unexported constant of the same value. -// Duplicated rather than imported: handlers_sts imports this package, not the reverse. +// stsActionAssumeRole mirrors domains/sts's unexported constant of the same value. +// Duplicated rather than imported: domains/sts imports this package, not the reverse. const stsActionAssumeRole = "sts:AssumeRole" // conditionKeySourceAccount is the only condition key accepted on the sts:AssumeRole -// trust-policy path. handlers_sts.assumeRoleConditionsHold is the evaluator that must +// trust-policy path. stsdomain.assumeRoleConditionsHold is the evaluator that must // accept exactly this key for this action — the two lists must never drift apart, or // a stored condition becomes a security control nothing enforces. const conditionKeySourceAccount = "aws:SourceAccount" @@ -828,7 +828,7 @@ func validateWebIdentityCondition(i int, stmt TrustStatement) error { return nil } - // action == stsActionAssumeRole: the evaluator (handlers_sts.assumeRoleConditionsHold) + // action == stsActionAssumeRole: the evaluator (stsdomain.assumeRoleConditionsHold) // enforces only aws:SourceAccount under StringEquals; name the offending // operator, key and action rather than a bare "malformed" refusal. for op, keys := range ops { diff --git a/spinifex/handlers/sts/assume_role.go b/spinifex/domains/sts/assume_role.go similarity index 99% rename from spinifex/handlers/sts/assume_role.go rename to spinifex/domains/sts/assume_role.go index e3c6d9b03..e3df2d77b 100644 --- a/spinifex/handlers/sts/assume_role.go +++ b/spinifex/domains/sts/assume_role.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "context" diff --git a/spinifex/handlers/sts/assume_role_test.go b/spinifex/domains/sts/assume_role_test.go similarity index 99% rename from spinifex/handlers/sts/assume_role_test.go rename to spinifex/domains/sts/assume_role_test.go index 59284fae0..12f82196c 100644 --- a/spinifex/handlers/sts/assume_role_test.go +++ b/spinifex/domains/sts/assume_role_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "crypto/hmac" diff --git a/spinifex/handlers/sts/assume_role_with_web_identity.go b/spinifex/domains/sts/assume_role_with_web_identity.go similarity index 99% rename from spinifex/handlers/sts/assume_role_with_web_identity.go rename to spinifex/domains/sts/assume_role_with_web_identity.go index 72c979099..eaa2c8c69 100644 --- a/spinifex/handlers/sts/assume_role_with_web_identity.go +++ b/spinifex/domains/sts/assume_role_with_web_identity.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "context" diff --git a/spinifex/handlers/sts/assume_role_with_web_identity_test.go b/spinifex/domains/sts/assume_role_with_web_identity_test.go similarity index 99% rename from spinifex/handlers/sts/assume_role_with_web_identity_test.go rename to spinifex/domains/sts/assume_role_with_web_identity_test.go index 8c499bf9c..2328c866a 100644 --- a/spinifex/handlers/sts/assume_role_with_web_identity_test.go +++ b/spinifex/domains/sts/assume_role_with_web_identity_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "crypto/ecdsa" diff --git a/spinifex/handlers/sts/fake_jwks_resolver_test.go b/spinifex/domains/sts/fake_jwks_resolver_test.go similarity index 99% rename from spinifex/handlers/sts/fake_jwks_resolver_test.go rename to spinifex/domains/sts/fake_jwks_resolver_test.go index 8d40c4f72..3426d8c27 100644 --- a/spinifex/handlers/sts/fake_jwks_resolver_test.go +++ b/spinifex/domains/sts/fake_jwks_resolver_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "context" diff --git a/spinifex/handlers/sts/get_access_key_info.go b/spinifex/domains/sts/get_access_key_info.go similarity index 99% rename from spinifex/handlers/sts/get_access_key_info.go rename to spinifex/domains/sts/get_access_key_info.go index e4e222e3d..b29616303 100644 --- a/spinifex/handlers/sts/get_access_key_info.go +++ b/spinifex/domains/sts/get_access_key_info.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "errors" diff --git a/spinifex/handlers/sts/get_access_key_info_test.go b/spinifex/domains/sts/get_access_key_info_test.go similarity index 99% rename from spinifex/handlers/sts/get_access_key_info_test.go rename to spinifex/domains/sts/get_access_key_info_test.go index bdf800198..efcc96867 100644 --- a/spinifex/handlers/sts/get_access_key_info_test.go +++ b/spinifex/domains/sts/get_access_key_info_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "errors" diff --git a/spinifex/handlers/sts/get_caller_identity.go b/spinifex/domains/sts/get_caller_identity.go similarity index 96% rename from spinifex/handlers/sts/get_caller_identity.go rename to spinifex/domains/sts/get_caller_identity.go index 64fc148bc..0ca0140b5 100644 --- a/spinifex/handlers/sts/get_caller_identity.go +++ b/spinifex/domains/sts/get_caller_identity.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "github.com/aws/aws-sdk-go/aws" diff --git a/spinifex/handlers/sts/get_caller_identity_test.go b/spinifex/domains/sts/get_caller_identity_test.go similarity index 99% rename from spinifex/handlers/sts/get_caller_identity_test.go rename to spinifex/domains/sts/get_caller_identity_test.go index 6aba94177..cab8b645b 100644 --- a/spinifex/handlers/sts/get_caller_identity_test.go +++ b/spinifex/domains/sts/get_caller_identity_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "testing" diff --git a/spinifex/handlers/sts/get_session_token.go b/spinifex/domains/sts/get_session_token.go similarity index 99% rename from spinifex/handlers/sts/get_session_token.go rename to spinifex/domains/sts/get_session_token.go index 26c120712..a8a539e55 100644 --- a/spinifex/handlers/sts/get_session_token.go +++ b/spinifex/domains/sts/get_session_token.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "context" diff --git a/spinifex/handlers/sts/get_session_token_test.go b/spinifex/domains/sts/get_session_token_test.go similarity index 99% rename from spinifex/handlers/sts/get_session_token_test.go rename to spinifex/domains/sts/get_session_token_test.go index 7dcaa55d2..455373f04 100644 --- a/spinifex/handlers/sts/get_session_token_test.go +++ b/spinifex/domains/sts/get_session_token_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "crypto/hmac" diff --git a/spinifex/handlers/sts/imds_rpc.go b/spinifex/domains/sts/imds_rpc.go similarity index 98% rename from spinifex/handlers/sts/imds_rpc.go rename to spinifex/domains/sts/imds_rpc.go index 270deb55b..80f214bc2 100644 --- a/spinifex/handlers/sts/imds_rpc.go +++ b/spinifex/domains/sts/imds_rpc.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "github.com/aws/aws-sdk-go/service/sts" diff --git a/spinifex/handlers/sts/invariants_test.go b/spinifex/domains/sts/invariants_test.go similarity index 99% rename from spinifex/handlers/sts/invariants_test.go rename to spinifex/domains/sts/invariants_test.go index b5f17545b..336d8914c 100644 --- a/spinifex/handlers/sts/invariants_test.go +++ b/spinifex/domains/sts/invariants_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "testing" diff --git a/spinifex/handlers/sts/oidc_jwks.go b/spinifex/domains/sts/oidc_jwks.go similarity index 99% rename from spinifex/handlers/sts/oidc_jwks.go rename to spinifex/domains/sts/oidc_jwks.go index 842299bcb..81de3f26f 100644 --- a/spinifex/handlers/sts/oidc_jwks.go +++ b/spinifex/domains/sts/oidc_jwks.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "context" diff --git a/spinifex/handlers/sts/oidc_jwks_current_behaviour_test.go b/spinifex/domains/sts/oidc_jwks_current_behaviour_test.go similarity index 99% rename from spinifex/handlers/sts/oidc_jwks_current_behaviour_test.go rename to spinifex/domains/sts/oidc_jwks_current_behaviour_test.go index 092123ea7..5441e09f8 100644 --- a/spinifex/handlers/sts/oidc_jwks_current_behaviour_test.go +++ b/spinifex/domains/sts/oidc_jwks_current_behaviour_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "context" diff --git a/spinifex/handlers/sts/oidc_jwks_test.go b/spinifex/domains/sts/oidc_jwks_test.go similarity index 99% rename from spinifex/handlers/sts/oidc_jwks_test.go rename to spinifex/domains/sts/oidc_jwks_test.go index c9d2611d2..f4b951d4b 100644 --- a/spinifex/handlers/sts/oidc_jwks_test.go +++ b/spinifex/domains/sts/oidc_jwks_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "encoding/json" diff --git a/spinifex/handlers/sts/presigned_url_verify.go b/spinifex/domains/sts/presigned_url_verify.go similarity index 99% rename from spinifex/handlers/sts/presigned_url_verify.go rename to spinifex/domains/sts/presigned_url_verify.go index d890f37a4..315561e42 100644 --- a/spinifex/handlers/sts/presigned_url_verify.go +++ b/spinifex/domains/sts/presigned_url_verify.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "errors" diff --git a/spinifex/handlers/sts/presigned_url_verify_test.go b/spinifex/domains/sts/presigned_url_verify_test.go similarity index 99% rename from spinifex/handlers/sts/presigned_url_verify_test.go rename to spinifex/domains/sts/presigned_url_verify_test.go index 38fae0345..3f2f1942f 100644 --- a/spinifex/handlers/sts/presigned_url_verify_test.go +++ b/spinifex/domains/sts/presigned_url_verify_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "errors" diff --git a/spinifex/handlers/sts/role_arn.go b/spinifex/domains/sts/role_arn.go similarity index 98% rename from spinifex/handlers/sts/role_arn.go rename to spinifex/domains/sts/role_arn.go index 8a7ba66a4..18676a22a 100644 --- a/spinifex/handlers/sts/role_arn.go +++ b/spinifex/domains/sts/role_arn.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "errors" diff --git a/spinifex/handlers/sts/role_arn_test.go b/spinifex/domains/sts/role_arn_test.go similarity index 70% rename from spinifex/handlers/sts/role_arn_test.go rename to spinifex/domains/sts/role_arn_test.go index 7160180ce..2ef78d1e4 100644 --- a/spinifex/handlers/sts/role_arn_test.go +++ b/spinifex/domains/sts/role_arn_test.go @@ -1,4 +1,4 @@ -package handlers_sts_test +package sts_test import ( "errors" @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" ) // roleGetterStub answers GetRole from a name-to-stored-ARN map, the way IAM @@ -36,38 +36,38 @@ func TestResolveRoleByARN(t *testing.T) { } t.Run("stored ARN resolves", func(t *testing.T) { - gotAccount, role, err := handlers_sts.ResolveRoleByARN(roles, "arn:aws:iam::000000000000:role/admin") + gotAccount, role, err := stsdomain.ResolveRoleByARN(roles, "arn:aws:iam::000000000000:role/admin") require.NoError(t, err) assert.Equal(t, account, gotAccount) assert.Equal(t, "arn:aws:iam::000000000000:role/admin", aws.StringValue(role.Arn)) }) t.Run("a pathed role resolves by its full stored ARN", func(t *testing.T) { - _, role, err := handlers_sts.ResolveRoleByARN(roles, "arn:aws:iam::000000000000:role/team/app-worker") + _, role, err := stsdomain.ResolveRoleByARN(roles, "arn:aws:iam::000000000000:role/team/app-worker") require.NoError(t, err) assert.Equal(t, "arn:aws:iam::000000000000:role/team/app-worker", aws.StringValue(role.Arn)) }) t.Run("an invented path does not reach the role it names", func(t *testing.T) { // `app-x/admin` looks up `admin`, whose stored ARN carries no such path. - _, _, err := handlers_sts.ResolveRoleByARN(roles, "arn:aws:iam::000000000000:role/app-x/admin") - require.ErrorIs(t, err, handlers_sts.ErrRoleUnresolved) + _, _, err := stsdomain.ResolveRoleByARN(roles, "arn:aws:iam::000000000000:role/app-x/admin") + require.ErrorIs(t, err, stsdomain.ErrRoleUnresolved) }) t.Run("a pathed role is not reachable by its pathless ARN", func(t *testing.T) { - _, _, err := handlers_sts.ResolveRoleByARN(roles, "arn:aws:iam::000000000000:role/app-worker") - require.ErrorIs(t, err, handlers_sts.ErrRoleUnresolved) + _, _, err := stsdomain.ResolveRoleByARN(roles, "arn:aws:iam::000000000000:role/app-worker") + require.ErrorIs(t, err, stsdomain.ErrRoleUnresolved) }) t.Run("a role that does not exist is unresolved", func(t *testing.T) { - _, _, err := handlers_sts.ResolveRoleByARN(roles, "arn:aws:iam::000000000000:role/nobody") - require.ErrorIs(t, err, handlers_sts.ErrRoleUnresolved) + _, _, err := stsdomain.ResolveRoleByARN(roles, "arn:aws:iam::000000000000:role/nobody") + require.ErrorIs(t, err, stsdomain.ErrRoleUnresolved) }) t.Run("a malformed ARN is a validation error", func(t *testing.T) { - _, _, err := handlers_sts.ResolveRoleByARN(roles, "not-an-arn") + _, _, err := stsdomain.ResolveRoleByARN(roles, "not-an-arn") require.Error(t, err) - assert.NotErrorIs(t, err, handlers_sts.ErrRoleUnresolved) + assert.NotErrorIs(t, err, stsdomain.ErrRoleUnresolved) code, ok := awserrors.ResolveErrorCode(err) require.True(t, ok) assert.Equal(t, awserrors.ErrorValidationError, code) diff --git a/spinifex/handlers/sts/service.go b/spinifex/domains/sts/service.go similarity index 97% rename from spinifex/handlers/sts/service.go rename to spinifex/domains/sts/service.go index 88af8092e..462a75b58 100644 --- a/spinifex/handlers/sts/service.go +++ b/spinifex/domains/sts/service.go @@ -1,7 +1,7 @@ -// Package handlers_sts implements STS for the gateway: role assumption, web +// Package sts implements STS for the gateway: role assumption, web // identity federation and session tokens, with session credentials persisted in // JetStream KV under the master key shared with IAM. -package handlers_sts +package sts import ( "github.com/aws/aws-sdk-go/service/sts" diff --git a/spinifex/handlers/sts/service_impl.go b/spinifex/domains/sts/service_impl.go similarity index 99% rename from spinifex/handlers/sts/service_impl.go rename to spinifex/domains/sts/service_impl.go index 9e596be42..e177fe680 100644 --- a/spinifex/handlers/sts/service_impl.go +++ b/spinifex/domains/sts/service_impl.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "context" diff --git a/spinifex/handlers/sts/service_impl_test.go b/spinifex/domains/sts/service_impl_test.go similarity index 99% rename from spinifex/handlers/sts/service_impl_test.go rename to spinifex/domains/sts/service_impl_test.go index 839a49768..12f78e51b 100644 --- a/spinifex/handlers/sts/service_impl_test.go +++ b/spinifex/domains/sts/service_impl_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "bytes" diff --git a/spinifex/handlers/sts/session_credentials.go b/spinifex/domains/sts/session_credentials.go similarity index 99% rename from spinifex/handlers/sts/session_credentials.go rename to spinifex/domains/sts/session_credentials.go index fa8985544..2e384aec7 100644 --- a/spinifex/handlers/sts/session_credentials.go +++ b/spinifex/domains/sts/session_credentials.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "context" diff --git a/spinifex/handlers/sts/session_credentials_test.go b/spinifex/domains/sts/session_credentials_test.go similarity index 99% rename from spinifex/handlers/sts/session_credentials_test.go rename to spinifex/domains/sts/session_credentials_test.go index 7f26200f0..69f3ba00a 100644 --- a/spinifex/handlers/sts/session_credentials_test.go +++ b/spinifex/domains/sts/session_credentials_test.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "context" diff --git a/spinifex/handlers/sts/session_principal.go b/spinifex/domains/sts/session_principal.go similarity index 99% rename from spinifex/handlers/sts/session_principal.go rename to spinifex/domains/sts/session_principal.go index fda699c02..6e0466e75 100644 --- a/spinifex/handlers/sts/session_principal.go +++ b/spinifex/domains/sts/session_principal.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "errors" diff --git a/spinifex/handlers/sts/session_principal_test.go b/spinifex/domains/sts/session_principal_test.go similarity index 99% rename from spinifex/handlers/sts/session_principal_test.go rename to spinifex/domains/sts/session_principal_test.go index 033b82b69..1947ae9a1 100644 --- a/spinifex/handlers/sts/session_principal_test.go +++ b/spinifex/domains/sts/session_principal_test.go @@ -2,7 +2,7 @@ // and the credential fixtures set unexported principal-type constants. // //test:in-package — the verdicts are asserted against the real IAM backend -package handlers_sts +package sts import ( "errors" diff --git a/spinifex/handlers/sts/validation.go b/spinifex/domains/sts/validation.go similarity index 99% rename from spinifex/handlers/sts/validation.go rename to spinifex/domains/sts/validation.go index 51f8b26dd..35cb6048c 100644 --- a/spinifex/handlers/sts/validation.go +++ b/spinifex/domains/sts/validation.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "fmt" diff --git a/spinifex/handlers/sts/web_identity_trust_policy.go b/spinifex/domains/sts/web_identity_trust_policy.go similarity index 99% rename from spinifex/handlers/sts/web_identity_trust_policy.go rename to spinifex/domains/sts/web_identity_trust_policy.go index d2240daca..2b4e804a1 100644 --- a/spinifex/handlers/sts/web_identity_trust_policy.go +++ b/spinifex/domains/sts/web_identity_trust_policy.go @@ -1,4 +1,4 @@ -package handlers_sts +package sts import ( "crypto/subtle" diff --git a/spinifex/gateway/auth.go b/spinifex/gateway/auth.go index 9658a79f5..1ba458aaa 100644 --- a/spinifex/gateway/auth.go +++ b/spinifex/gateway/auth.go @@ -14,9 +14,9 @@ import ( "github.com/mulgadc/bluebottle/pkg/sigv4" iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" telemetry "github.com/mulgadc/spinifex/spinifex/foundation/telemetry" - 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" ingresshttp "github.com/mulgadc/spinifex/spinifex/ingress/http" @@ -368,7 +368,7 @@ type principalContext struct { userARN string // sessionCred is the rehydrated STS record on the ASIA path, nil on the // long-lived one. It carries the continuity check past sig.Verify. - sessionCred *handlers_sts.SessionCredential + sessionCred *stsdomain.SessionCredential } // resolveLongLivedAKID handles the AKIA path: IAM lookup, status check, secret decrypt. @@ -490,7 +490,7 @@ func (gw *GatewayConfig) checkSessionPrincipal(principal principalContext, acces } if _, err := gw.STSService.VerifySessionPrincipal(principal.sessionCred); err != nil { - if !handlers_sts.IsSessionPrincipalVerdict(err) { + if !stsdomain.IsSessionPrincipalVerdict(err) { slog.Error("Session principal continuity check faulted, refusing to authorize", "accessKeyID", accessKeyID, "err", err) return awserrors.ErrorInternalError @@ -504,7 +504,7 @@ func (gw *GatewayConfig) checkSessionPrincipal(principal principalContext, acces // Every legacy record fails at once on the deploy that ships this check, so // counting them would lock out a shared egress address on distinct-attempt // volume alone and answer "retry later" to a fleet that cannot act on it. - if !errors.Is(err, handlers_sts.ErrSessionPrincipalLegacy) { + if !errors.Is(err, stsdomain.ErrSessionPrincipalLegacy) { gw.RateLimiter.RecordFailure(src.limitKey, authlimit.Fingerprint("session-principal", accessKeyID)) } return awserrors.ErrorInvalidClientTokenId diff --git a/spinifex/gateway/auth_current_behaviour_test.go b/spinifex/gateway/auth_current_behaviour_test.go index 8358d5eef..70ad405f7 100644 --- a/spinifex/gateway/auth_current_behaviour_test.go +++ b/spinifex/gateway/auth_current_behaviour_test.go @@ -29,11 +29,11 @@ import ( "github.com/mulgadc/spinifex/internal/testkit" iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" 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_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" ) // The TestCurrentBehaviour_ tests in this file pin today's SigV4 authentication, @@ -80,7 +80,7 @@ type cbEnv struct { iamNC *nats.Conn // IAM/STS connection; == nc unless separateIAMConn js jetstream.JetStream iam *iamdomain.IAMServiceImpl - sts *handlers_sts.STSServiceImpl + sts *stsdomain.STSServiceImpl counter *cbDecryptCounter h http.Handler accountID string @@ -105,7 +105,7 @@ func newCBEnv(t *testing.T, opts cbEnvOpts) *cbEnv { e.iam, err = iamdomain.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)) + e.sts, err = stsdomain.NewSTSServiceImpl(t.Context(), e.iamNC, e.iam, masterKey, handlers_eks.NewOIDCJWKSReader(iamJS)) require.NoError(t, err) if !opts.noRevoker { e.iam.SetSessionRevoker(e.sts) @@ -268,11 +268,11 @@ func (e *cbEnv) getSessionToken(t *testing.T, c cbCreds) cbCreds { // expireSession rewrites a stored session record's expiry into the past. func (e *cbEnv) expireSession(t *testing.T, akid string) { t.Helper() - kv, err := e.js.KeyValue(t.Context(), handlers_sts.KVBucketSessionCredentials) + kv, err := e.js.KeyValue(t.Context(), stsdomain.KVBucketSessionCredentials) require.NoError(t, err) entry, err := kv.Get(t.Context(), akid) require.NoError(t, err) - var cred handlers_sts.SessionCredential + var cred stsdomain.SessionCredential require.NoError(t, json.Unmarshal(entry.Value(), &cred)) cred.ExpiresAt = time.Now().UTC().Add(-time.Minute) data, err := json.Marshal(cred) @@ -495,7 +495,7 @@ func TestCurrentBehaviour_Session_WebIdentityCredentials(t *testing.T) { require.NoError(t, err) point, err := priv.PublicKey.Bytes() require.NoError(t, err) - jwks, err := json.Marshal(handlers_sts.JWKS{Keys: []handlers_sts.JWK{{ + jwks, err := json.Marshal(stsdomain.JWKS{Keys: []stsdomain.JWK{{ Kty: "EC", Kid: "k1", Alg: "ES256", Use: "sig", Crv: "P-256", X: base64.RawURLEncoding.EncodeToString(point[1:33]), Y: base64.RawURLEncoding.EncodeToString(point[33:65]), @@ -754,7 +754,7 @@ func TestCurrentBehaviour_Auth_BackingStateUnavailable(t *testing.T) { e := newCBEnv(t, cbEnvOpts{}) roleARN, _ := e.createAssumableRole(t, "app") s := e.assumeRole(t, roleARN, "s1") - require.NoError(t, e.js.DeleteKeyValue(t.Context(), handlers_sts.KVBucketSessionCredentials)) + require.NoError(t, e.js.DeleteKeyValue(t.Context(), stsdomain.KVBucketSessionCredentials)) assert.Equal(t, internal, cbOutcomeOf(e.callerIdentity(t, s))) }) diff --git a/spinifex/gateway/auth_test.go b/spinifex/gateway/auth_test.go index 891eab0cc..6702d0902 100644 --- a/spinifex/gateway/auth_test.go +++ b/spinifex/gateway/auth_test.go @@ -31,8 +31,8 @@ import ( "github.com/mulgadc/bluebottle/pkg/sigv4" "github.com/mulgadc/spinifex/internal/testkit" iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" "github.com/nats-io/nats.go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1642,15 +1642,15 @@ func TestSigV4Auth_NATSDisconnectedShortCircuit(t *testing.T) { // --- Session credential (ASIA) auth tests --- -// mockSTSService implements handlers_sts.STSService for auth-middleware tests. +// mockSTSService implements stsdomain.STSService for auth-middleware tests. // It embeds the interface so it satisfies the full contract; only // LookupSessionCredential and VerifySessionToken are exercised, and any other // method nil-panics so a misrouted call fails loudly instead of silently // allowing. type mockSTSService struct { - handlers_sts.STSService + stsdomain.STSService - sessions map[string]*handlers_sts.SessionCredential + sessions map[string]*stsdomain.SessionCredential tokens map[string]string // AKID → plaintext wire token for HMAC equivalence lookupErr error lookups atomic.Int32 // counts LookupSessionCredential calls for negative-side-effect assertions @@ -1659,7 +1659,7 @@ type mockSTSService struct { principalChecks atomic.Int32 // counts VerifySessionPrincipal calls, to pin its ordering } -func (m *mockSTSService) LookupSessionCredential(accessKeyID string) (*handlers_sts.SessionCredential, error) { +func (m *mockSTSService) LookupSessionCredential(accessKeyID string) (*stsdomain.SessionCredential, error) { m.lookups.Add(1) if m.lookupErr != nil { return nil, m.lookupErr @@ -1676,7 +1676,7 @@ func (m *mockSTSService) LookupSessionCredential(accessKeyID string) (*handlers_ // VerifySessionToken uses plaintext equality rather than HMAC to keep tests // simple while preserving the contract shape. -func (m *mockSTSService) VerifySessionToken(cred *handlers_sts.SessionCredential, wireToken string) bool { +func (m *mockSTSService) VerifySessionToken(cred *stsdomain.SessionCredential, wireToken string) bool { if cred == nil { return false } @@ -1689,7 +1689,7 @@ func (m *mockSTSService) VerifySessionToken(cred *handlers_sts.SessionCredential // VerifySessionPrincipal defaults to "still the same principal"; principalErr // drives the rejection-verdict and dependency-fault branches. -func (m *mockSTSService) VerifySessionPrincipal(cred *handlers_sts.SessionCredential) (*handlers_sts.SessionPrincipal, error) { +func (m *mockSTSService) VerifySessionPrincipal(cred *stsdomain.SessionCredential) (*stsdomain.SessionPrincipal, error) { m.principalChecks.Add(1) if m.principalErr != nil { return nil, m.principalErr @@ -1701,7 +1701,7 @@ func (m *mockSTSService) VerifySessionPrincipal(cred *handlers_sts.SessionCreden // package doubles. It derives RoleName from the underlying role ARN rather than // the session name, because that is what the real implementation returns: a // stub echoing SessionName would let a door read the wrong field and stay green. -func stubSessionPrincipal(cred *handlers_sts.SessionCredential) (*handlers_sts.SessionPrincipal, error) { +func stubSessionPrincipal(cred *stsdomain.SessionCredential) (*stsdomain.SessionPrincipal, error) { if cred == nil { return nil, errors.New("nil session credential") } @@ -1709,7 +1709,7 @@ func stubSessionPrincipal(cred *handlers_sts.SessionCredential) (*handlers_sts.S if idx := strings.LastIndex(roleName, "/"); idx >= 0 { roleName = roleName[idx+1:] } - return &handlers_sts.SessionPrincipal{ + return &stsdomain.SessionPrincipal{ UserID: cred.UserID, RoleID: cred.RoleID, RoleName: roleName, @@ -1731,7 +1731,7 @@ func setupSessionTestApp(t *testing.T, expiresAt time.Time) (http.Handler, *mock encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey) require.NoError(t, err) - cred := &handlers_sts.SessionCredential{ + cred := &stsdomain.SessionCredential{ AccessKeyID: testSessionAKID, SecretEncrypted: encryptedSecret, SessionTokenHMAC: "ignored-in-mock", // mock verifies by plaintext map @@ -1750,13 +1750,13 @@ func setupSessionTestApp(t *testing.T, expiresAt time.Time) (http.Handler, *mock // mountSessionGateway wires SigV4 middleware over a handler that echoes the // principal context; varies cred to drive the principal-type branch in resolveSessionAKID. -func mountSessionGateway(t *testing.T, cred *handlers_sts.SessionCredential) (http.Handler, *mockSTSService) { +func mountSessionGateway(t *testing.T, cred *stsdomain.SessionCredential) (http.Handler, *mockSTSService) { t.Helper() encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey) require.NoError(t, err) stsMock := &mockSTSService{ - sessions: map[string]*handlers_sts.SessionCredential{cred.AccessKeyID: cred}, + sessions: map[string]*stsdomain.SessionCredential{cred.AccessKeyID: cred}, tokens: map[string]string{cred.AccessKeyID: testSessionToken}, } @@ -1826,7 +1826,7 @@ func TestSigV4Auth_Session_UserPrincipal(t *testing.T) { // user with no assumed-role fields, so buildCallerARN yields arn:aws:iam::A:user/N. encryptedSecret, err := iamdomain.EncryptSecret(testSecretKey, testMasterKey) require.NoError(t, err) - cred := &handlers_sts.SessionCredential{ + cred := &stsdomain.SessionCredential{ AccessKeyID: testSessionAKID, SecretEncrypted: encryptedSecret, SessionTokenHMAC: "ignored-in-mock", @@ -1858,7 +1858,7 @@ 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) require.NoError(t, err) - cred := &handlers_sts.SessionCredential{ + cred := &stsdomain.SessionCredential{ AccessKeyID: testSessionAKID, SecretEncrypted: encryptedSecret, SessionTokenHMAC: "ignored-in-mock", @@ -1954,9 +1954,9 @@ func TestSigV4Auth_Session_StalePrincipalRejected(t *testing.T) { name string err error }{ - {"gone", handlers_sts.ErrSessionPrincipalGone}, - {"replaced", handlers_sts.ErrSessionPrincipalReplaced}, - {"legacy", handlers_sts.ErrSessionPrincipalLegacy}, + {"gone", stsdomain.ErrSessionPrincipalGone}, + {"replaced", stsdomain.ErrSessionPrincipalReplaced}, + {"legacy", stsdomain.ErrSessionPrincipalLegacy}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -1998,7 +1998,7 @@ func TestSigV4Auth_Session_PrincipalCheckFault_IsInternalError(t *testing.T) { // sig.Verify, so a bad signature is answered without the check running at all. func TestSigV4Auth_Session_StalePrincipal_BadSignatureWinsFirst(t *testing.T) { handler, stsMock := setupSessionTestApp(t, time.Now().UTC().Add(time.Hour)) - stsMock.principalErr = handlers_sts.ErrSessionPrincipalReplaced + stsMock.principalErr = stsdomain.ErrSessionPrincipalReplaced req := httptest.NewRequest(http.MethodGet, "/", nil) req.Host = "localhost:9999" @@ -2041,9 +2041,9 @@ func TestSigV4Auth_SessionPrincipalVerdict_RateLimitRecording(t *testing.T) { err error recorded bool }{ - {"gone", handlers_sts.ErrSessionPrincipalGone, true}, - {"replaced", handlers_sts.ErrSessionPrincipalReplaced, true}, - {"legacy", handlers_sts.ErrSessionPrincipalLegacy, false}, + {"gone", stsdomain.ErrSessionPrincipalGone, true}, + {"replaced", stsdomain.ErrSessionPrincipalReplaced, true}, + {"legacy", stsdomain.ErrSessionPrincipalLegacy, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -2057,7 +2057,7 @@ func TestSigV4Auth_SessionPrincipalVerdict_RateLimitRecording(t *testing.T) { principal := principalContext{ identity: "test-session", principalType: principalTypeAssumedRole, - sessionCred: &handlers_sts.SessionCredential{AccessKeyID: testSessionAKID}, + sessionCred: &stsdomain.SessionCredential{AccessKeyID: testSessionAKID}, } const ip = "10.15.8.11" @@ -2215,7 +2215,7 @@ func TestCheckPolicy_SessionNameCollision(t *testing.T) { }, } - cred := &handlers_sts.SessionCredential{ + cred := &stsdomain.SessionCredential{ AccessKeyID: testSessionAKID, SecretEncrypted: encryptedSecret, AccountID: "123456789012", @@ -2227,7 +2227,7 @@ func TestCheckPolicy_SessionNameCollision(t *testing.T) { CreatedAt: time.Now().UTC().Add(-time.Minute), } stsMock := &mockSTSService{ - sessions: map[string]*handlers_sts.SessionCredential{testSessionAKID: cred}, + sessions: map[string]*stsdomain.SessionCredential{testSessionAKID: cred}, tokens: map[string]string{testSessionAKID: testSessionToken}, } @@ -2272,8 +2272,8 @@ func allowPolicyResource(action, resource string) []iamdomain.PolicyDocument { // assumedRoleSessionCred builds an assumed-role SessionCredential. // SecretEncrypted is filled in by the gateway builder. -func assumedRoleSessionCred(sessionName, underlyingRoleARN, accountID string) *handlers_sts.SessionCredential { - return &handlers_sts.SessionCredential{ +func assumedRoleSessionCred(sessionName, underlyingRoleARN, accountID string) *stsdomain.SessionCredential { + return &stsdomain.SessionCredential{ AccessKeyID: testSessionAKID, AccountID: accountID, PrincipalType: principalTypeAssumedRole, @@ -2287,7 +2287,7 @@ 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, +func newAssumedRoleEnforcementGateway(t *testing.T, cred *stsdomain.SessionCredential, getRoleFn func(accountID, roleName string) ([]iamdomain.PolicyDocument, error), ) *GatewayConfig { t.Helper() @@ -2304,7 +2304,7 @@ func newAssumedRoleEnforcementGateway(t *testing.T, cred *handlers_sts.SessionCr getRolePoliciesFn: getRoleFn, } stsMock := &mockSTSService{ - sessions: map[string]*handlers_sts.SessionCredential{cred.AccessKeyID: cred}, + sessions: map[string]*stsdomain.SessionCredential{cred.AccessKeyID: cred}, tokens: map[string]string{cred.AccessKeyID: testSessionToken}, } return &GatewayConfig{ diff --git a/spinifex/gateway/ecr_principal.go b/spinifex/gateway/ecr_principal.go index 7867b7453..6eee29dd8 100644 --- a/spinifex/gateway/ecr_principal.go +++ b/spinifex/gateway/ecr_principal.go @@ -10,8 +10,8 @@ import ( "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" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" ) // An ECR registry token is a signed pointer to an IAM/STS record, not a @@ -176,7 +176,7 @@ func (gw *GatewayConfig) resolveECRSessionPrincipal(claims *ecrauth.Claims) (pri // supplies the resolved record the branches below would otherwise re-read. live, err := gw.STSService.VerifySessionPrincipal(cred) if err != nil { - if handlers_sts.IsSessionPrincipalVerdict(err) { + if stsdomain.IsSessionPrincipalVerdict(err) { return principalContext{}, ecrInvalidPrincipal("session principal is no longer valid: %w", err) } return principalContext{}, ecrDependencyFailure("session principal check failed: %w", err) diff --git a/spinifex/gateway/ecr_principal_test.go b/spinifex/gateway/ecr_principal_test.go index 5e94006d0..217d5ce5b 100644 --- a/spinifex/gateway/ecr_principal_test.go +++ b/spinifex/gateway/ecr_principal_test.go @@ -11,8 +11,8 @@ import ( "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" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -107,17 +107,17 @@ func (m *ecrMockIAMService) GetRolePolicies(accountID, roleName string) ([]iamdo return m.rolePolicies[accountID+"|"+roleName], nil } -// ecrMockSTSService implements handlers_sts.STSService for ECR principal +// ecrMockSTSService implements stsdomain.STSService for ECR principal // rehydration tests. Only LookupSessionCredential and VerifySessionPrincipal // are wired. type ecrMockSTSService struct { - handlers_sts.STSService + stsdomain.STSService - sessions map[string]*handlers_sts.SessionCredential + sessions map[string]*stsdomain.SessionCredential // principalErr is the continuity verdict (or dependency fault) the check // returns. Detection itself is exercised against real IAM/STS services in - // handlers/sts; what these tests pin is how this door maps the answer. + // domains/sts; what these tests pin is how this door maps the answer. principalErr error // liveUserARN is the stored ARN the continuity read returns for a user session. @@ -125,14 +125,14 @@ type ecrMockSTSService struct { } func newECRMockSTSService() *ecrMockSTSService { - return &ecrMockSTSService{sessions: map[string]*handlers_sts.SessionCredential{}} + return &ecrMockSTSService{sessions: map[string]*stsdomain.SessionCredential{}} } -func (m *ecrMockSTSService) LookupSessionCredential(accessKeyID string) (*handlers_sts.SessionCredential, error) { +func (m *ecrMockSTSService) LookupSessionCredential(accessKeyID string) (*stsdomain.SessionCredential, error) { return m.sessions[accessKeyID], nil } -func (m *ecrMockSTSService) VerifySessionPrincipal(cred *handlers_sts.SessionCredential) (*handlers_sts.SessionPrincipal, error) { +func (m *ecrMockSTSService) VerifySessionPrincipal(cred *stsdomain.SessionCredential) (*stsdomain.SessionPrincipal, error) { if m.principalErr != nil { return nil, m.principalErr } @@ -341,7 +341,7 @@ func seedECRSessionUser(iamSvc *ecrMockIAMService, stsSvc *ecrMockSTSService, ac Arn: aws.String("arn:aws:iam::" + accountID + ":user/" + userName), } iamSvc.accounts[accountID] = &iamdomain.Account{AccountID: accountID, Status: iamdomain.AccountStatusActive} - stsSvc.sessions[asid] = &handlers_sts.SessionCredential{ + stsSvc.sessions[asid] = &stsdomain.SessionCredential{ AccessKeyID: asid, AccountID: accountID, PrincipalType: principalTypeUser, @@ -400,9 +400,9 @@ func TestResolveECRPrincipal_SessionToken_User_StalePrincipalRejected(t *testing name string err error }{ - {"user deleted", handlers_sts.ErrSessionPrincipalGone}, - {"user recreated same name", handlers_sts.ErrSessionPrincipalReplaced}, - {"legacy record with no immutable UserID", handlers_sts.ErrSessionPrincipalLegacy}, + {"user deleted", stsdomain.ErrSessionPrincipalGone}, + {"user recreated same name", stsdomain.ErrSessionPrincipalReplaced}, + {"legacy record with no immutable UserID", stsdomain.ErrSessionPrincipalLegacy}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -469,7 +469,7 @@ func seedECRAssumedRole(iamSvc *ecrMockIAMService, stsSvc *ecrMockSTSService, ac // underlying role (resolved via GetRole) stays a role/ ARN, matching the // production shape. assumedRoleARN := "arn:aws:sts::" + accountID + ":assumed-role/" + roleName + "/" + sessionName - stsSvc.sessions[asid] = &handlers_sts.SessionCredential{ + stsSvc.sessions[asid] = &stsdomain.SessionCredential{ AccessKeyID: asid, AccountID: accountID, PrincipalType: principalTypeAssumedRole, @@ -538,9 +538,9 @@ func TestResolveECRPrincipal_AssumedRole_Rejections(t *testing.T) { // client to retry a credential that will never work again. t.Run("continuity verdicts are invalid-principal, not dependency failures", func(t *testing.T) { verdicts := map[string]error{ - "role deleted": handlers_sts.ErrSessionPrincipalGone, - "role replaced (RoleId changed)": handlers_sts.ErrSessionPrincipalReplaced, - "legacy record with no immutable RoleID": handlers_sts.ErrSessionPrincipalLegacy, + "role deleted": stsdomain.ErrSessionPrincipalGone, + "role replaced (RoleId changed)": stsdomain.ErrSessionPrincipalReplaced, + "legacy record with no immutable RoleID": stsdomain.ErrSessionPrincipalLegacy, } for name, verdict := range verdicts { t.Run(name, func(t *testing.T) { diff --git a/spinifex/gateway/gateway.go b/spinifex/gateway/gateway.go index 6d2c5ea97..55212d1a6 100644 --- a/spinifex/gateway/gateway.go +++ b/spinifex/gateway/gateway.go @@ -35,13 +35,13 @@ import ( 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" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/domains/tagging" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" "github.com/mulgadc/spinifex/spinifex/foundation/aws/policy" "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_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" "github.com/mulgadc/spinifex/spinifex/ingress/aws/dispatch" "github.com/mulgadc/spinifex/spinifex/ingress/aws/envelope" ingresshttp "github.com/mulgadc/spinifex/spinifex/ingress/http" @@ -153,7 +153,7 @@ type GatewayConfig struct { // trusts to reach any bucket, so this grants enumeration, not access. // Nil makes DeleteAccount refuse rather than tear down around the data. BucketStore accountteardown.BucketStore - STSService handlers_sts.STSService + STSService stsdomain.STSService RateLimiter *authlimit.AuthRateLimiter // Per-IP auth failure rate limiter Throttler *ratelimit.Throttler // Per-account+action API request throttler // accountStatus caches which accounts are ACTIVE, so enforcing account diff --git a/spinifex/gateway/oidc_discovery_current_behaviour_test.go b/spinifex/gateway/oidc_discovery_current_behaviour_test.go index f880390fe..5cc887e56 100644 --- a/spinifex/gateway/oidc_discovery_current_behaviour_test.go +++ b/spinifex/gateway/oidc_discovery_current_behaviour_test.go @@ -21,9 +21,9 @@ import ( "github.com/mulgadc/spinifex/internal/testkit" iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil" handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" ) // The TestCurrentBehaviour_ tests in this file pin today's public OIDC discovery @@ -178,7 +178,7 @@ func TestCurrentBehaviour_WebIdentityJWKSMissingWireResponse(t *testing.T) { require.NoError(t, err) iamSvc, err := iamdomain.NewIAMServiceImpl(t.Context(), nc, masterKey) require.NoError(t, err) - stsSvc, err := handlers_sts.NewSTSServiceImpl(t.Context(), nc, iamSvc, masterKey, handlers_eks.NewOIDCJWKSReader(js)) + stsSvc, err := stsdomain.NewSTSServiceImpl(t.Context(), nc, iamSvc, masterKey, handlers_eks.NewOIDCJWKSReader(js)) require.NoError(t, err) issuer := testDiscIssuer diff --git a/spinifex/gateway/sts.go b/spinifex/gateway/sts.go index b153c76ef..6f06fbbb8 100644 --- a/spinifex/gateway/sts.go +++ b/spinifex/gateway/sts.go @@ -8,11 +8,11 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/sts" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" spxarn "github.com/mulgadc/spinifex/spinifex/foundation/aws/arn" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" awsxml "github.com/mulgadc/spinifex/spinifex/foundation/aws/xml" gateway_sts "github.com/mulgadc/spinifex/spinifex/gateway/sts" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" "github.com/mulgadc/spinifex/spinifex/ingress/aws/query" ) @@ -105,11 +105,11 @@ func (gw *GatewayConfig) stsPolicyResource(queryArgs map[string]string) (string, return "", errors.New(awserrors.ErrorInternalError) } - _, role, err := handlers_sts.ResolveRoleByARN(gw.IAMService, roleARN) + _, role, err := stsdomain.ResolveRoleByARN(gw.IAMService, roleARN) switch { case err == nil: return aws.StringValue(role.Arn), nil - case errors.Is(err, handlers_sts.ErrRoleUnresolved): + case errors.Is(err, stsdomain.ErrRoleUnresolved): // Echoing the supplied ARN keeps the denial identical whether or not the // role exists. The handler refuses an ARN that is not the stored one, so // a grant matched here on an invented path still ends in a denial there. diff --git a/spinifex/gateway/sts/AssumeRole.go b/spinifex/gateway/sts/AssumeRole.go index fb6873c73..028807df6 100644 --- a/spinifex/gateway/sts/AssumeRole.go +++ b/spinifex/gateway/sts/AssumeRole.go @@ -5,15 +5,15 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/sts" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" ) // AssumeRole validates the request and delegates to STSService. Authorization // takes two gates: the dispatcher checks the caller's identity policy against // the target role ARN, and STSService checks the role's trust policy. // callerARN is resolved by the dispatcher from the SigV4 principal. -func AssumeRole(callerAccountID, callerARN, callerIdentity string, input *sts.AssumeRoleInput, svc handlers_sts.STSService) (*sts.AssumeRoleOutput, error) { +func AssumeRole(callerAccountID, callerARN, callerIdentity string, input *sts.AssumeRoleInput, svc stsdomain.STSService) (*sts.AssumeRoleOutput, error) { if input == nil { return nil, errors.New(awserrors.ErrorMissingParameter) } diff --git a/spinifex/gateway/sts/AssumeRoleWithWebIdentity.go b/spinifex/gateway/sts/AssumeRoleWithWebIdentity.go index 390ecfc58..914966500 100644 --- a/spinifex/gateway/sts/AssumeRoleWithWebIdentity.go +++ b/spinifex/gateway/sts/AssumeRoleWithWebIdentity.go @@ -5,14 +5,14 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/sts" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" ) // AssumeRoleWithWebIdentity validates the request and delegates to STSService. // This action is anonymous — the caller is identified by the OIDC token, not // SigV4. The role's trust policy is the authoritative authorization check. -func AssumeRoleWithWebIdentity(input *sts.AssumeRoleWithWebIdentityInput, svc handlers_sts.STSService) (*sts.AssumeRoleWithWebIdentityOutput, error) { +func AssumeRoleWithWebIdentity(input *sts.AssumeRoleWithWebIdentityInput, svc stsdomain.STSService) (*sts.AssumeRoleWithWebIdentityOutput, error) { if input == nil { return nil, errors.New(awserrors.ErrorMissingParameter) } diff --git a/spinifex/gateway/sts/GetAccessKeyInfo.go b/spinifex/gateway/sts/GetAccessKeyInfo.go index ca7b5745d..ab235c3c3 100644 --- a/spinifex/gateway/sts/GetAccessKeyInfo.go +++ b/spinifex/gateway/sts/GetAccessKeyInfo.go @@ -2,12 +2,12 @@ package gateway_sts import ( "github.com/aws/aws-sdk-go/service/sts" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" ) // GetAccessKeyInfo delegates to the STSService; not gated by caller IAM policy, // as AWS requires no permission for this call. No caller fields are passed: the // answer is not scoped to the caller, so a key owned by another account resolves. -func GetAccessKeyInfo(input *sts.GetAccessKeyInfoInput, svc handlers_sts.STSService) (*sts.GetAccessKeyInfoOutput, error) { +func GetAccessKeyInfo(input *sts.GetAccessKeyInfoInput, svc stsdomain.STSService) (*sts.GetAccessKeyInfoOutput, error) { return svc.GetAccessKeyInfo(input) } diff --git a/spinifex/gateway/sts/GetCallerIdentity.go b/spinifex/gateway/sts/GetCallerIdentity.go index a2ede31d4..d995c93f8 100644 --- a/spinifex/gateway/sts/GetCallerIdentity.go +++ b/spinifex/gateway/sts/GetCallerIdentity.go @@ -11,8 +11,8 @@ import ( "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/sts" iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" ) // Principal-type values mirror gateway.principalType{User,AssumedRole,Root}, @@ -36,7 +36,7 @@ func GetCallerIdentity( accountID, callerARN, callerPrincipalType, identity, assumedRoleID string, input *sts.GetCallerIdentityInput, iamSvc iamdomain.IAMService, - stsSvc handlers_sts.STSService, + stsSvc stsdomain.STSService, ) (*sts.GetCallerIdentityOutput, error) { userID, _, err := ResolveCallerUser(accountID, callerPrincipalType, identity, assumedRoleID, iamSvc) if err != nil { diff --git a/spinifex/gateway/sts/GetSessionToken.go b/spinifex/gateway/sts/GetSessionToken.go index 51b979e49..01a030e09 100644 --- a/spinifex/gateway/sts/GetSessionToken.go +++ b/spinifex/gateway/sts/GetSessionToken.go @@ -2,12 +2,12 @@ package gateway_sts import ( "github.com/aws/aws-sdk-go/service/sts" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" ) // GetSessionToken delegates to the STSService; not gated by caller IAM policy, // as AWS requires no permission for authentication operations. callerUserName // and callerAccessKeyID come from the SigV4 context. -func GetSessionToken(callerAccountID, callerUserName, callerPrincipalType, callerAccessKeyID string, input *sts.GetSessionTokenInput, svc handlers_sts.STSService) (*sts.GetSessionTokenOutput, error) { +func GetSessionToken(callerAccountID, callerUserName, callerPrincipalType, callerAccessKeyID string, input *sts.GetSessionTokenInput, svc stsdomain.STSService) (*sts.GetSessionTokenOutput, error) { return svc.GetSessionToken(callerAccountID, callerUserName, callerPrincipalType, callerAccessKeyID, input) } diff --git a/spinifex/gateway/sts/handler_test.go b/spinifex/gateway/sts/handler_test.go index 7bb835300..d24e9b7c8 100644 --- a/spinifex/gateway/sts/handler_test.go +++ b/spinifex/gateway/sts/handler_test.go @@ -8,27 +8,27 @@ import ( "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/sts" iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// stubSTSService implements handlers_sts.STSService for table tests. It embeds +// stubSTSService implements stsdomain.STSService for table tests. It embeds // the interface so it satisfies the full contract; each fn-field method // delegates to an injectable function so individual cases can simulate the // underlying service's behaviour without standing up NATS/JetStream. Any other // method nil-panics if a test reaches an unmocked path. type stubSTSService struct { - handlers_sts.STSService + stsdomain.STSService assumeRoleFn func(callerAccountID, callerARN, callerIdentity string, input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) getCallerIdentityFn func(callerAccountID, callerARN, callerUserID string, input *sts.GetCallerIdentityInput) (*sts.GetCallerIdentityOutput, error) getSessionTokenFn func(callerAccountID, callerUserName, callerPrincipalType, callerAccessKeyID string, input *sts.GetSessionTokenInput) (*sts.GetSessionTokenOutput, error) - lookupSessionFn func(akid string) (*handlers_sts.SessionCredential, error) + lookupSessionFn func(akid string) (*stsdomain.SessionCredential, error) } -var _ handlers_sts.STSService = (*stubSTSService)(nil) +var _ stsdomain.STSService = (*stubSTSService)(nil) func (s *stubSTSService) AssumeRole(callerAccountID, callerARN, callerIdentity string, input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) { if s.assumeRoleFn != nil { @@ -55,7 +55,7 @@ func (s *stubSTSService) GetSessionToken(callerAccountID, callerUserName, caller return &sts.GetSessionTokenOutput{}, nil } -func (s *stubSTSService) LookupSessionCredential(akid string) (*handlers_sts.SessionCredential, error) { +func (s *stubSTSService) LookupSessionCredential(akid string) (*stsdomain.SessionCredential, error) { if s.lookupSessionFn != nil { return s.lookupSessionFn(akid) } @@ -235,7 +235,7 @@ func TestGetCallerIdentity_AssumedRole(t *testing.T) { // handler must NOT issue a second LookupSessionCredential — the verifier // already loaded the record. stsSvc := &stubSTSService{ - lookupSessionFn: func(string) (*handlers_sts.SessionCredential, error) { + lookupSessionFn: func(string) (*stsdomain.SessionCredential, error) { t.Fatal("assumed-role path must not re-lookup the session credential") return nil, nil }, diff --git a/spinifex/gateway/sts_policy_test.go b/spinifex/gateway/sts_policy_test.go index 188f1e127..06ab1b515 100644 --- a/spinifex/gateway/sts_policy_test.go +++ b/spinifex/gateway/sts_policy_test.go @@ -222,7 +222,7 @@ func TestSTSRequest_AssumeRole_PolicyScopedToRole(t *testing.T) { // TestSTSRequest_AssumeRole_PathIsResolvedNotTrusted pins the gate to the ARN // IAM stored, so a role genuinely stored under a path is matched by its full // ARN. An ARN that is not the stored one is refused by role resolution itself -// — see TestResolveRoleByARN in handlers/sts. +// — see TestResolveRoleByARN in domains/sts. func TestSTSRequest_AssumeRole_PathIsResolvedNotTrusted(t *testing.T) { t.Run("stored ARN matching exactly is admitted", func(t *testing.T) { svc := stsIdentityPolicy( diff --git a/spinifex/gateway/sts_request_test.go b/spinifex/gateway/sts_request_test.go index 9e5e342b3..361ccfbb3 100644 --- a/spinifex/gateway/sts_request_test.go +++ b/spinifex/gateway/sts_request_test.go @@ -14,8 +14,8 @@ import ( "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/sts" iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -25,16 +25,16 @@ import ( // the fn-field methods below are wired, and any other method nil-panics if a // test reaches an unmocked path. type flexMockSTSService struct { - handlers_sts.STSService + stsdomain.STSService assumeRoleFn func(string, string, string, *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) getCallerIdentityFn func(string, string, string, *sts.GetCallerIdentityInput) (*sts.GetCallerIdentityOutput, error) getSessionTokenFn func(string, string, string, string, *sts.GetSessionTokenInput) (*sts.GetSessionTokenOutput, error) - lookupSessionFn func(string) (*handlers_sts.SessionCredential, error) + lookupSessionFn func(string) (*stsdomain.SessionCredential, error) assumeWebIdentityFn func(*sts.AssumeRoleWithWebIdentityInput) (*sts.AssumeRoleWithWebIdentityOutput, error) } -var _ handlers_sts.STSService = (*flexMockSTSService)(nil) +var _ stsdomain.STSService = (*flexMockSTSService)(nil) func (m *flexMockSTSService) AssumeRole(callerAccountID, callerARN, callerIdentity string, input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error) { if m.assumeRoleFn != nil { @@ -61,7 +61,7 @@ func (m *flexMockSTSService) GetSessionToken(callerAccountID, callerUserName, ca return &sts.GetSessionTokenOutput{}, nil } -func (m *flexMockSTSService) LookupSessionCredential(akid string) (*handlers_sts.SessionCredential, error) { +func (m *flexMockSTSService) LookupSessionCredential(akid string) (*stsdomain.SessionCredential, error) { if m.lookupSessionFn != nil { return m.lookupSessionFn(akid) } @@ -86,7 +86,7 @@ type stsRequestParams struct { // underlyingRoleARN is what the policy gate resolves an assumed-role // caller's policies by; SessionName never participates. underlyingRoleARN string - stsSvc handlers_sts.STSService + stsSvc stsdomain.STSService iamSvc iamdomain.IAMService } @@ -166,7 +166,7 @@ func TestSTSRequest_AssumeRole_Success(t *testing.T) { func TestSTSRequest_GetCallerIdentity_AssumedRole(t *testing.T) { // AssumedRoleID comes from the resolved SessionCredential; the dispatcher must not re-lookup it. svc := &flexMockSTSService{ - lookupSessionFn: func(string) (*handlers_sts.SessionCredential, error) { + lookupSessionFn: func(string) (*stsdomain.SessionCredential, error) { t.Fatal("assumed-role GetCallerIdentity must not re-lookup the session credential") return nil, nil }, diff --git a/spinifex/handlers/eks/oidc_jwks_reader.go b/spinifex/handlers/eks/oidc_jwks_reader.go index cc1af3cf1..b5b72bdb0 100644 --- a/spinifex/handlers/eks/oidc_jwks_reader.go +++ b/spinifex/handlers/eks/oidc_jwks_reader.go @@ -10,8 +10,8 @@ import ( // OIDCJWKSReader resolves the raw JWKS document published for an EKS // cluster's OIDC issuer, from the per-account bucket this package owns. -// It is the transitional handlers/sts-facing adapter; runtime/roles/awsgw -// wires it into STS as a handlers_sts.ClusterJWKSResolver. +// It is the transitional domains/sts-facing adapter; runtime/roles/awsgw +// wires it into STS as a stsdomain.ClusterJWKSResolver. type OIDCJWKSReader struct { js jetstream.JetStream } diff --git a/spinifex/handlers/eks/token_webhook.go b/spinifex/handlers/eks/token_webhook.go index aa2d8b756..b99ec5045 100644 --- a/spinifex/handlers/eks/token_webhook.go +++ b/spinifex/handlers/eks/token_webhook.go @@ -29,7 +29,7 @@ type TokenVerifyRequest struct { } // TokenVerifyResponse is the NATS reply: the resolved caller principal. It -// mirrors the fields of handlers_sts.PresignedCallerIdentity the webhook needs +// mirrors the fields of stsdomain.PresignedCallerIdentity the webhook needs // to key its AccessEntry lookup. type TokenVerifyResponse struct { AccountID string `json:"accountID"` diff --git a/spinifex/runtime/roles/awsgw/awsgw.go b/spinifex/runtime/roles/awsgw/awsgw.go index 802946570..9748d7e09 100644 --- a/spinifex/runtime/roles/awsgw/awsgw.go +++ b/spinifex/runtime/roles/awsgw/awsgw.go @@ -37,13 +37,13 @@ import ( "github.com/mulgadc/spinifex/spinifex/domains/network/reconcile" "github.com/mulgadc/spinifex/spinifex/domains/ochre" ochrevector "github.com/mulgadc/spinifex/spinifex/domains/ochre/vector" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/lifecycle/reconciler" "github.com/mulgadc/spinifex/spinifex/foundation/state/kvstore" "github.com/mulgadc/spinifex/spinifex/foundation/state/kvutil" "github.com/mulgadc/spinifex/spinifex/gateway" gateway_bedrock "github.com/mulgadc/spinifex/spinifex/gateway/bedrock" handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" "github.com/mulgadc/spinifex/spinifex/ingress/aws/dispatch" "github.com/mulgadc/spinifex/spinifex/providers/objectstore" "github.com/mulgadc/spinifex/spinifex/runtime/compute/cache" @@ -57,8 +57,8 @@ import ( var serviceName = "awsgw" // OIDCJWKSReader is wired into STS here, not beside its own definition in -// handlers/eks, because handlers/eks must not import handlers/sts. -var _ handlers_sts.ClusterJWKSResolver = (*handlers_eks.OIDCJWKSReader)(nil) +// handlers/eks, because handlers/eks must not import domains/sts. +var _ stsdomain.ClusterJWKSResolver = (*handlers_eks.OIDCJWKSReader)(nil) // AccountStatusReader also serves as the quota override seal's TERMINATING // precondition, wired here because domains/iam must not import @@ -237,7 +237,7 @@ func launchService(config *config.ClusterConfig) error { // STS service shares the IAM master key (single envelope for at-rest // secrets + session-token HMACs) and resolves roles via IAMService. - stsService, err := handlers_sts.NewSTSServiceImpl(janitorCtx, natsConn, iamService, masterKey, jwksResolver) + stsService, err := stsdomain.NewSTSServiceImpl(janitorCtx, natsConn, iamService, masterKey, jwksResolver) if err != nil { return fmt.Errorf("initialize STS service: %w", err) } diff --git a/spinifex/runtime/roles/awsgw/eks_token_verify.go b/spinifex/runtime/roles/awsgw/eks_token_verify.go index 351e4569a..3f4973da0 100644 --- a/spinifex/runtime/roles/awsgw/eks_token_verify.go +++ b/spinifex/runtime/roles/awsgw/eks_token_verify.go @@ -4,9 +4,9 @@ import ( "encoding/json" "log/slog" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/mulgadc/spinifex/spinifex/foundation/aws/errors" handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" "github.com/nats-io/nats.go" ) @@ -18,10 +18,10 @@ const eksTokenVerifyQueue = "awsgw-eks-token-verify" //nolint:gosec // queue-gro // needs: resolve an `aws eks get-token` presigned URL into a caller principal, // binding the signature to the expected cluster name (anti-replay, Q10). type presignedVerifier interface { - VerifyPresignedGetCallerIdentity(presignedURL, expectedClusterName string) (*handlers_sts.PresignedCallerIdentity, error) + VerifyPresignedGetCallerIdentity(presignedURL, expectedClusterName string) (*stsdomain.PresignedCallerIdentity, error) } -var _ presignedVerifier = (*handlers_sts.STSServiceImpl)(nil) +var _ presignedVerifier = (*stsdomain.STSServiceImpl)(nil) // registerEKSTokenVerify subscribes the awsgw service to TokenVerifySubject so // the in-cluster eks-token-webhook can resolve get-token presigned URLs over diff --git a/spinifex/runtime/roles/awsgw/eks_token_verify_test.go b/spinifex/runtime/roles/awsgw/eks_token_verify_test.go index 5f61c6f94..703d91620 100644 --- a/spinifex/runtime/roles/awsgw/eks_token_verify_test.go +++ b/spinifex/runtime/roles/awsgw/eks_token_verify_test.go @@ -8,20 +8,20 @@ import ( "time" "github.com/mulgadc/spinifex/internal/testkit" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type fakeVerifier struct { - ident *handlers_sts.PresignedCallerIdentity + ident *stsdomain.PresignedCallerIdentity err error gotURL string gotName string } -func (f *fakeVerifier) VerifyPresignedGetCallerIdentity(presignedURL, expectedClusterName string) (*handlers_sts.PresignedCallerIdentity, error) { +func (f *fakeVerifier) VerifyPresignedGetCallerIdentity(presignedURL, expectedClusterName string) (*stsdomain.PresignedCallerIdentity, error) { f.gotURL = presignedURL f.gotName = expectedClusterName return f.ident, f.err @@ -29,7 +29,7 @@ func (f *fakeVerifier) VerifyPresignedGetCallerIdentity(presignedURL, expectedCl func TestEKSTokenVerify_ResolvesPrincipal(t *testing.T) { _, nc := testutil.StartTestNATS(t) - fake := &fakeVerifier{ident: &handlers_sts.PresignedCallerIdentity{ + fake := &fakeVerifier{ident: &stsdomain.PresignedCallerIdentity{ AccountID: "111122223333", ARN: "arn:aws:iam::111122223333:role/admin", UserID: "AROAEXAMPLE:sess", @@ -66,7 +66,7 @@ func TestEKSTokenVerify_RejectedTokenIsError(t *testing.T) { func TestEKSTokenVerify_RejectsEmptyFields(t *testing.T) { _, nc := testutil.StartTestNATS(t) - fake := &fakeVerifier{ident: &handlers_sts.PresignedCallerIdentity{ARN: "arn:..."}} + fake := &fakeVerifier{ident: &stsdomain.PresignedCallerIdentity{ARN: "arn:..."}} sub, err := registerEKSTokenVerify(nc, fake) require.NoError(t, err) defer func() { _ = sub.Unsubscribe() }() diff --git a/tests/integration/harness.go b/tests/integration/harness.go index 7c8024961..70963c838 100644 --- a/tests/integration/harness.go +++ b/tests/integration/harness.go @@ -43,7 +43,7 @@ import ( gateway_bedrock "github.com/mulgadc/spinifex/spinifex/gateway/bedrock" handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks" iamdomain "github.com/mulgadc/spinifex/spinifex/domains/iam" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" awsdispatch "github.com/mulgadc/spinifex/spinifex/ingress/aws/dispatch" "github.com/mulgadc/spinifex/spinifex/providers/objectstore" "github.com/nats-io/nats.go" @@ -148,7 +148,7 @@ func startGateway(t *testing.T, collector *conformanceCollector, opts ...Option) iamSvc, err := iamdomain.NewIAMServiceWithRetry(t.Context(), nc, masterKey) require.NoError(t, err) - stsSvc, err := handlers_sts.NewSTSServiceImpl(t.Context(), nc, iamSvc, masterKey, handlers_eks.NewOIDCJWKSReader(js)) + stsSvc, err := stsdomain.NewSTSServiceImpl(t.Context(), nc, iamSvc, masterKey, handlers_eks.NewOIDCJWKSReader(js)) require.NoError(t, err) encryptedSecret, err := iamdomain.EncryptSecret(testSecretAccessKey, masterKey) diff --git a/tests/integration/sts_eks_oidc_issuer_test.go b/tests/integration/sts_eks_oidc_issuer_test.go index 264cb3604..e22be3780 100644 --- a/tests/integration/sts_eks_oidc_issuer_test.go +++ b/tests/integration/sts_eks_oidc_issuer_test.go @@ -6,14 +6,14 @@ import ( "testing" handlers_eks "github.com/mulgadc/spinifex/spinifex/handlers/eks" - handlers_sts "github.com/mulgadc/spinifex/spinifex/handlers/sts" + stsdomain "github.com/mulgadc/spinifex/spinifex/domains/sts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestEKSClusterOIDCIssuer_RoundTripsThroughSTSParseEKSIssuerURL is a // composition test: handlers/eks mints an issuer URL via ClusterOIDCIssuer and -// handlers/sts must parse it back via ParseEKSIssuerURL to the same +// domains/sts must parse it back via ParseEKSIssuerURL to the same // (accountID, clusterName) — the collision the JWKS lookup must avoid. // Importing both packages together is intentional here; neither package's own // tests may depend on the other. @@ -21,7 +21,7 @@ func TestEKSClusterOIDCIssuer_RoundTripsThroughSTSParseEKSIssuerURL(t *testing.T issuer, err := handlers_eks.ClusterOIDCIssuer("https://10.0.0.1:9999", "au-mel-1", "123456789012", "my-cluster") require.NoError(t, err) - accountID, clusterName, err := handlers_sts.ParseEKSIssuerURL(issuer) + accountID, clusterName, err := stsdomain.ParseEKSIssuerURL(issuer) require.NoError(t, err) assert.Equal(t, "123456789012", accountID) assert.Equal(t, "my-cluster", clusterName)