carlos / platform Public

Add unmounted captured AWS namespace candidate and signup binding

claude agent, operated by paul@keymail.dev 26a35228f8a20f45113a86300d11272492ca755c parent a7ce3b5
8 files changed, +1235 −0
  • internal/console/signupscope_test.go +8 −0
  • internal/console/signupscopebinding.go +29 −0
  • internal/console/signupscopebinding_test.go +198 −0
  • internal/release/namespacecandidate.go +197 −0
  • internal/release/namespacecandidate_test.go +569 −0
  • internal/release/namespacecredentials.go +19 −0
  • internal/s3compat/awsprofile.go +114 −0
  • internal/s3compat/awsprofile_test.go +101 −0

CI passed — run details

diff --git a/internal/console/signupscope_test.go b/internal/console/signupscope_test.go
index 588dd96d7..5d0c65bee 100644
--- a/internal/console/signupscope_test.go
+++ b/internal/console/signupscope_test.go
@@ -328,6 +328,11 @@ func TestSignupScopeBucketRefusals(t *testing.T) {
if _, code := signupScopeEncode(d); code == signupScopeNoRefusal {
t.Fatalf("bucket %q encoded", bucket)
}
+ for _, style := range []string{"path", "virtual"} {
+ if _, code := signupBindingTestCandidate(t, bucket, "eu-west-1", "aws", style); code == 0 {
+ t.Fatalf("codec-refused bucket %q accepted by factory in %s mode", bucket, style)
+ }
+ }
})
}
}
@@ -580,6 +585,9 @@ func TestSignupScopeCodecStaysPureAndUnmounted(t *testing.T) {
t.Fatal(err)
}
ast.Inspect(file, func(node ast.Node) bool {
+ if decl, ok := node.(*ast.FuncDecl); ok && name == "signupscopebinding.go" && decl.Name.Name == "signupScopeBindingFromCandidate" {
+ return false
+ }
ident, ok := node.(*ast.Ident)
if ok && codecNames[ident.Name] {
t.Errorf("%s: production reference to %s; codec mounting needs a separate reviewed design", fset.Position(ident.Pos()), ident.Name)
diff --git a/internal/console/signupscopebinding.go b/internal/console/signupscopebinding.go
new file mode 100644
index 000000000..24c86e76e
--- /dev/null
+++ b/internal/console/signupscopebinding.go
@@ -0,0 +1,29 @@
+package console
+
+import "github.com/carlosframework/platform/internal/release"
+
+type signupScopeBinding struct {
+ candidate release.NamespaceCandidate
+ encoded string
+}
+
+func (b signupScopeBinding) bytes() []byte { return []byte(b.encoded) }
+
+func signupScopeBindingFromCandidate(candidate release.NamespaceCandidate, authorityID, lineageID string) (signupScopeBinding, signupScopeRefusal) {
+ facts, code := candidate.NamespaceFacts()
+ if code != release.NamespaceAccepted || facts.Mode != "aws" || facts.EffectivePrefix != "" {
+ return signupScopeBinding{}, signupScopeRefuseField
+ }
+ encoded, refusal := signupScopeEncode(signupScopeDescriptor{
+ backendKind: facts.BackendKind, mode: signupScopeModeAWS,
+ logicalAuthorityID: authorityID, databaseLineageID: lineageID,
+ partition: facts.Partition, region: facts.Region, customOrigin: facts.CustomOrigin,
+ namespaceKind: facts.NamespaceKind, namespaceToken: facts.NamespaceToken,
+ addressingStyle: facts.AddressingStyle, bucket: facts.Bucket, effectivePrefix: facts.EffectivePrefix,
+ adapterConfigurationRevision: facts.AdapterConfigurationRevision,
+ })
+ if refusal != signupScopeNoRefusal {
+ return signupScopeBinding{}, refusal
+ }
+ return signupScopeBinding{candidate: candidate, encoded: string(encoded)}, signupScopeNoRefusal
+}
diff --git a/internal/console/signupscopebinding_test.go b/internal/console/signupscopebinding_test.go
new file mode 100644
index 000000000..949e2c052
--- /dev/null
+++ b/internal/console/signupscopebinding_test.go
@@ -0,0 +1,198 @@
+package console
+
+import (
+ "bytes"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/release"
+ "github.com/carlosframework/platform/internal/s3compat"
+)
+
+func signupBindingTestCandidate(t *testing.T, bucket, region, partition, style string) (release.NamespaceCandidate, release.NamespaceRefusal) {
+ t.Helper()
+ values := map[string]string{"CARLOS_DEPLOYMENT_BUCKET": bucket, "AWS_REGION": region, "CARLOS_S3_PARTITION": partition, "CARLOS_S3_ADDRESSING_STYLE": style}
+ snapshot, code := s3compat.CaptureAWSProfile(func(k string) (string, bool) { v, ok := values[k]; return v, ok })
+ if code != s3compat.AWSProfileAccepted {
+ t.Fatalf("capture %d", code)
+ }
+ return release.NewNamespaceCandidate(snapshot)
+}
+
+func TestSignupScopeBindingPairsCandidateAndCopies(t *testing.T) {
+ authority, lineage := strings.Repeat("1", 64), strings.Repeat("2", 64)
+ var previous []byte
+ for _, tc := range []struct{ bucket, region, partition, style string }{
+ {"factory-test-bucket", "eu-west-1", "aws", "path"},
+ {"second-test-bucket", "eu-west-1", "aws", "path"},
+ {"second-test-bucket", "us-east-1", "aws", "virtual"},
+ {"second-test-bucket", "cn-north-1", "aws-cn", "path"},
+ } {
+ candidate, code := signupBindingTestCandidate(t, tc.bucket, tc.region, tc.partition, tc.style)
+ if code != release.NamespaceAccepted {
+ t.Fatalf("candidate %d", code)
+ }
+ binding, refusal := signupScopeBindingFromCandidate(candidate, authority, lineage)
+ if refusal != signupScopeNoRefusal {
+ t.Fatalf("binding %d", refusal)
+ }
+ encoded := binding.bytes()
+ saved := bytes.Clone(encoded)
+ d, refusal := signupScopeDecode(encoded)
+ if refusal != signupScopeNoRefusal {
+ t.Fatalf("decode %d", refusal)
+ }
+ if d.bucket != tc.bucket || d.region != tc.region || d.partition != tc.partition || d.addressingStyle != tc.style || d.logicalAuthorityID != authority || d.databaseLineageID != lineage || d.effectivePrefix != "" || d.mode != signupScopeModeAWS || d.adapterConfigurationRevision != "aws-root-v1-sdk-s3-1-105-0" {
+ t.Fatalf("paired descriptor %#v", d)
+ }
+ paired, _ := binding.candidate.NamespaceFacts()
+ input, _ := candidate.NamespaceFacts()
+ if paired != input {
+ t.Fatal("different candidate retained")
+ }
+ input.Bucket = "mutated"
+ candidate = release.NamespaceCandidate{}
+ for i := range encoded {
+ encoded[i] ^= 0xff
+ }
+ if !bytes.Equal(binding.bytes(), saved) {
+ t.Fatal("returned bytes alias binding")
+ }
+ if previous != nil && bytes.Equal(previous, saved) {
+ t.Fatal("changed candidate used prior descriptor")
+ }
+ previous = saved
+ }
+ candidate, _ := signupBindingTestCandidate(t, "factory-test-bucket", "eu-west-1", "aws", "path")
+ first, code := signupScopeBindingFromCandidate(candidate, authority, lineage)
+ if code != signupScopeNoRefusal {
+ t.Fatal(code)
+ }
+ for _, ids := range [][2]string{{lineage, lineage}, {authority, authority}} {
+ b, code := signupScopeBindingFromCandidate(candidate, ids[0], ids[1])
+ if code != signupScopeNoRefusal || bytes.Equal(first.bytes(), b.bytes()) {
+ t.Fatal("identity change lost")
+ }
+ }
+ for _, ids := range [][2]string{{"", lineage}, {authority, ""}, {strings.Repeat("0", 64), lineage}, {authority, "invalid"}} {
+ b, code := signupScopeBindingFromCandidate(candidate, ids[0], ids[1])
+ if code == signupScopeNoRefusal || len(b.bytes()) != 0 {
+ t.Fatal("invalid IDs accepted")
+ }
+ }
+ if b, code := signupScopeBindingFromCandidate(release.NamespaceCandidate{}, authority, lineage); code == signupScopeNoRefusal || len(b.bytes()) != 0 {
+ t.Fatal("zero candidate bound")
+ }
+}
+
+func TestSignupScopeBindingStaysPrivateAndUnmounted(t *testing.T) {
+ fset := token.NewFileSet()
+ files, err := filepath.Glob("*.go")
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, name := range files {
+ if strings.HasSuffix(name, "_test.go") {
+ continue
+ }
+ file, err := parser.ParseFile(fset, name, nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, decl := range file.Decls {
+ allowed := false
+ if name == "signupscopebinding.go" {
+ switch d := decl.(type) {
+ case *ast.FuncDecl:
+ allowed = d.Name.Name == "signupScopeBindingFromCandidate" || d.Name.Name == "bytes" && d.Recv != nil && len(d.Recv.List) == 1
+ case *ast.GenDecl:
+ if len(d.Specs) == 1 {
+ if s, ok := d.Specs[0].(*ast.TypeSpec); ok {
+ allowed = s.Name.Name == "signupScopeBinding"
+ }
+ }
+ }
+ }
+ if allowed {
+ continue
+ }
+ ast.Inspect(decl, func(n ast.Node) bool {
+ if id, ok := n.(*ast.Ident); ok && (id.Name == "signupScopeBinding" || id.Name == "signupScopeBindingFromCandidate") {
+ t.Errorf("binding production reference %s", fset.Position(id.Pos()))
+ }
+ return true
+ })
+ }
+ }
+ typ := reflect.TypeOf(signupScopeBinding{})
+ for i := 0; i < typ.NumField(); i++ {
+ if typ.Field(i).IsExported() || typ.Field(i).Anonymous {
+ t.Fatal("binding exported field")
+ }
+ }
+ ctor := reflect.TypeOf(signupScopeBindingFromCandidate)
+ if ctor.NumIn() != 3 || ctor.In(0) != reflect.TypeOf(release.NamespaceCandidate{}) || ctor.In(1).Kind() != reflect.String || ctor.In(2).Kind() != reflect.String || ctor.IsVariadic() {
+ t.Fatal("binding accepts claimed facts/store")
+ }
+}
+
+func TestSignupScopeBindingRejectsFutureProfileAST(t *testing.T) {
+ fset := token.NewFileSet()
+ file, err := parser.ParseFile(fset, "signupscopebinding.go", nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var helper *ast.FuncDecl
+ for _, decl := range file.Decls {
+ if fn, ok := decl.(*ast.FuncDecl); ok && fn.Name.Name == "signupScopeBindingFromCandidate" {
+ helper = fn
+ }
+ }
+ if helper == nil || len(helper.Body.List) < 2 {
+ t.Fatal("missing helper guard")
+ }
+ guard, ok := helper.Body.List[1].(*ast.IfStmt)
+ if !ok {
+ t.Fatal("guard must precede encoding")
+ }
+ checks := map[string]bool{}
+ ast.Inspect(guard.Cond, func(n ast.Node) bool {
+ binary, ok := n.(*ast.BinaryExpr)
+ if !ok || binary.Op != token.NEQ {
+ return true
+ }
+ sel, ok := binary.X.(*ast.SelectorExpr)
+ if !ok {
+ return true
+ }
+ id, ok := sel.X.(*ast.Ident)
+ if !ok || id.Name != "facts" {
+ return true
+ }
+ literal, ok := binary.Y.(*ast.BasicLit)
+ if !ok {
+ return true
+ }
+ checks[sel.Sel.Name+"="+literal.Value] = true
+ return true
+ })
+ if !checks[`Mode="aws"`] || !checks[`EffectivePrefix=""`] {
+ t.Fatal("future custom/prefix candidate needs independent refusal")
+ }
+ if len(guard.Body.List) != 1 {
+ t.Fatal("profile refusal side effects")
+ }
+ ret, ok := guard.Body.List[0].(*ast.ReturnStmt)
+ if !ok || len(ret.Results) != 2 {
+ t.Fatal("profile guard does not refuse")
+ }
+ code, ok := ret.Results[1].(*ast.Ident)
+ if !ok || code.Name != "signupScopeRefuseField" {
+ t.Fatal("profile guard grants binding")
+ }
+}
diff --git a/internal/release/namespacecandidate.go b/internal/release/namespacecandidate.go
new file mode 100644
index 000000000..0d87b3425
--- /dev/null
+++ b/internal/release/namespacecandidate.go
@@ -0,0 +1,197 @@
+package release
+
+import (
+ "context"
+ "net"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/aws/retry"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ smithyauth "github.com/aws/smithy-go/auth"
+ endpoints "github.com/aws/smithy-go/endpoints"
+ "github.com/aws/smithy-go/middleware"
+ smithyhttp "github.com/aws/smithy-go/transport/http"
+ "github.com/carlosframework/platform/internal/s3compat"
+ "github.com/carlosframework/platform/internal/sigfence"
+)
+
+type NamespaceRefusal uint8
+
+const (
+ NamespaceAccepted NamespaceRefusal = iota
+ NamespaceRefuseSnapshot
+ NamespaceRefuseBucket
+ NamespaceRefuseCandidate
+)
+
+type NamespaceFacts struct {
+ BackendKind string
+ Mode string
+ Partition string
+ Region string
+ CustomOrigin string
+ NamespaceKind string
+ NamespaceToken string
+ AddressingStyle string
+ Bucket string
+ EffectivePrefix string
+ AdapterConfigurationRevision string
+}
+
+type NamespaceCandidate struct {
+ adapter *S3Store
+ facts NamespaceFacts
+}
+
+func (c NamespaceCandidate) NamespaceFacts() (NamespaceFacts, NamespaceRefusal) {
+ if c.adapter == nil {
+ return NamespaceFacts{}, NamespaceRefuseCandidate
+ }
+ return c.facts, NamespaceAccepted
+}
+
+func NewNamespaceCandidate(snapshot s3compat.AWSProfileSnapshot) (NamespaceCandidate, NamespaceRefusal) {
+ values, code := snapshot.Values()
+ if code != s3compat.AWSProfileAccepted {
+ return NamespaceCandidate{}, NamespaceRefuseSnapshot
+ }
+ if !namespaceBucket(values.Bucket) || (values.AddressingStyle == "virtual" && strings.Contains(values.Bucket, ".")) {
+ return NamespaceCandidate{}, NamespaceRefuseBucket
+ }
+ facts := NamespaceFacts{
+ BackendKind: "s3", Mode: "aws", Partition: values.Partition, Region: values.Region,
+ NamespaceKind: "global", AddressingStyle: values.AddressingStyle, Bucket: values.Bucket,
+ AdapterConfigurationRevision: values.Revision,
+ }
+ client := awss3.New(namespaceOptions(values))
+ return NamespaceCandidate{adapter: &S3Store{Client: client, Bucket: facts.Bucket}, facts: facts}, NamespaceAccepted
+}
+
+func namespaceOptions(v s3compat.AWSProfileValues) awss3.Options {
+ return awss3.Options{
+ Credentials: namespaceRefusingCredentials{},
+ Region: v.Region,
+ EndpointResolverV2: namespaceResolver{values: v},
+ HTTPClient: &http.Client{
+ Transport: &http.Transport{
+ Proxy: nil,
+ DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
+ ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second,
+ TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: time.Second,
+ },
+ CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
+ Jar: nil,
+ },
+ APIOptions: []func(*middleware.Stack) error{sigfence.WatchAttempts},
+ HTTPSignerV4: sigfence.NewS3Signer(),
+ RequestChecksumCalculation: aws.RequestChecksumCalculationWhenRequired,
+ ResponseChecksumValidation: aws.ResponseChecksumValidationWhenRequired,
+ ClientLogMode: 0,
+ DefaultsMode: aws.DefaultsModeStandard,
+ RetryMode: aws.RetryModeStandard,
+ RetryMaxAttempts: 3,
+ Retryer: retry.NewStandard(func(o *retry.StandardOptions) { o.MaxAttempts = 3 }),
+ UsePathStyle: v.AddressingStyle == "path",
+ UseAccelerate: false, UseARNRegion: false, UseDualstack: false,
+ EndpointOptions: awss3.EndpointResolverOptions{
+ UseFIPSEndpoint: aws.FIPSEndpointStateDisabled,
+ UseDualStackEndpoint: aws.DualStackEndpointStateDisabled,
+ },
+ DisableMultiRegionAccessPoints: true,
+ DisableS3ExpressSessionAuth: aws.Bool(true),
+ }
+}
+
+type namespaceEndpointRefusal uint8
+
+func (e namespaceEndpointRefusal) Error() string { return strconv.Itoa(int(e)) }
+
+type namespaceResolver struct{ values s3compat.AWSProfileValues }
+
+func (r namespaceResolver) ResolveEndpoint(ctx context.Context, p awss3.EndpointParameters) (endpoints.Endpoint, error) {
+ v := r.values
+ if p.Region == nil || *p.Region != v.Region || p.Bucket == nil || *p.Bucket != v.Bucket ||
+ p.Endpoint != nil || aws.ToBool(p.UseFIPS) || aws.ToBool(p.UseDualStack) || aws.ToBool(p.Accelerate) ||
+ aws.ToBool(p.UseGlobalEndpoint) || aws.ToBool(p.UseObjectLambdaEndpoint) || aws.ToBool(p.UseArnRegion) ||
+ aws.ToBool(p.UseS3ExpressControlEndpoint) || !aws.ToBool(p.DisableMultiRegionAccessPoints) ||
+ !aws.ToBool(p.DisableS3ExpressSessionAuth) || p.ForcePathStyle == nil || *p.ForcePathStyle != (v.AddressingStyle == "path") {
+ return endpoints.Endpoint{}, namespaceEndpointRefusal(1)
+ }
+ ep, err := awss3.NewDefaultEndpointResolverV2().ResolveEndpoint(ctx, p)
+ if err != nil {
+ return endpoints.Endpoint{}, err
+ }
+ if err := r.checkEndpoint(ep); err != nil {
+ return endpoints.Endpoint{}, err
+ }
+ return ep, nil
+}
+
+func (r namespaceResolver) checkEndpoint(ep endpoints.Endpoint) error {
+ v, u := r.values, ep.URI
+ if u.Scheme != "https" || u.Host == "" || u.User != nil || u.Opaque != "" || u.Port() != "" ||
+ u.RawQuery != "" || u.ForceQuery || u.Fragment != "" || u.RawPath != "" ||
+ !(u.Host == v.DNSSuffix || strings.HasSuffix(u.Host, "."+v.DNSSuffix)) {
+ return namespaceEndpointRefusal(2)
+ }
+ switch v.AddressingStyle {
+ case "virtual":
+ if !strings.HasPrefix(u.Host, v.Bucket+".") || (u.Path != "" && u.Path != "/") {
+ return namespaceEndpointRefusal(3)
+ }
+ case "path":
+ if u.Path != "/"+v.Bucket && u.Path != "/"+v.Bucket+"/" {
+ return namespaceEndpointRefusal(3)
+ }
+ default:
+ return namespaceEndpointRefusal(3)
+ }
+ if len(ep.Headers) != 0 || ep.Properties.Has("backend") {
+ return namespaceEndpointRefusal(4)
+ }
+ options, ok := smithyauth.GetAuthOptions(&ep.Properties)
+ if !ok || len(options) != 1 || options[0] == nil || options[0].SchemeID != "sigv4" {
+ return namespaceEndpointRefusal(5)
+ }
+ name, okName := smithyhttp.GetSigV4SigningName(&options[0].SignerProperties)
+ region, okRegion := smithyhttp.GetSigV4SigningRegion(&options[0].SignerProperties)
+ if !okName || !okRegion || name != "s3" || region != v.Region {
+ return namespaceEndpointRefusal(6)
+ }
+ return nil
+}
+
+func namespaceBucket(s string) bool {
+ alnum := func(c byte) bool { return c >= 'a' && c <= 'z' || c >= '0' && c <= '9' }
+ if len(s) < 3 || len(s) > 63 || !alnum(s[0]) || !alnum(s[len(s)-1]) {
+ return false
+ }
+ for i := range len(s) {
+ if !alnum(s[i]) && s[i] != '.' && s[i] != '-' {
+ return false
+ }
+ }
+ if strings.Contains(s, "..") || strings.HasPrefix(s, "xn--") || strings.HasPrefix(s, "sthree-") ||
+ strings.HasSuffix(s, "-s3alias") || strings.HasSuffix(s, "--ol-s3") || strings.HasSuffix(s, ".mrap") || strings.HasSuffix(s, "--x-s3") {
+ return false
+ }
+ parts := strings.Split(s, ".")
+ if len(parts) == 4 {
+ decimal := true
+ for _, part := range parts {
+ for i := range len(part) {
+ if part[i] < '0' || part[i] > '9' {
+ decimal = false
+ }
+ }
+ }
+ if decimal {
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/release/namespacecandidate_test.go b/internal/release/namespacecandidate_test.go
new file mode 100644
index 000000000..18b40db9b
--- /dev/null
+++ b/internal/release/namespacecandidate_test.go
@@ -0,0 +1,569 @@
+package release
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
+ "go/ast"
+ "go/parser"
+ "go/token"
+ "io"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ smithyauth "github.com/aws/smithy-go/auth"
+ endpoints "github.com/aws/smithy-go/endpoints"
+ smithyhttp "github.com/aws/smithy-go/transport/http"
+ "github.com/carlosframework/platform/internal/s3compat"
+ "github.com/carlosframework/platform/internal/sigfence"
+)
+
+func namespaceTestSnapshot(t *testing.T, bucket, region, partition, style string) s3compat.AWSProfileSnapshot {
+ t.Helper()
+ values := map[string]string{"CARLOS_DEPLOYMENT_BUCKET": bucket, "AWS_REGION": region, "CARLOS_S3_PARTITION": partition, "CARLOS_S3_ADDRESSING_STYLE": style}
+ snapshot, code := s3compat.CaptureAWSProfile(func(k string) (string, bool) { v, ok := values[k]; return v, ok })
+ if code != s3compat.AWSProfileAccepted {
+ t.Fatalf("capture: %d", code)
+ }
+ return snapshot
+}
+
+func namespaceTestCandidate(t *testing.T, bucket, region, partition, style string) NamespaceCandidate {
+ t.Helper()
+ c, code := NewNamespaceCandidate(namespaceTestSnapshot(t, bucket, region, partition, style))
+ if code != NamespaceAccepted {
+ t.Fatalf("construct: %d", code)
+ }
+ return c
+}
+
+type namespaceTestTransport func(*http.Request) (*http.Response, error)
+
+func (f namespaceTestTransport) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
+func namespaceTestResponse(r *http.Request, status int) *http.Response {
+ return &http.Response{StatusCode: status, Header: http.Header{"Etag": []string{`"test-etag"`}}, Body: io.NopCloser(strings.NewReader("")), Request: r}
+}
+
+func TestNamespaceCandidateNilCredentialsAnonymousWitness(t *testing.T) {
+ snapshot := namespaceTestSnapshot(t, "factory-test-bucket", "eu-west-1", "aws", "path")
+ v, _ := snapshot.Values()
+ opts := namespaceOptions(v)
+ opts.Credentials = nil
+ calls := 0
+ opts.HTTPClient.(*http.Client).Transport = namespaceTestTransport(func(r *http.Request) (*http.Response, error) {
+ calls++
+ if r.Header.Get("Authorization") != "" {
+ t.Error("nil credentials unexpectedly signed")
+ }
+ return namespaceTestResponse(r, 200), nil
+ })
+ store := &S3Store{Client: awss3.New(opts), Bucket: v.Bucket}
+ if _, err := store.Put(context.Background(), "test-key", []byte("test"), ""); err != nil || calls != 1 {
+ t.Fatalf("pinned SDK nil witness changed: err=%v dispatches=%d", err, calls)
+ }
+}
+
+type namespaceTestRefusalCounter struct{ calls *int }
+
+func (p namespaceTestRefusalCounter) Retrieve(ctx context.Context) (aws.Credentials, error) {
+ *p.calls++
+ return (namespaceRefusingCredentials{}).Retrieve(ctx)
+}
+
+type namespaceTestSigner struct{ calls *int }
+
+func (s namespaceTestSigner) SignHTTP(context.Context, aws.Credentials, *http.Request, string, string, string, time.Time, ...func(*v4.SignerOptions)) error {
+ *s.calls++
+ return errors.New("unexpected signing")
+}
+
+func TestNamespaceCandidateRefusingCredentialsZeroDispatch(t *testing.T) {
+ for _, key := range []string{"AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_PROFILE", "AWS_SHARED_CREDENTIALS_FILE", "AWS_CONFIG_FILE", "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "AWS_CONTAINER_CREDENTIALS_FULL_URI", "AWS_EC2_METADATA_SERVICE_ENDPOINT"} {
+ t.Setenv(key, "test-only-never-read")
+ }
+ c := namespaceTestCandidate(t, "factory-test-bucket", "eu-west-1", "aws", "path")
+ calls := 0
+ transport := namespaceTestTransport(func(r *http.Request) (*http.Response, error) {
+ calls++
+ t.Errorf("unexpected request headers: %v", r.Header)
+ return namespaceTestResponse(r, 200), nil
+ })
+ c.adapter.Client.Options().HTTPClient.(*http.Client).Transport = transport
+ if _, err := c.adapter.Put(context.Background(), "test-key", []byte("test"), ""); !errors.Is(err, namespaceCredentialsUnavailable) || calls != 0 {
+ t.Fatalf("candidate refusal err=%v dispatches=%d", err, calls)
+ }
+ retrieves, signs := 0, 0
+ opts := c.adapter.Client.Options()
+ opts.Credentials = namespaceTestRefusalCounter{&retrieves}
+ opts.HTTPSignerV4 = namespaceTestSigner{&signs}
+ store := &S3Store{Client: awss3.New(opts), Bucket: c.adapter.Bucket}
+ for _, operation := range []func() error{
+ func() error { _, e := store.Put(context.Background(), "key", []byte("test"), ""); return e },
+ func() error { _, _, e := store.Get(context.Background(), "key"); return e },
+ func() error { _, e := store.List(context.Background(), ""); return e },
+ func() error { return store.Delete(context.Background(), "key") },
+ } {
+ retrieves = 0
+ if err := operation(); !errors.Is(err, namespaceCredentialsUnavailable) || calls != 0 || signs != 0 || retrieves != 1 {
+ t.Fatalf("refusal err=%v dispatch=%d sign=%d retrieve=%d", err, calls, signs, retrieves)
+ }
+ }
+ provider := namespaceRefusingCredentials{}
+ if aws.IsCredentialsProvider(provider, (*aws.AnonymousCredentials)(nil)) {
+ t.Fatal("refusing provider treated as anonymous")
+ }
+ value, err := provider.Retrieve(context.Background())
+ if value != (aws.Credentials{}) || err != namespaceCredentialsUnavailable {
+ t.Fatal("provider produced material or variable error")
+ }
+ typ := reflect.TypeOf(provider)
+ if typ.NumField() != 0 || typ.NumMethod() != 1 || typ.Method(0).Name != "Retrieve" {
+ t.Fatal("provider has state or another interface")
+ }
+ errType := reflect.TypeOf(namespaceCredentialsUnavailable)
+ if errType.NumMethod() != 1 || errType.Method(0).Name != "Error" {
+ t.Fatal("refusal acquired retry/source interface")
+ }
+}
+
+func namespaceTestCredentialClient(c NamespaceCandidate, transport http.RoundTripper) *S3Store {
+ opts := c.adapter.Client.Options()
+ opts.Credentials = credentials.NewStaticCredentialsProvider("TESTKEY", "TESTSECRET", "")
+ original := opts.HTTPClient.(*http.Client)
+ copied := *original
+ copied.Transport = transport
+ opts.HTTPClient = &copied
+ return &S3Store{Client: awss3.New(opts), Bucket: c.adapter.Bucket}
+}
+
+func namespaceTestParams(v s3compat.AWSProfileValues) awss3.EndpointParameters {
+ return awss3.EndpointParameters{Region: aws.String(v.Region), Bucket: aws.String(v.Bucket), UseFIPS: aws.Bool(false), UseDualStack: aws.Bool(false),
+ ForcePathStyle: aws.Bool(v.AddressingStyle == "path"), Accelerate: aws.Bool(false), UseGlobalEndpoint: aws.Bool(false), UseArnRegion: aws.Bool(false),
+ DisableMultiRegionAccessPoints: aws.Bool(true), DisableS3ExpressSessionAuth: aws.Bool(true)}
+}
+
+func TestNamespaceCandidateOwnedOptionsAndSurface(t *testing.T) {
+ snapshot := namespaceTestSnapshot(t, "factory-test-bucket", "eu-west-1", "aws", "path")
+ values, _ := snapshot.Values()
+ o := namespaceOptions(values)
+ if o.BaseEndpoint != nil || o.EndpointResolver != nil || reflect.TypeOf(o.Credentials) != reflect.TypeOf(namespaceRefusingCredentials{}) || o.AuthSchemeResolver != nil || o.AuthSchemes != nil || o.AuthSchemePreference != nil ||
+ o.ExpressCredentials != nil || !reflect.DeepEqual(o.Interceptors, smithyhttp.InterceptorRegistry{}) || len(o.APIOptions) != 1 ||
+ reflect.ValueOf(o.APIOptions[0]).Pointer() != reflect.ValueOf(sigfence.WatchAttempts).Pointer() {
+ t.Fatal("unowned callback or credential")
+ }
+ if _, ok := o.HTTPSignerV4.(sigfence.Signer); !ok {
+ t.Fatal("signer changed")
+ }
+ if o.DefaultsMode != aws.DefaultsModeStandard || o.RetryMode != aws.RetryModeStandard || o.RetryMaxAttempts != 3 || o.Retryer.MaxAttempts() != 3 ||
+ o.RequestChecksumCalculation != aws.RequestChecksumCalculationWhenRequired || o.ResponseChecksumValidation != aws.ResponseChecksumValidationWhenRequired ||
+ o.ClientLogMode != 0 || o.UseAccelerate || o.UseARNRegion || o.UseDualstack || !o.UsePathStyle || !o.DisableMultiRegionAccessPoints ||
+ !aws.ToBool(o.DisableS3ExpressSessionAuth) || o.EndpointOptions.UseFIPSEndpoint != aws.FIPSEndpointStateDisabled ||
+ o.EndpointOptions.UseDualStackEndpoint != aws.DualStackEndpointStateDisabled {
+ t.Fatalf("profile options: %#v", o)
+ }
+ h := o.HTTPClient.(*http.Client)
+ tr := h.Transport.(*http.Transport)
+ if h.Jar != nil || h.CheckRedirect == nil || h.CheckRedirect(nil, nil) != http.ErrUseLastResponse || tr.Proxy != nil ||
+ tr.TLSClientConfig != nil && tr.TLSClientConfig.InsecureSkipVerify {
+ t.Fatal("transport escape")
+ }
+ second := namespaceOptions(values)
+ if second.HTTPClient == o.HTTPClient || second.Retryer == o.Retryer || second.HTTPClient.(*http.Client).Transport == tr {
+ t.Fatal("shared mutable owned state")
+ }
+ c := namespaceTestCandidate(t, values.Bucket, values.Region, values.Partition, values.AddressingStyle)
+ before, _ := c.NamespaceFacts()
+ copy, _ := c.NamespaceFacts()
+ rv := reflect.ValueOf(&copy).Elem()
+ for i := 0; i < rv.NumField(); i++ {
+ rv.Field(i).SetString("changed")
+ }
+ after, _ := c.NamespaceFacts()
+ if before != after {
+ t.Fatal("facts alias")
+ }
+ values.Bucket = "changed"
+ snapshot = s3compat.AWSProfileSnapshot{}
+ if c.adapter.Bucket != before.Bucket {
+ t.Fatal("caller mutation affected adapter")
+ }
+ for _, typ := range []reflect.Type{reflect.TypeOf(c), reflect.TypeOf(&c)} {
+ if typ.NumMethod() != 1 || typ.Method(0).Name != "NamespaceFacts" {
+ t.Fatalf("candidate methods: %v", typ)
+ }
+ if typ.Implements(reflect.TypeOf((*Store)(nil)).Elem()) {
+ t.Fatal("candidate exposes store")
+ }
+ }
+ typ := reflect.TypeOf(c)
+ for i := 0; i < typ.NumField(); i++ {
+ if typ.Field(i).IsExported() || typ.Field(i).Anonymous {
+ t.Fatal("candidate field escape")
+ }
+ }
+ ctor := reflect.TypeOf(NewNamespaceCandidate)
+ if ctor.NumIn() != 1 || ctor.In(0) != reflect.TypeOf(s3compat.AWSProfileSnapshot{}) || ctor.IsVariadic() {
+ t.Fatal("constructor accepts unsupported wrapper/options")
+ }
+ if _, code := NewNamespaceCandidate(snapshot); code == NamespaceAccepted {
+ t.Fatal("zero snapshot accepted")
+ }
+ if _, code := (NamespaceCandidate{}).NamespaceFacts(); code == NamespaceAccepted {
+ t.Fatal("zero candidate accepted")
+ }
+}
+
+func TestNamespaceCandidateRequestShapeAndFence(t *testing.T) {
+ for _, style := range []string{"path", "virtual"} {
+ t.Run(style, func(t *testing.T) {
+ snapshot := namespaceTestSnapshot(t, "factory-test-bucket", "eu-west-1", "aws", style)
+ for _, key := range []string{"CARLOS_DEPLOYMENT_BUCKET", "AWS_REGION", "CARLOS_S3_PARTITION", "CARLOS_S3_ADDRESSING_STYLE", "CARLOS_S3_ENDPOINT", "CARLOS_S3_QUIRKS", "AWS_ENDPOINT_URL", "AWS_ENDPOINT_URL_S3", "AWS_IGNORE_CONFIGURED_ENDPOINT_URLS", "AWS_USE_FIPS_ENDPOINT", "AWS_USE_DUALSTACK_ENDPOINT", "AWS_S3_USE_ARN_REGION", "AWS_CA_BUNDLE", "AWS_AUTH_SCHEME_PREFERENCE", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy", "AWS_LAMBDA_FUNCTION_NAME", "_X_AMZN_TRACE_ID", "AWS_EXECUTION_ENV", "AWS_DEFAULT_REGION", "AWS_PROFILE", "AWS_CONFIG_FILE", "AWS_SHARED_CREDENTIALS_FILE"} {
+ t.Setenv(key, "changed-after-capture")
+ }
+ c, code := NewNamespaceCandidate(snapshot)
+ if code != NamespaceAccepted {
+ t.Fatalf("postcapture construction: %d", code)
+ }
+ facts, _ := c.NamespaceFacts()
+ calls := 0
+ at := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
+ store := namespaceTestCredentialClient(c, namespaceTestTransport(func(r *http.Request) (*http.Response, error) {
+ calls++
+ host, path := "s3.eu-west-1.amazonaws.com", "/factory-test-bucket/root%20key"
+ if style == "virtual" {
+ host = "factory-test-bucket.s3.eu-west-1.amazonaws.com"
+ path = "/root%20key"
+ }
+ if r.URL.Scheme != "https" || r.URL.Host != host || r.URL.EscapedPath() != path || r.Header.Get("If-None-Match") != "*" ||
+ !strings.Contains(r.Header.Get("Authorization"), "/eu-west-1/s3/aws4_request") || r.Header.Get("X-Amz-Date") != "20260102T030405Z" {
+ t.Errorf("request %s headers %#v facts %#v", r.URL, r.Header, facts)
+ }
+ if r.Header.Get("X-Amz-Sdk-Checksum-Algorithm") != "" || r.Header.Get("Content-Encoding") == "aws-chunked" {
+ t.Error("unexpected flexible checksum")
+ }
+ body, _ := io.ReadAll(r.Body)
+ if string(body) != "test" {
+ t.Errorf("body %q", body)
+ }
+ if calls == 1 {
+ return namespaceTestResponse(r, 503), nil
+ }
+ return namespaceTestResponse(r, 200), nil
+ }))
+ ctx, doubt := sigfence.Track(sigfence.With(context.Background(), at))
+ if _, err := store.Put(ctx, "root key", []byte("test"), ""); err != nil {
+ t.Fatal(err)
+ }
+ if calls != 2 || !doubt.Raised() {
+ t.Fatalf("attempt watcher: calls=%d doubt=%v", calls, doubt.Raised())
+ }
+ if facts.Bucket != "factory-test-bucket" || facts.Region != "eu-west-1" || facts.Mode != "aws" || facts.EffectivePrefix != "" || facts.AddressingStyle != style {
+ t.Fatalf("facts %#v", facts)
+ }
+ })
+ }
+}
+
+func TestNamespaceCandidateRefusesEveryRedirect(t *testing.T) {
+ for _, status := range []int{301, 302, 307, 308} {
+ for _, target := range []string{"/another-bucket/key", "https://other.example.test/another-bucket/key"} {
+ t.Run(fmt.Sprintf("%d/%s", status, target), func(t *testing.T) {
+ c := namespaceTestCandidate(t, "factory-test-bucket", "eu-west-1", "aws", "path")
+ calls := 0
+ store := namespaceTestCredentialClient(c, namespaceTestTransport(func(r *http.Request) (*http.Response, error) {
+ calls++
+ resp := namespaceTestResponse(r, status)
+ resp.Header.Set("Location", target)
+ return resp, nil
+ }))
+ if _, err := store.Put(context.Background(), "key", []byte("test"), ""); err == nil {
+ t.Fatal("redirect accepted")
+ }
+ if calls != 1 {
+ t.Fatalf("redirect dispatched %d requests", calls)
+ }
+ })
+ }
+ }
+}
+
+func TestNamespaceCandidateResolverPostconditions(t *testing.T) {
+ for _, style := range []string{"path", "virtual"} {
+ v, _ := namespaceTestSnapshot(t, "factory-test-bucket", "eu-west-1", "aws", style).Values()
+ resolver := namespaceResolver{values: v}
+ valid := func() endpoints.Endpoint {
+ ep, err := resolver.ResolveEndpoint(context.Background(), namespaceTestParams(v))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return ep
+ }
+ mutations := map[string]func(*endpoints.Endpoint){
+ "scheme": func(e *endpoints.Endpoint) { e.URI.Scheme = "http" },
+ "suffix-boundary": func(e *endpoints.Endpoint) { e.URI.Host = "factory-test-bucket.notamazonaws.com" },
+ "suffix-extension": func(e *endpoints.Endpoint) { e.URI.Host += ".evil.test" },
+ "bucket": func(e *endpoints.Endpoint) {
+ if style == "path" {
+ e.URI.Path = "/other-bucket"
+ } else {
+ e.URI.Host = "other-bucket.s3.eu-west-1.amazonaws.com"
+ }
+ },
+ "prefix": func(e *endpoints.Endpoint) { e.URI.Path += "/prefix" },
+ "userinfo": func(e *endpoints.Endpoint) { e.URI.User = url.User("user") },
+ "port": func(e *endpoints.Endpoint) { e.URI.Host += ":443" },
+ "query": func(e *endpoints.Endpoint) { e.URI.RawQuery = "x=1" },
+ "headers": func(e *endpoints.Endpoint) { e.Headers = http.Header{"Extra": nil} },
+ "express": func(e *endpoints.Endpoint) { e.Properties.Set("backend", "S3Express") },
+ "auth-absent": func(e *endpoints.Endpoint) { smithyauth.SetAuthOptions(&e.Properties, nil) },
+ "auth-extra": func(e *endpoints.Endpoint) {
+ a, _ := smithyauth.GetAuthOptions(&e.Properties)
+ smithyauth.SetAuthOptions(&e.Properties, append(a, a[0]))
+ },
+ "auth-nil": func(e *endpoints.Endpoint) { smithyauth.SetAuthOptions(&e.Properties, []*smithyauth.Option{nil}) },
+ "auth-anonymous": func(e *endpoints.Endpoint) {
+ a, _ := smithyauth.GetAuthOptions(&e.Properties)
+ a[0].SchemeID = "smithy.api#noAuth"
+ },
+ "signing-name": func(e *endpoints.Endpoint) {
+ a, _ := smithyauth.GetAuthOptions(&e.Properties)
+ smithyhttp.SetSigV4SigningName(&a[0].SignerProperties, "other")
+ },
+ "signing-region": func(e *endpoints.Endpoint) {
+ a, _ := smithyauth.GetAuthOptions(&e.Properties)
+ smithyhttp.SetSigV4SigningRegion(&a[0].SignerProperties, "us-east-1")
+ },
+ }
+ for name, mutate := range mutations {
+ t.Run(style+"/"+name, func(t *testing.T) {
+ ep := valid()
+ mutate(&ep)
+ if resolver.checkEndpoint(ep) == nil {
+ t.Fatal("accepted changed endpoint")
+ }
+ })
+ }
+ for name, mutate := range map[string]func(*awss3.EndpointParameters){
+ "region": func(p *awss3.EndpointParameters) { p.Region = aws.String("unknown") },
+ "bucket": func(p *awss3.EndpointParameters) { p.Bucket = aws.String("other-bucket") },
+ "base": func(p *awss3.EndpointParameters) { p.Endpoint = aws.String("https://example.test") },
+ "addressing": func(p *awss3.EndpointParameters) { p.ForcePathStyle = aws.Bool(style != "path") },
+ "fips": func(p *awss3.EndpointParameters) { p.UseFIPS = aws.Bool(true) },
+ "dualstack": func(p *awss3.EndpointParameters) { p.UseDualStack = aws.Bool(true) },
+ "accelerate": func(p *awss3.EndpointParameters) { p.Accelerate = aws.Bool(true) },
+ "global": func(p *awss3.EndpointParameters) { p.UseGlobalEndpoint = aws.Bool(true) },
+ "mrap": func(p *awss3.EndpointParameters) { p.DisableMultiRegionAccessPoints = aws.Bool(false) },
+ "express": func(p *awss3.EndpointParameters) { p.DisableS3ExpressSessionAuth = aws.Bool(false) },
+ } {
+ t.Run(style+"/params-"+name, func(t *testing.T) {
+ p := namespaceTestParams(v)
+ mutate(&p)
+ if _, err := resolver.ResolveEndpoint(context.Background(), p); err == nil {
+ t.Fatal("accepted routing override")
+ }
+ })
+ }
+ }
+}
+
+func TestNamespaceCandidateBucketRefusals(t *testing.T) {
+ for _, bucket := range []string{"ab", strings.Repeat("a", 64), "Bad-bucket", "bad_bucket", "-bucket", "bucket-", "bucket..name", "192.168.5.4", "999.999.999.999", "xn--bucket", "sthree-bucket", "bucket-s3alias", "bucket--ol-s3", "bucket.mrap", "bucket--x-s3"} {
+ for _, style := range []string{"path", "virtual"} {
+ if _, code := NewNamespaceCandidate(namespaceTestSnapshot(t, bucket, "eu-west-1", "aws", style)); code != NamespaceRefuseBucket {
+ t.Fatalf("accepted bucket %q: %d", bucket, code)
+ }
+ }
+ }
+ if _, code := NewNamespaceCandidate(namespaceTestSnapshot(t, "dotted.bucket", "eu-west-1", "aws", "virtual")); code != NamespaceRefuseBucket {
+ t.Fatal("dotted virtual")
+ }
+ namespaceTestCandidate(t, "dotted.bucket", "eu-west-1", "aws", "path")
+}
+
+func TestNamespaceCandidateStaticBoundary(t *testing.T) {
+ fset := token.NewFileSet()
+ file, err := parser.ParseFile(fset, "namespacecandidate.go", nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ aliases := map[string]string{}
+ for _, imp := range file.Imports {
+ path := strings.Trim(imp.Path.Value, `"`)
+ name := filepath.Base(path)
+ if imp.Name != nil {
+ name = imp.Name.Name
+ }
+ aliases[name] = path
+ if path == "os" || path == "github.com/aws/aws-sdk-go-v2/config" || path == "github.com/carlosframework/platform/internal/console" {
+ t.Fatalf("forbidden factory import %s", path)
+ }
+ }
+ ast.Inspect(file, func(n ast.Node) bool {
+ if sel, ok := n.(*ast.SelectorExpr); ok {
+ if sel.Sel.Name == "LoadDefaultConfig" || sel.Sel.Name == "NewFromConfig" || sel.Sel.Name == "Getenv" || sel.Sel.Name == "LookupEnv" {
+ t.Errorf("forbidden %s", sel.Sel.Name)
+ }
+ if id, ok := sel.X.(*ast.Ident); ok && aliases[id.Name] == "github.com/carlosframework/platform/internal/s3compat" && (sel.Sel.Name == "Options" || sel.Sel.Name == "Quirks" || sel.Sel.Name == "Endpoint") {
+ t.Errorf("ambient %s", sel.Sel.Name)
+ }
+ }
+ return true
+ })
+}
+
+func TestNamespaceCandidateEveryClosedRegion(t *testing.T) {
+ cases := []struct{ partition, region, suffix string }{
+ {"aws", "af-south-1", "amazonaws.com"},
+ {"aws", "ap-east-1", "amazonaws.com"},
+ {"aws", "ap-east-2", "amazonaws.com"},
+ {"aws", "ap-northeast-1", "amazonaws.com"},
+ {"aws", "ap-northeast-2", "amazonaws.com"},
+ {"aws", "ap-northeast-3", "amazonaws.com"},
+ {"aws", "ap-south-1", "amazonaws.com"},
+ {"aws", "ap-south-2", "amazonaws.com"},
+ {"aws", "ap-southeast-1", "amazonaws.com"},
+ {"aws", "ap-southeast-2", "amazonaws.com"},
+ {"aws", "ap-southeast-3", "amazonaws.com"},
+ {"aws", "ap-southeast-4", "amazonaws.com"},
+ {"aws", "ap-southeast-5", "amazonaws.com"},
+ {"aws", "ap-southeast-6", "amazonaws.com"},
+ {"aws", "ap-southeast-7", "amazonaws.com"},
+ {"aws", "ca-central-1", "amazonaws.com"},
+ {"aws", "ca-west-1", "amazonaws.com"},
+ {"aws", "eu-central-1", "amazonaws.com"},
+ {"aws", "eu-central-2", "amazonaws.com"},
+ {"aws", "eu-north-1", "amazonaws.com"},
+ {"aws", "eu-south-1", "amazonaws.com"},
+ {"aws", "eu-south-2", "amazonaws.com"},
+ {"aws", "eu-west-1", "amazonaws.com"},
+ {"aws", "eu-west-2", "amazonaws.com"},
+ {"aws", "eu-west-3", "amazonaws.com"},
+ {"aws", "il-central-1", "amazonaws.com"},
+ {"aws", "me-central-1", "amazonaws.com"},
+ {"aws", "me-south-1", "amazonaws.com"},
+ {"aws", "mx-central-1", "amazonaws.com"},
+ {"aws", "sa-east-1", "amazonaws.com"},
+ {"aws", "us-east-1", "amazonaws.com"},
+ {"aws", "us-east-2", "amazonaws.com"},
+ {"aws", "us-west-1", "amazonaws.com"},
+ {"aws", "us-west-2", "amazonaws.com"},
+ {"aws-cn", "cn-north-1", "amazonaws.com.cn"},
+ {"aws-cn", "cn-northwest-1", "amazonaws.com.cn"},
+ {"aws-eusc", "eusc-de-east-1", "amazonaws.eu"},
+ {"aws-iso", "us-iso-east-1", "c2s.ic.gov"},
+ {"aws-iso", "us-iso-west-1", "c2s.ic.gov"},
+ {"aws-iso-b", "us-isob-east-1", "sc2s.sgov.gov"},
+ {"aws-iso-b", "us-isob-west-1", "sc2s.sgov.gov"},
+ {"aws-iso-e", "eu-isoe-west-1", "cloud.adc-e.uk"},
+ {"aws-iso-f", "us-isof-east-1", "csp.hci.ic.gov"},
+ {"aws-iso-f", "us-isof-south-1", "csp.hci.ic.gov"},
+ {"aws-us-gov", "us-gov-east-1", "amazonaws.com"},
+ {"aws-us-gov", "us-gov-west-1", "amazonaws.com"},
+ }
+ for _, tc := range cases {
+ for _, style := range []string{"path", "virtual"} {
+ t.Run(tc.partition+"/"+tc.region+"/"+style, func(t *testing.T) {
+ snapshot := namespaceTestSnapshot(t, "factory-test-bucket", tc.region, tc.partition, style)
+ v, _ := snapshot.Values()
+ if v.DNSSuffix != tc.suffix {
+ t.Fatalf("suffix %q", v.DNSSuffix)
+ }
+ r := namespaceResolver{values: v}
+ ep, err := r.ResolveEndpoint(context.Background(), namespaceTestParams(v))
+ if err != nil {
+ t.Fatal(err)
+ }
+ wantHost := "s3." + tc.region + "." + tc.suffix
+ wantPath := "/factory-test-bucket"
+ if style == "virtual" {
+ wantHost = "factory-test-bucket." + wantHost
+ wantPath = "/"
+ }
+ if ep.URI.Host != wantHost || strings.TrimSuffix(ep.URI.Path, "/") != strings.TrimSuffix(wantPath, "/") {
+ t.Fatalf("endpoint %s, want host %s path %s", ep.URI.String(), wantHost, wantPath)
+ }
+ c, code := NewNamespaceCandidate(snapshot)
+ if code != NamespaceAccepted {
+ t.Fatalf("construct %d", code)
+ }
+ calls := 0
+ store := namespaceTestCredentialClient(c, namespaceTestTransport(func(req *http.Request) (*http.Response, error) {
+ calls++
+ if req.URL.Host != wantHost || !strings.Contains(req.Header.Get("Authorization"), "/"+tc.region+"/s3/aws4_request") {
+ t.Errorf("request %s %v", req.URL, req.Header)
+ }
+ return namespaceTestResponse(req, 200), nil
+ }))
+ if _, err := store.Put(context.Background(), "key", []byte("test"), ""); err != nil {
+ t.Fatal(err)
+ }
+ if calls != 1 {
+ t.Fatalf("calls %d", calls)
+ }
+ })
+ }
+ }
+}
+
+func TestNamespaceCandidateCredentialProviderStaticBoundary(t *testing.T) {
+ fset := token.NewFileSet()
+ file, err := parser.ParseFile(fset, "namespacecredentials.go", nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, imp := range file.Imports {
+ path := strings.Trim(imp.Path.Value, `"`)
+ if path != "context" && path != "github.com/aws/aws-sdk-go-v2/aws" {
+ t.Fatalf("provider I/O import %s", path)
+ }
+ }
+ for _, decl := range file.Decls {
+ if g, ok := decl.(*ast.GenDecl); ok && g.Tok == token.VAR {
+ t.Fatal("mutable provider/refusal state")
+ }
+ }
+ ast.Inspect(file, func(n ast.Node) bool {
+ if c, ok := n.(*ast.CallExpr); ok {
+ t.Errorf("provider has effectful call %s", fset.Position(c.Pos()))
+ }
+ return true
+ })
+}
+
+func TestNamespaceCandidateProductionCallersStayUnmounted(t *testing.T) {
+ fset := token.NewFileSet()
+ for _, dir := range []string{".", "../s3compat"} {
+ files, err := filepath.Glob(filepath.Join(dir, "*.go"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, name := range files {
+ base := filepath.Base(name)
+ if strings.HasSuffix(name, "_test.go") || base == "namespacecandidate.go" || base == "namespacecredentials.go" || base == "awsprofile.go" {
+ continue
+ }
+ file, err := parser.ParseFile(fset, name, nil, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ ast.Inspect(file, func(n ast.Node) bool {
+ if id, ok := n.(*ast.Ident); ok {
+ switch id.Name {
+ case "NamespaceCandidate", "NewNamespaceCandidate", "namespaceOptions", "namespaceResolver", "namespaceRefusingCredentials", "CaptureAWSProfile", "AWSProfileSnapshot":
+ t.Errorf("new production reference %s at %s", id.Name, fset.Position(id.Pos()))
+ }
+ }
+ return true
+ })
+ }
+ }
+}
diff --git a/internal/release/namespacecredentials.go b/internal/release/namespacecredentials.go
new file mode 100644
index 000000000..c7a1e75af
--- /dev/null
+++ b/internal/release/namespacecredentials.go
@@ -0,0 +1,19 @@
+package release
+
+import (
+ "context"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+)
+
+type namespaceCredentialsRefusal uint8
+
+const namespaceCredentialsUnavailable namespaceCredentialsRefusal = 1
+
+func (namespaceCredentialsRefusal) Error() string { return "1" }
+
+type namespaceRefusingCredentials struct{}
+
+func (namespaceRefusingCredentials) Retrieve(context.Context) (aws.Credentials, error) {
+ return aws.Credentials{}, namespaceCredentialsUnavailable
+}
diff --git a/internal/s3compat/awsprofile.go b/internal/s3compat/awsprofile.go
new file mode 100644
index 000000000..cca619f25
--- /dev/null
+++ b/internal/s3compat/awsprofile.go
@@ -0,0 +1,114 @@
+package s3compat
+
+type AWSProfileRefusal uint8
+
+const (
+ AWSProfileAccepted AWSProfileRefusal = iota
+ AWSProfileRefuseInput
+ AWSProfileRefuseOverride
+ AWSProfileRefuseRegion
+)
+
+type AWSProfileValues struct {
+ Bucket string
+ Region string
+ Partition string
+ AddressingStyle string
+ DNSSuffix string
+ Revision string
+}
+
+type AWSProfileSnapshot struct {
+ values AWSProfileValues
+ captured bool
+}
+
+func (s AWSProfileSnapshot) Values() (AWSProfileValues, AWSProfileRefusal) {
+ if !s.captured {
+ return AWSProfileValues{}, AWSProfileRefuseInput
+ }
+ return s.values, AWSProfileAccepted
+}
+
+func CaptureAWSProfile(lookup func(string) (string, bool)) (AWSProfileSnapshot, AWSProfileRefusal) {
+ if lookup == nil {
+ return AWSProfileSnapshot{}, AWSProfileRefuseInput
+ }
+ var values [4]string
+ missing := false
+ for i, key := range [...]string{"CARLOS_DEPLOYMENT_BUCKET", "AWS_REGION", "CARLOS_S3_PARTITION", "CARLOS_S3_ADDRESSING_STYLE"} {
+ value, present := lookup(key)
+ values[i] = value
+ missing = missing || !present || value == ""
+ }
+ overridden := false
+ for _, key := range [...]string{
+ "CARLOS_S3_ENDPOINT", "CARLOS_S3_QUIRKS", "AWS_ENDPOINT_URL", "AWS_ENDPOINT_URL_S3",
+ "AWS_IGNORE_CONFIGURED_ENDPOINT_URLS", "AWS_USE_FIPS_ENDPOINT", "AWS_USE_DUALSTACK_ENDPOINT",
+ "AWS_S3_USE_ARN_REGION", "AWS_CA_BUNDLE", "AWS_AUTH_SCHEME_PREFERENCE",
+ "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy",
+ } {
+ _, present := lookup(key)
+ overridden = overridden || present
+ }
+ if overridden {
+ return AWSProfileSnapshot{}, AWSProfileRefuseOverride
+ }
+ if missing || (values[3] != "path" && values[3] != "virtual") {
+ return AWSProfileSnapshot{}, AWSProfileRefuseInput
+ }
+ suffix := awsProfileDNSSuffix(values[2], values[1])
+ if suffix == "" {
+ return AWSProfileSnapshot{}, AWSProfileRefuseRegion
+ }
+ return AWSProfileSnapshot{values: AWSProfileValues{
+ Bucket: values[0], Region: values[1], Partition: values[2], AddressingStyle: values[3],
+ DNSSuffix: suffix, Revision: "aws-root-v1-sdk-s3-1-105-0",
+ }, captured: true}, AWSProfileAccepted
+}
+
+func awsProfileDNSSuffix(partition, region string) string {
+ switch partition {
+ case "aws":
+ switch region {
+ case "af-south-1", "ap-east-1", "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-south-2", "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", "ap-southeast-5", "ap-southeast-6", "ap-southeast-7", "ca-central-1", "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", "eu-south-1", "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", "il-central-1", "me-central-1", "me-south-1", "mx-central-1", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2":
+ return "amazonaws.com"
+ }
+ case "aws-cn":
+ switch region {
+ case "cn-north-1", "cn-northwest-1":
+ return "amazonaws.com.cn"
+ }
+ case "aws-eusc":
+ switch region {
+ case "eusc-de-east-1":
+ return "amazonaws.eu"
+ }
+ case "aws-iso":
+ switch region {
+ case "us-iso-east-1", "us-iso-west-1":
+ return "c2s.ic.gov"
+ }
+ case "aws-iso-b":
+ switch region {
+ case "us-isob-east-1", "us-isob-west-1":
+ return "sc2s.sgov.gov"
+ }
+ case "aws-iso-e":
+ switch region {
+ case "eu-isoe-west-1":
+ return "cloud.adc-e.uk"
+ }
+ case "aws-iso-f":
+ switch region {
+ case "us-isof-east-1", "us-isof-south-1":
+ return "csp.hci.ic.gov"
+ }
+ case "aws-us-gov":
+ switch region {
+ case "us-gov-east-1", "us-gov-west-1":
+ return "amazonaws.com"
+ }
+ }
+ return ""
+}
diff --git a/internal/s3compat/awsprofile_test.go b/internal/s3compat/awsprofile_test.go
new file mode 100644
index 000000000..f1db0c5cc
--- /dev/null
+++ b/internal/s3compat/awsprofile_test.go
@@ -0,0 +1,101 @@
+package s3compat
+
+import (
+ "reflect"
+ "testing"
+)
+
+func awsProfileTestEnv() map[string]string {
+ return map[string]string{"CARLOS_DEPLOYMENT_BUCKET": "factory-test-bucket", "AWS_REGION": "eu-west-1", "CARLOS_S3_PARTITION": "aws", "CARLOS_S3_ADDRESSING_STYLE": "path"}
+}
+
+func awsProfileTestKeys() []string {
+ return []string{"CARLOS_DEPLOYMENT_BUCKET", "AWS_REGION", "CARLOS_S3_PARTITION", "CARLOS_S3_ADDRESSING_STYLE",
+ "CARLOS_S3_ENDPOINT", "CARLOS_S3_QUIRKS", "AWS_ENDPOINT_URL", "AWS_ENDPOINT_URL_S3", "AWS_IGNORE_CONFIGURED_ENDPOINT_URLS",
+ "AWS_USE_FIPS_ENDPOINT", "AWS_USE_DUALSTACK_ENDPOINT", "AWS_S3_USE_ARN_REGION", "AWS_CA_BUNDLE", "AWS_AUTH_SCHEME_PREFERENCE",
+ "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", "http_proxy", "https_proxy", "no_proxy"}
+}
+
+func TestAWSProfileCaptureReadsExactlyOnceAndCopies(t *testing.T) {
+ values := awsProfileTestEnv()
+ reads := map[string]int{}
+ snapshot, code := CaptureAWSProfile(func(key string) (string, bool) { reads[key]++; v, ok := values[key]; return v, ok })
+ if code != AWSProfileAccepted {
+ t.Fatalf("capture: %d", code)
+ }
+ want := map[string]int{}
+ for _, key := range awsProfileTestKeys() {
+ want[key] = 1
+ values[key] = "changed"
+ }
+ if !reflect.DeepEqual(reads, want) {
+ t.Fatalf("reads = %#v", reads)
+ }
+ before, _ := snapshot.Values()
+ copy, _ := snapshot.Values()
+ rv := reflect.ValueOf(&copy).Elem()
+ for i := 0; i < rv.NumField(); i++ {
+ rv.Field(i).SetString("mutated")
+ }
+ after, code := snapshot.Values()
+ if code != AWSProfileAccepted || after != before || before.Bucket != "factory-test-bucket" || before.Region != "eu-west-1" || before.Revision != "aws-root-v1-sdk-s3-1-105-0" {
+ t.Fatalf("snapshot changed: %#v", after)
+ }
+ if !reflect.DeepEqual(reads, want) {
+ t.Fatal("lookup retained")
+ }
+}
+
+func TestAWSProfileCaptureRefusals(t *testing.T) {
+ for _, key := range awsProfileTestKeys()[4:] {
+ for _, value := range []string{"", "configured"} {
+ t.Run(key+"/"+value, func(t *testing.T) {
+ values := awsProfileTestEnv()
+ values[key] = value
+ reads := map[string]int{}
+ snapshot, code := CaptureAWSProfile(func(k string) (string, bool) { reads[k]++; v, ok := values[k]; return v, ok })
+ if code != AWSProfileRefuseOverride {
+ t.Fatalf("accepted override: %d", code)
+ }
+ if _, code := snapshot.Values(); code == AWSProfileAccepted {
+ t.Fatal("refused capture returned usable snapshot")
+ }
+ for _, k := range awsProfileTestKeys() {
+ if reads[k] != 1 {
+ t.Fatalf("reads[%s]=%d", k, reads[k])
+ }
+ }
+ })
+ }
+ }
+ for _, key := range awsProfileTestKeys()[:4] {
+ for _, empty := range []bool{false, true} {
+ values := awsProfileTestEnv()
+ delete(values, key)
+ if empty {
+ values[key] = ""
+ }
+ if _, code := CaptureAWSProfile(func(k string) (string, bool) { v, ok := values[k]; return v, ok }); code == AWSProfileAccepted {
+ t.Fatalf("accepted missing %s", key)
+ }
+ }
+ }
+ for _, change := range []map[string]string{
+ {"AWS_REGION": "eu-future-99"}, {"AWS_REGION": "aws-global"}, {"CARLOS_S3_PARTITION": "unknown"},
+ {"CARLOS_S3_PARTITION": "aws-cn"}, {"AWS_REGION": " us-east-1"}, {"CARLOS_S3_ADDRESSING_STYLE": "auto"},
+ } {
+ values := awsProfileTestEnv()
+ for k, v := range change {
+ values[k] = v
+ }
+ if _, code := CaptureAWSProfile(func(k string) (string, bool) { v, ok := values[k]; return v, ok }); code == AWSProfileAccepted {
+ t.Fatalf("accepted %#v", change)
+ }
+ }
+ if _, code := CaptureAWSProfile(nil); code == AWSProfileAccepted {
+ t.Fatal("nil lookup")
+ }
+ if _, code := (AWSProfileSnapshot{}).Values(); code == AWSProfileAccepted {
+ t.Fatal("zero snapshot")
+ }
+}