refactor(sts): move handlers/sts to domains/sts (#1250)

* refactor(sts): move handlers/sts to domains/sts

Mechanical ADR-0001 relocation of the STS implementation. APIs, NATS
subjects, KV buckets and behaviour are unchanged; only the package name
changes from handlers_sts to sts (imported as stsdomain). The frozen-legacy
entry is removed and the allowlist entries now name domains/sts; the
per-consumer IAM capability debt remains.

* docs: name the STS relocation PR in the migration record
This commit is contained in:
Josh Siviour
2026-10-26 08:28:10 +10:00
committed by GitHub
parent b557386c4b
commit fbf76cbedc
58 changed files with 167 additions and 167 deletions
+2 -2
View File
@@ -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"},
-1
View File
@@ -25,7 +25,6 @@ var frozenLegacyPackages = []string{
"handlers/eks",
"handlers/elbv2",
"handlers/rds",
"handlers/sts",
"lbagent",
}
+4 -3
View File
@@ -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. |
@@ -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,
@@ -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",
+4 -4
View File
@@ -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 {
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"context"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"crypto/hmac"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"context"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"crypto/ecdsa"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"context"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"errors"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"errors"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"github.com/aws/aws-sdk-go/aws"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"testing"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"context"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"crypto/hmac"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"github.com/aws/aws-sdk-go/service/sts"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"testing"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"context"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"context"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"encoding/json"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"errors"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"errors"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"errors"
@@ -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)
@@ -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"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"context"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"bytes"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"context"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"context"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"errors"
@@ -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"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"fmt"
@@ -1,4 +1,4 @@
package handlers_sts
package sts
import (
"crypto/subtle"
+4 -4
View File
@@ -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
@@ -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)))
})
+28 -28
View File
@@ -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{
+2 -2
View File
@@ -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)
+16 -16
View File
@@ -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) {
+2 -2
View File
@@ -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
@@ -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
+3 -3
View File
@@ -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.
+2 -2
View File
@@ -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)
}
@@ -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)
}
+2 -2
View File
@@ -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)
}
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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)
}
+7 -7
View File
@@ -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
},
+1 -1
View File
@@ -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(
+7 -7
View File
@@ -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
},
+2 -2
View File
@@ -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
}
+1 -1
View File
@@ -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"`
+4 -4
View File
@@ -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)
}
@@ -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
@@ -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() }()
+2 -2
View File
@@ -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)
@@ -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)