mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-26 05:59:41 +00:00
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:
@@ -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"},
|
||||
|
||||
@@ -25,7 +25,6 @@ var frozenLegacyPackages = []string{
|
||||
"handlers/eks",
|
||||
"handlers/elbv2",
|
||||
"handlers/rds",
|
||||
"handlers/sts",
|
||||
"lbagent",
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"testing"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
+1
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"context"
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"errors"
|
||||
+1
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
package handlers_sts
|
||||
package sts
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
@@ -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)))
|
||||
})
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
@@ -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,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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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() }()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user