mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-20 19:25:52 +00:00
a4a4288d7d
* feat(ecr): auth bridge + GetAuthorizationToken JWT (Sprint 2c)
Mint a self-contained ES256 JWT from GetAuthorizationToken and verify it on
the /v2 OCI registry via a new ECRAuthBridgeMiddleware (Bearer | Basic AWS:jwt).
Signing keys live in the cluster-replicated awsgw-keys KV bucket under
jwt-signing/{kid}, encrypted with the IAM master key.
The registry is now scoped per-request to the token's account (replacing the
fixed bootstrap account); bucket-ensure is cached per account. Cross-account
token/host mismatch is rejected 403; unauthenticated /v2 access gets a 401
Bearer challenge.
* fix(ecr): use Basic auth challenge on /v2 (not Bearer token-endpoint flow)
ECR authenticates the OCI registry with Basic AWS:<token>; a Bearer realm
made docker chase an unimplemented /v2/token endpoint and fail login.
76 lines
2.5 KiB
Go
76 lines
2.5 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestHandleGetAuthorizationToken_MintsUsableToken(t *testing.T) {
|
|
iss, verify := newECRAuth(t)
|
|
gw := &GatewayConfig{
|
|
Region: ecrTestRegion, InternalSuffix: ecrTestSuffix,
|
|
ECRTokenIssuer: iss, ECRTokenVerifier: verify, DisableLogging: true,
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("{}"))
|
|
ctx := context.WithValue(req.Context(), ctxAccountID, ecrTestAccount)
|
|
ctx = context.WithValue(ctx, ctxPrincipalType, principalTypeUser)
|
|
w := httptest.NewRecorder()
|
|
|
|
require.NoError(t, gw.handleGetAuthorizationToken(w, req.WithContext(ctx)))
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var out struct {
|
|
AuthorizationData []struct {
|
|
AuthorizationToken string `json:"authorizationToken"`
|
|
ProxyEndpoint string `json:"proxyEndpoint"`
|
|
ExpiresAt float64 `json:"expiresAt"`
|
|
} `json:"authorizationData"`
|
|
}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &out))
|
|
require.Len(t, out.AuthorizationData, 1)
|
|
|
|
ad := out.AuthorizationData[0]
|
|
assert.Equal(t, "https://"+ecrTestAccount+".dkr.ecr."+ecrTestRegion+"."+ecrTestSuffix, ad.ProxyEndpoint)
|
|
assert.Positive(t, ad.ExpiresAt)
|
|
|
|
// authorizationToken decodes to "AWS:<jwt>" and the jwt verifies to the account.
|
|
decoded, err := base64.StdEncoding.DecodeString(ad.AuthorizationToken)
|
|
require.NoError(t, err)
|
|
user, jwtStr, found := strings.Cut(string(decoded), ":")
|
|
require.True(t, found)
|
|
assert.Equal(t, "AWS", user)
|
|
|
|
claims, err := verify.Verify(jwtStr)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, ecrTestAccount, claims.AccountID)
|
|
}
|
|
|
|
func TestHandleGetAuthorizationToken_NoIssuerNotImplemented(t *testing.T) {
|
|
gw := &GatewayConfig{DisableLogging: true}
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("{}"))
|
|
ctx := context.WithValue(req.Context(), ctxAccountID, ecrTestAccount)
|
|
err := gw.handleGetAuthorizationToken(httptest.NewRecorder(), req.WithContext(ctx))
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestECRRequest_GetAuthorizationTokenDispatched(t *testing.T) {
|
|
iss, verify := newECRAuth(t)
|
|
gw := &GatewayConfig{
|
|
Region: ecrTestRegion, InternalSuffix: ecrTestSuffix,
|
|
ECRTokenIssuer: iss, ECRTokenVerifier: verify, DisableLogging: true,
|
|
}
|
|
w := httptest.NewRecorder()
|
|
req := setupECRRequest("AmazonEC2ContainerRegistry_V20150921.GetAuthorizationToken", "{}")
|
|
require.NoError(t, gw.ECR_Request(w, req))
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
}
|