fix: fail closed in the resource authorization check (#755)

This commit is contained in:
Julian Sommer
2026-08-10 12:28:03 +10:00
committed by GitHub
parent 9b2334b964
commit f6ef90a605
20 changed files with 237 additions and 136 deletions
+4 -4
View File
@@ -19,7 +19,7 @@ func setupACMRequest(target, body string) *http.Request {
}
ctx := context.WithValue(req.Context(), ctxService, "acm")
ctx = context.WithValue(ctx, ctxAccountID, "123456789012")
return req.WithContext(ctx)
return withTestIdentity(req.WithContext(ctx))
}
func TestACMActionFromTarget(t *testing.T) {
@@ -48,7 +48,7 @@ func TestACMActionsMap_AllActionsRegistered(t *testing.T) {
}
func TestACMRequest_MissingTarget(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
err := gw.ACM_Request(w, setupACMRequest("", ""))
require.Error(t, err)
@@ -56,7 +56,7 @@ func TestACMRequest_MissingTarget(t *testing.T) {
}
func TestACMRequest_UnknownAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
err := gw.ACM_Request(w, setupACMRequest("CertificateManager.BogusAction", "{}"))
require.Error(t, err)
@@ -66,7 +66,7 @@ func TestACMRequest_UnknownAction(t *testing.T) {
// A known action with no NATS connection passes routing + policy and fails at
// the NATS-availability guard.
func TestACMRequest_KnownActionNoNATS(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
err := gw.ACM_Request(w, setupACMRequest("CertificateManager.ListCertificates", "{}"))
require.Error(t, err)
+2 -2
View File
@@ -580,7 +580,7 @@ func TestSigV4Auth_MalformedTimestamp(t *testing.T) {
// --- writeSigV4Error Response Format Tests ---
func TestWriteSigV4Error_ResponseFormat(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, Region: testRegion}
gw := &GatewayConfig{DisableLogging: true, Region: testRegion, IAMService: allowAllIAMService()}
testCases := []struct {
errorCode string
@@ -630,7 +630,7 @@ func TestWriteSigV4Error_ResponseFormat(t *testing.T) {
}
func TestWriteSigV4Error_IgnoresClientRequestID(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, Region: testRegion}
gw := &GatewayConfig{DisableLogging: true, Region: testRegion, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
+17 -10
View File
@@ -74,10 +74,10 @@ func (grantAllModels) Granted(_ context.Context, _, _ string) (bool, error) { re
// newBedrockRequestGateway builds a GatewayConfig with a real NATS connection
// (satisfying Bedrock_Request/BedrockRuntime_Request's nil-check only — no
// NATS subject handling is required for these routes), no IAMService (so
// checkPolicy is a no-op), BedrockEndpoints pinned at the given vLLM stub, and
// every model granted — access is deny-by-default, so without a resolver these
// routes would all return before reaching the code under test.
// NATS subject handling is required for these routes), an allow-everything
// IAMService so the policy gate passes, BedrockEndpoints pinned at the given
// vLLM stub, and every model granted — access is deny-by-default, so without a
// resolver these routes would all return before reaching the code under test.
// ListFoundationModels/GetFoundationModel read the weights resolver through a
// package-level setter rather than a GatewayConfig field, so it is installed
// here and reset on cleanup.
@@ -89,6 +89,7 @@ func newBedrockRequestGateway(t *testing.T, vllmURL string) *GatewayConfig {
return &GatewayConfig{
NATSConn: nc,
DisableLogging: true,
IAMService: allowAllIAMService(),
BedrockEndpoints: map[string]string{
bedrockTestLlamaModelID: vllmURL,
},
@@ -99,6 +100,8 @@ func newBedrockRequestGateway(t *testing.T, vllmURL string) *GatewayConfig {
func bedrockRequestWithAccount(method, path, body string) *http.Request {
req := httptest.NewRequest(method, path, strings.NewReader(body))
ctx := context.WithValue(req.Context(), ctxAccountID, bedrockTestAccount)
ctx = context.WithValue(ctx, ctxIdentity, "alice")
ctx = context.WithValue(ctx, ctxPrincipalType, principalTypeUser)
return req.WithContext(ctx)
}
@@ -206,15 +209,17 @@ func TestBedrockRequest_UnknownRouteReturnsInvalidAction(t *testing.T) {
assert.Equal(t, awserrors.ErrorInvalidAction, err.Error())
}
func TestBedrockRequest_MissingAccountIDReturnsServerInternal(t *testing.T) {
func TestBedrockRequest_MissingAccountIDReturnsInternalError(t *testing.T) {
ts := newVLLMStub(t)
gw := newBedrockRequestGateway(t, ts.URL)
req := httptest.NewRequest(http.MethodGet, "/foundation-models", nil)
req := withTestIdentity(httptest.NewRequest(http.MethodGet, "/foundation-models", nil))
w := httptest.NewRecorder()
// Account-less requests are rejected by the policy gate, which runs ahead of
// the handler's own account guard.
err := gw.Bedrock_Request(w, req)
require.Error(t, err)
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
assert.Equal(t, awserrors.ErrorInternalError, err.Error())
}
func TestBedrockRuntimeRequest_Converse(t *testing.T) {
@@ -266,15 +271,17 @@ func TestBedrockRuntimeRequest_InvokeModel(t *testing.T) {
}`, w.Body.String())
}
func TestBedrockRuntimeRequest_MissingAccountIDReturnsServerInternal(t *testing.T) {
func TestBedrockRuntimeRequest_MissingAccountIDReturnsInternalError(t *testing.T) {
ts := newVLLMStub(t)
gw := newBedrockRequestGateway(t, ts.URL)
req := httptest.NewRequest(http.MethodPost, "/model/"+bedrockTestLlamaModelID+"/converse", strings.NewReader("{}"))
req := withTestIdentity(httptest.NewRequest(http.MethodPost, "/model/"+bedrockTestLlamaModelID+"/converse", strings.NewReader("{}")))
w := httptest.NewRecorder()
// Account-less requests are rejected by the policy gate, which runs ahead of
// the handler's own account guard.
err := gw.BedrockRuntime_Request(w, req)
require.Error(t, err)
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
assert.Equal(t, awserrors.ErrorInternalError, err.Error())
}
// newVLLMStreamStub stands up an httptest server answering the OpenAI
@@ -30,6 +30,7 @@ func newRepoLifecycleGateway(t *testing.T) (*GatewayConfig, *nats.Conn) {
serveECRMeta(t, nc, handlers_ecr.SubjectManifestList, svc.ManifestList)
gw := &GatewayConfig{
NATSConn: nc, Region: ecrTestRegion, InternalSuffix: ecrTestSuffix, DisableLogging: true,
IAMService: allowAllIAMService(),
}
return gw, nc
}
@@ -55,6 +55,7 @@ func newDescribeReposGateway(t *testing.T, repos ...string) *GatewayConfig {
}
return &GatewayConfig{
NATSConn: nc, Region: ecrTestRegion, InternalSuffix: ecrTestSuffix, DisableLogging: true,
IAMService: allowAllIAMService(),
}
}
@@ -18,6 +18,7 @@ func TestHandleGetAuthorizationToken_MintsUsableToken(t *testing.T) {
gw := &GatewayConfig{
Region: ecrTestRegion, InternalSuffix: ecrTestSuffix,
ECRTokenIssuer: iss, ECRTokenVerifier: verify, DisableLogging: true,
IAMService: allowAllIAMService(),
}
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("{}"))
@@ -61,6 +62,7 @@ func TestHandleGetAuthorizationToken_ProxyEndpointCarriesPort(t *testing.T) {
gw := &GatewayConfig{
Region: ecrTestRegion, InternalSuffix: ecrTestSuffix, RegistryPort: "9999",
ECRTokenIssuer: iss, ECRTokenVerifier: verify, DisableLogging: true,
IAMService: allowAllIAMService(),
}
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("{}"))
@@ -82,7 +84,7 @@ func TestHandleGetAuthorizationToken_ProxyEndpointCarriesPort(t *testing.T) {
}
func TestHandleGetAuthorizationToken_NoIssuerNotImplemented(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("{}"))
ctx := context.WithValue(req.Context(), ctxAccountID, ecrTestAccount)
err := gw.handleGetAuthorizationToken(httptest.NewRecorder(), req.WithContext(ctx))
@@ -94,6 +96,7 @@ func TestECRRequest_GetAuthorizationTokenDispatched(t *testing.T) {
gw := &GatewayConfig{
Region: ecrTestRegion, InternalSuffix: ecrTestSuffix,
ECRTokenIssuer: iss, ECRTokenVerifier: verify, DisableLogging: true,
IAMService: allowAllIAMService(),
}
w := httptest.NewRecorder()
req := setupECRRequest("AmazonEC2ContainerRegistry_V20150921.GetAuthorizationToken", "{}")
+2 -2
View File
@@ -23,7 +23,7 @@ import (
func newImageGateway(t *testing.T) *GatewayConfig {
t.Helper()
reg := gateway_ecr.NewRegistry(objectstore.NewMemoryObjectStore(), handlers_ecr.NewMemoryMetaStore(), ecrTestAccount)
return &GatewayConfig{ECRRegistry: reg, Region: ecrTestRegion, InternalSuffix: ecrTestSuffix, DisableLogging: true}
return &GatewayConfig{ECRRegistry: reg, Region: ecrTestRegion, InternalSuffix: ecrTestSuffix, DisableLogging: true, IAMService: allowAllIAMService()}
}
// seedGatewayRepo creates the repository metadata so push/PutImage handlers —
@@ -233,7 +233,7 @@ func TestImageHandlers_GuardRails(t *testing.T) {
assert.Equal(t, "ServerInternal", err.Error())
// Nil registry -> ServerInternal.
bare := &GatewayConfig{DisableLogging: true}
bare := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err = bare.handleListImages(httptest.NewRecorder(), imageReq(t, `{"repositoryName":"team/app"}`))
require.Error(t, err)
assert.Equal(t, "ServerInternal", err.Error())
+5 -5
View File
@@ -53,14 +53,14 @@ func TestECRActionsMap_CoreActionsRegistered(t *testing.T) {
}
func TestECRRequest_MissingTarget(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err := gw.ECR_Request(httptest.NewRecorder(), setupECRRequest("", ""))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorMissingAction, err.Error())
}
func TestECRRequest_UnknownAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err := gw.ECR_Request(httptest.NewRecorder(),
setupECRRequest("AmazonEC2ContainerRegistry_V20150921.MadeUpAction", "{}"))
require.Error(t, err)
@@ -71,7 +71,7 @@ func TestECRRequest_UnknownAction(t *testing.T) {
// handler lands. The repo/image actions are now served inline, so
// ListRepositories stands in as a still-stubbed action.
func TestECRRequest_KnownActionNotImplemented(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err := gw.ECR_Request(httptest.NewRecorder(),
setupECRRequest("AmazonEC2ContainerRegistry_V20150921.ListRepositories", "{}"))
require.Error(t, err)
@@ -79,7 +79,7 @@ func TestECRRequest_KnownActionNotImplemented(t *testing.T) {
}
func TestOCIRegistry_VersionCheck(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
r := chi.NewRouter()
gw.mountOCIRegistry(r)
@@ -91,7 +91,7 @@ func TestOCIRegistry_VersionCheck(t *testing.T) {
}
func TestOCIRegistry_StubReturns501(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
r := chi.NewRouter()
gw.mountOCIRegistry(r)
+9 -7
View File
@@ -20,7 +20,7 @@ func setupECSRequest(target, body string) *http.Request {
}
ctx := context.WithValue(req.Context(), ctxService, "ecs")
ctx = context.WithValue(ctx, ctxAccountID, "123456789012")
return req.WithContext(ctx)
return withTestIdentity(req.WithContext(ctx))
}
func TestECSActionFromTarget(t *testing.T) {
@@ -62,14 +62,14 @@ func TestECSActionsMap_NamespaceRegistered(t *testing.T) {
}
func TestECSRequest_MissingTarget(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err := gw.ECS_Request(httptest.NewRecorder(), setupECSRequest("", ""))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorMissingAction, err.Error())
}
func TestECSRequest_UnknownAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err := gw.ECS_Request(httptest.NewRecorder(),
setupECSRequest("AmazonEC2ContainerServiceV20141113.MadeUpAction", "{}"))
require.Error(t, err)
@@ -78,7 +78,7 @@ func TestECSRequest_UnknownAction(t *testing.T) {
// An action whose real handler has not yet landed still resolves to the 501 stub.
func TestECSRequest_KnownActionNotImplemented(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err := gw.ECS_Request(httptest.NewRecorder(),
setupECSRequest("AmazonEC2ContainerServiceV20141113.UpdateCluster", "{}"))
require.Error(t, err)
@@ -90,10 +90,12 @@ func TestECSRequest_KnownActionNotImplemented(t *testing.T) {
func TestECSRequest_MissingAccountID(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("{}"))
req.Header.Set("X-Amz-Target", "AmazonEC2ContainerServiceV20141113.ListClusters")
req = req.WithContext(context.WithValue(req.Context(), ctxService, "ecs"))
req = withTestIdentity(req.WithContext(context.WithValue(req.Context(), ctxService, "ecs")))
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
// Account-less requests are rejected by the policy gate, which runs ahead of
// the handler's own account guard.
err := gw.ECS_Request(httptest.NewRecorder(), req)
require.Error(t, err)
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
assert.Equal(t, awserrors.ErrorInternalError, err.Error())
}
+5 -5
View File
@@ -174,11 +174,11 @@ func TestLookupEKSAction_UnknownReturnsFalse(t *testing.T) {
}
func TestEKSRequest_UnknownActionReturnsInvalidAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodGet, "/totally-unknown", nil)
ctx := context.WithValue(req.Context(), ctxService, "eks")
ctx = context.WithValue(ctx, ctxAccountID, "111122223333")
req = req.WithContext(ctx)
req = withTestIdentity(req.WithContext(ctx))
w := httptest.NewRecorder()
err := gw.EKS_Request(w, req)
@@ -187,11 +187,11 @@ func TestEKSRequest_UnknownActionReturnsInvalidAction(t *testing.T) {
}
func TestEKSRequest_MissingNATSReturnsServerInternal(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodGet, "/clusters", nil)
ctx := context.WithValue(req.Context(), ctxService, "eks")
ctx = context.WithValue(ctx, ctxAccountID, "111122223333")
req = req.WithContext(ctx)
req = withTestIdentity(req.WithContext(ctx))
w := httptest.NewRecorder()
err := gw.EKS_Request(w, req)
@@ -200,7 +200,7 @@ func TestEKSRequest_MissingNATSReturnsServerInternal(t *testing.T) {
}
func TestErrorHandler_EKSEmitsJSONNotXML(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodGet, "/clusters", nil)
ctx := context.WithValue(req.Context(), ctxService, "eks")
req = req.WithContext(ctx)
+2 -2
View File
@@ -20,7 +20,7 @@ func setupELBv2Request(body string) *http.Request {
}
func TestELBv2Request_MissingAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
err := gw.ELBv2_Request(w, setupELBv2Request(""))
require.Error(t, err)
@@ -28,7 +28,7 @@ func TestELBv2Request_MissingAction(t *testing.T) {
}
func TestELBv2Request_UnknownAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
err := gw.ELBv2_Request(w, setupELBv2Request("Action=FakeAction"))
require.Error(t, err)
+12 -14
View File
@@ -447,9 +447,10 @@ func (gw *GatewayConfig) checkPolicy(r *http.Request, service, action string) er
}
// checkPolicyResource evaluates IAM policies against a specific resource ARN.
// Root users bypass evaluation. Nil IAMService, or a request with no SigV4
// auth context, allows (pre-IAM compatibility). Used by EC2 paths that
// enforce iam:PassRole before attaching an instance profile.
// Root users bypass evaluation. A nil IAMService is a server fault and an
// unauthenticated request is denied — neither can reach a gated handler through
// the route tree. Used by EC2 paths that enforce iam:PassRole before attaching
// an instance profile.
func (gw *GatewayConfig) checkPolicyResource(r *http.Request, service, action, resource string) error {
// Every dispatcher — query-protocol and REST-JSON alike — reaches this
// point with its resolved action, so telemetry enrichment lives here
@@ -458,15 +459,14 @@ func (gw *GatewayConfig) checkPolicyResource(r *http.Request, service, action, r
recordResolvedAction(r.Context(), service, action)
if gw.IAMService == nil {
slog.Warn("checkPolicy: IAM service not available, skipping policy check",
"service", service, "action", action)
return nil
slog.Error("checkPolicy: IAM service not available", "service", service, "action", action)
return errors.New(awserrors.ErrorInternalError)
}
identityVal := r.Context().Value(ctxIdentity)
if identityVal == nil {
// No auth context — pre-IAM compatibility
return nil
slog.Warn("checkPolicy: request carries no auth context", "service", service, "action", action)
return errors.New(awserrors.ErrorAccessDenied)
}
identity, ok := identityVal.(string)
if !ok {
@@ -474,9 +474,9 @@ func (gw *GatewayConfig) checkPolicyResource(r *http.Request, service, action, r
return errors.New(awserrors.ErrorInternalError)
}
if identity == "" {
// Pre-IAM compatibility: an authenticated request with no identity name
// predates per-principal policy enforcement.
return nil
slog.Warn("checkPolicy: authenticated request carries no identity name",
"service", service, "action", action)
return errors.New(awserrors.ErrorAccessDenied)
}
accountID, _ := r.Context().Value(ctxAccountID).(string)
if accountID == "" {
@@ -506,9 +506,7 @@ func mustCtxString(r *http.Request, key contextKey) string {
// enforcement: given an already-resolved principal (from SigV4 or, for /v2/*
// ECR requests, a freshly rehydrated token identity), it resolves the
// principal's current policies and evaluates iamAction against resource.
// Unlike checkPolicyResource, a nil IAMService fails closed here — callers
// that want pre-IAM-compatibility bypass behavior must check for that before
// calling in, exactly as checkPolicyResource does.
// A nil IAMService fails closed here, matching checkPolicyResource.
func (gw *GatewayConfig) evaluatePrincipalPolicy(principal principalContext, iamAction, resource string) error {
if gw.IAMService == nil {
slog.Error("evaluatePrincipalPolicy: IAM service not available", "action", iamAction)
+91 -56
View File
@@ -157,7 +157,7 @@ func TestGenerateIAMErrorResponse_ValidXML(t *testing.T) {
}
func TestErrorHandler_IAMService(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxService, "iam")
@@ -179,7 +179,7 @@ func TestErrorHandler_IAMService(t *testing.T) {
}
func TestErrorHandler_UnknownError(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxService, "ec2")
@@ -200,7 +200,7 @@ func TestErrorHandler_UnknownError(t *testing.T) {
}
func TestErrorHandler_WrappedErrorCode(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxService, "ec2")
@@ -224,7 +224,7 @@ func TestErrorHandler_WrappedErrorCode(t *testing.T) {
// fidelity gap: a call site that names the blocking resource via awserrors.Errorf
// must have that wording reach the client instead of the generic ErrorLookup text.
func TestErrorHandler_PrefersCallSiteMessage(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxService, "ec2")
@@ -249,7 +249,7 @@ func TestErrorHandler_PrefersCallSiteMessage(t *testing.T) {
// ResourceInUseException collision check: ACM's DeleteCertificate must not
// surface EKS's "cluster already exists" wording for the shared wire code.
func TestErrorHandler_ACMResourceInUse_UsesACMWording(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", nil)
ctx := context.WithValue(req.Context(), ctxService, "acm")
req = req.WithContext(ctx)
@@ -270,7 +270,7 @@ func TestErrorHandler_ACMResourceInUse_UsesACMWording(t *testing.T) {
// own ResourceInUseException must keep its existing wording, unaffected by the
// ACM-specific override.
func TestErrorHandler_EKSResourceInUse_UsesEKSWording(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", nil)
ctx := context.WithValue(req.Context(), ctxService, "eks")
req = req.WithContext(ctx)
@@ -290,7 +290,7 @@ func TestErrorHandler_EKSResourceInUse_UsesEKSWording(t *testing.T) {
// case: an error path that supplies no message (the vast majority of call
// sites) must keep rendering exactly today's ErrorLookup text, unchanged.
func TestErrorHandler_NoMessageSupplied_MatchesErrorLookup(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxService, "ec2")
@@ -309,7 +309,7 @@ func TestErrorHandler_NoMessageSupplied_MatchesErrorLookup(t *testing.T) {
}
func TestErrorHandler_ELBv2Service(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxService, "elasticloadbalancing")
@@ -331,7 +331,7 @@ func TestErrorHandler_ELBv2Service(t *testing.T) {
}
func TestErrorHandler_EC2Service(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxService, "ec2")
@@ -351,7 +351,7 @@ func TestErrorHandler_EC2Service(t *testing.T) {
}
func TestErrorHandler_IgnoresClientRequestID(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxService, "ec2")
@@ -369,7 +369,7 @@ func TestErrorHandler_IgnoresClientRequestID(t *testing.T) {
}
func TestErrorHandler_ContentTypeXML(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxService, "ec2")
@@ -694,7 +694,7 @@ func TestParseAWSQueryArgs_CapturedClientBodies(t *testing.T) {
}
func TestGetService(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
tests := []struct {
name string
@@ -789,7 +789,7 @@ func TestGetService(t *testing.T) {
}
func TestRequest_NoServiceContext(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", nil)
w := httptest.NewRecorder()
@@ -802,7 +802,7 @@ func TestRequest_NoServiceContext(t *testing.T) {
}
func TestRequest_UnsupportedService(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", nil)
ctx := context.WithValue(req.Context(), ctxService, "s3")
req = req.WithContext(ctx)
@@ -830,7 +830,7 @@ func TestRequest_MalformedQueryString_EndToEnd(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// A live NATS connection bypasses the cluster-unavailable gate.
gw := &GatewayConfig{DisableLogging: true, NATSConn: connectedNATS(t)}
gw := &GatewayConfig{DisableLogging: true, NATSConn: connectedNATS(t), IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tc.body))
ctx := context.WithValue(req.Context(), ctxService, tc.service)
ctx = context.WithValue(ctx, ctxAccountID, "123456789012")
@@ -848,7 +848,7 @@ func TestRequest_MalformedQueryString_EndToEnd(t *testing.T) {
}
func TestRequest_EC2MissingAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, NATSConn: connectedNATS(t)}
gw := &GatewayConfig{DisableLogging: true, NATSConn: connectedNATS(t), IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(""))
ctx := context.WithValue(req.Context(), ctxService, "ec2")
ctx = context.WithValue(ctx, ctxAccountID, "123456789012")
@@ -899,12 +899,11 @@ func setupEC2Request(body string, accountID string) *http.Request {
if accountID != "" {
ctx = context.WithValue(ctx, ctxAccountID, accountID)
}
req = req.WithContext(ctx)
return req
return withTestIdentity(req.WithContext(ctx))
}
func TestEC2Request_MissingAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := setupEC2Request("", "123456789012")
w := httptest.NewRecorder()
@@ -914,7 +913,7 @@ func TestEC2Request_MissingAction(t *testing.T) {
}
func TestEC2Request_UnknownAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := setupEC2Request("Action=FakeAction", "123456789012")
w := httptest.NewRecorder()
@@ -924,7 +923,7 @@ func TestEC2Request_UnknownAction(t *testing.T) {
}
func TestEC2Request_NilNATSNonLocalAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, NATSConn: nil}
gw := &GatewayConfig{DisableLogging: true, NATSConn: nil, IAMService: allowAllIAMService()}
req := setupEC2Request("Action=DescribeInstances", "123456789012")
w := httptest.NewRecorder()
@@ -939,6 +938,7 @@ func TestEC2Request_NilNATSLocalAction(t *testing.T) {
NATSConn: nil,
Region: "us-east-1",
AZ: "us-east-1a",
IAMService: allowAllIAMService(),
}
req := setupEC2Request("Action=DescribeRegions", "123456789012")
w := httptest.NewRecorder()
@@ -955,18 +955,19 @@ func TestEC2Request_NilNATSLocalAction(t *testing.T) {
}
func TestEC2Request_MissingAccountID(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, NATSConn: nil}
gw := &GatewayConfig{DisableLogging: true, NATSConn: nil, IAMService: allowAllIAMService()}
// Use a local action so we don't fail on nil NATS first
req := setupEC2Request("Action=DescribeRegions", "")
w := httptest.NewRecorder()
// The policy gate rejects an account-less request before the handler's own guard.
err := gw.EC2_Request(w, req)
require.Error(t, err)
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
assert.Equal(t, awserrors.ErrorInternalError, err.Error())
}
func TestEC2Request_DescribeAccountAttributes(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, NATSConn: nil}
gw := &GatewayConfig{DisableLogging: true, NATSConn: nil, IAMService: allowAllIAMService()}
req := setupEC2Request("Action=DescribeAccountAttributes", "123456789012")
w := httptest.NewRecorder()
@@ -987,6 +988,7 @@ func TestEC2Request_DescribeAvailabilityZones(t *testing.T) {
NATSConn: nil,
Region: "us-east-1",
AZ: "us-east-1a",
IAMService: allowAllIAMService(),
}
req := setupEC2Request("Action=DescribeAvailabilityZones", "123456789012")
w := httptest.NewRecorder()
@@ -1002,20 +1004,64 @@ func TestEC2Request_DescribeAvailabilityZones(t *testing.T) {
assert.Contains(t, string(body), "DescribeAvailabilityZonesResponse")
}
func TestCheckPolicy_NilIAMService(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, IAMService: nil}
req := httptest.NewRequest(http.MethodPost, "/", nil)
// TestCheckPolicy_FailsClosed covers the three states that once returned allow
// before any policy was consulted. A missing service is a server fault; a
// request with no resolved principal is denied.
func TestCheckPolicy_FailsClosed(t *testing.T) {
authed := func(identity string) *http.Request {
req := httptest.NewRequest(http.MethodPost, "/", nil)
ctx := context.WithValue(req.Context(), ctxIdentity, identity)
ctx = context.WithValue(ctx, ctxAccountID, "123456789012")
ctx = context.WithValue(ctx, ctxPrincipalType, principalTypeUser)
return req.WithContext(ctx)
}
err := gw.checkPolicy(req, "ec2", "DescribeInstances")
assert.NoError(t, err)
cases := []struct {
name string
gw *GatewayConfig
req *http.Request
wantErr string
}{
{
name: "nil IAM service",
gw: &GatewayConfig{DisableLogging: true, IAMService: nil},
req: authed("alice"),
wantErr: awserrors.ErrorInternalError,
},
{
name: "no auth context",
gw: &GatewayConfig{DisableLogging: true, IAMService: &mockIAMService{}},
req: httptest.NewRequest(http.MethodPost, "/", nil),
wantErr: awserrors.ErrorAccessDenied,
},
{
name: "empty identity",
gw: &GatewayConfig{DisableLogging: true, IAMService: &mockIAMService{}},
req: authed(""),
wantErr: awserrors.ErrorAccessDenied,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := tc.gw.checkPolicy(tc.req, "ec2", "DescribeInstances")
require.Error(t, err)
assert.Equal(t, tc.wantErr, err.Error())
})
}
}
func TestCheckPolicy_NoIdentityInContext(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, IAMService: &mockIAMService{}}
// TestCheckPolicy_PassRoleFailsClosed proves the change reaches a real gated
// call site, not just the helper: an unauthenticated RunInstances carrying an
// instance profile is denied rather than passing the iam:PassRole check.
func TestCheckPolicy_PassRoleFailsClosed(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", nil)
err := gw.checkPolicy(req, "ec2", "DescribeInstances")
assert.NoError(t, err)
err := gw.checkPolicyResource(req, "iam", "PassRole",
"arn:aws:iam::123456789012:role/app")
require.Error(t, err)
assert.Equal(t, awserrors.ErrorAccessDenied, err.Error())
}
func TestCheckPolicy_RootUserGlobalAccount(t *testing.T) {
@@ -1132,18 +1178,6 @@ func TestCheckPolicy_GetUserPoliciesError(t *testing.T) {
assert.Equal(t, awserrors.ErrorInternalError, err.Error())
}
func TestCheckPolicy_EmptyIdentity(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, IAMService: &mockIAMService{}}
req := httptest.NewRequest(http.MethodPost, "/", nil)
ctx := context.WithValue(req.Context(), ctxIdentity, "")
ctx = context.WithValue(ctx, ctxAccountID, "123456789012")
ctx = context.WithValue(ctx, ctxPrincipalType, principalTypeUser)
req = req.WithContext(ctx)
err := gw.checkPolicy(req, "ec2", "DescribeInstances")
assert.NoError(t, err)
}
func TestCheckPolicy_MissingAccountID(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true, IAMService: &mockIAMService{}}
req := httptest.NewRequest(http.MethodPost, "/", nil)
@@ -1245,7 +1279,7 @@ func TestImportKeyPair_Base64PaddingWorkaround(t *testing.T) {
"PublicKeyMaterial": "c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFCZ1FD%3D%3D",
}
gw := &GatewayConfig{DisableLogging: true, NATSConn: nil}
gw := &GatewayConfig{DisableLogging: true, NATSConn: nil, IAMService: allowAllIAMService()}
// NATS is nil so the handler errors, but PublicKeyMaterial is modified before that.
_, _ = handler("ImportKeyPair", q, gw, "123456789012", nil)
@@ -1258,7 +1292,7 @@ func TestImportKeyPair_Base64PaddingWorkaround(t *testing.T) {
// --- Throttle middleware integration tests ---
func TestThrottleKeyFuncs_ExtractsAccountAndAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
keyFuncs := gw.throttleKeyFuncs()
require.Len(t, keyFuncs, 2)
@@ -1277,7 +1311,7 @@ func TestThrottleKeyFuncs_ExtractsAccountAndAction(t *testing.T) {
}
func TestThrottleKeyFuncs_UnknownAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
keyFuncs := gw.throttleKeyFuncs()
// No ctxAction in context — should return "unknown".
@@ -1291,7 +1325,7 @@ func TestThrottleKeyFuncs_UnknownAction(t *testing.T) {
}
func TestThrottleKeyFuncs_MissingAccountID(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
keyFuncs := gw.throttleKeyFuncs()
// No ctxAccountID in context — should return an error.
@@ -1303,7 +1337,7 @@ func TestThrottleKeyFuncs_MissingAccountID(t *testing.T) {
}
func TestWriteThrottleError_EC2(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", nil)
ctx := context.WithValue(req.Context(), ctxService, "ec2")
@@ -1321,7 +1355,7 @@ func TestWriteThrottleError_EC2(t *testing.T) {
}
func TestWriteThrottleError_IAM(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", nil)
ctx := context.WithValue(req.Context(), ctxService, "iam")
@@ -1350,6 +1384,7 @@ func TestThrottleMiddleware_Integration(t *testing.T) {
gw := &GatewayConfig{
DisableLogging: true,
Throttler: throttler,
IAMService: allowAllIAMService(),
}
inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
@@ -1387,7 +1422,7 @@ func TestThrottleMiddleware_Integration(t *testing.T) {
func TestThrottleMiddleware_DisabledConfig(t *testing.T) {
// When Throttler is nil, SetupRoutes skips middleware — no panic.
gw := &GatewayConfig{DisableLogging: true, Throttler: nil}
gw := &GatewayConfig{DisableLogging: true, Throttler: nil, IAMService: allowAllIAMService()}
handler := gw.SetupRoutes()
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=DescribeInstances"))
@@ -1411,7 +1446,7 @@ func TestThrottleMiddleware_PerActionIsolation(t *testing.T) {
throttler := ratelimit.New(cfg)
defer throttler.Stop()
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
@@ -1442,7 +1477,7 @@ func TestThrottleMiddleware_PerActionIsolation(t *testing.T) {
}
func TestRequest_ClusterUnavailableNilConn_EC2(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", nil)
ctx := context.WithValue(req.Context(), ctxService, "ec2")
@@ -1462,7 +1497,7 @@ func TestRequest_ClusterUnavailableNilConn_EC2(t *testing.T) {
}
func TestRequest_ClusterUnavailableNilConn_IAM(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", nil)
ctx := context.WithValue(req.Context(), ctxService, "iam")
@@ -1485,7 +1520,7 @@ func TestRequest_ClusterUnavailableClosedConn(t *testing.T) {
require.NoError(t, err)
nc.Close()
gw := &GatewayConfig{DisableLogging: true, NATSConn: nc}
gw := &GatewayConfig{DisableLogging: true, NATSConn: nc, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", nil)
ctx := context.WithValue(req.Context(), ctxService, "ec2")
+5 -1
View File
@@ -162,6 +162,8 @@ func setupIAMRequestHandler(svc handlers_iam.IAMService) http.Handler {
ctx := r.Context()
ctx = context.WithValue(ctx, ctxService, "iam")
ctx = context.WithValue(ctx, ctxAccountID, "000000000000")
ctx = context.WithValue(ctx, ctxIdentity, "root")
ctx = context.WithValue(ctx, ctxPrincipalType, principalTypeUser)
r = r.WithContext(ctx)
if err := gw.IAM_Request(w, r); err != nil {
gw.ErrorHandler(w, r, err)
@@ -287,6 +289,8 @@ func TestIAMRequest_NilService(t *testing.T) {
ctx := r.Context()
ctx = context.WithValue(ctx, ctxService, "iam")
ctx = context.WithValue(ctx, ctxAccountID, "000000000000")
ctx = context.WithValue(ctx, ctxIdentity, "root")
ctx = context.WithValue(ctx, ctxPrincipalType, principalTypeUser)
r = r.WithContext(ctx)
if err := gw.IAM_Request(w, r); err != nil {
gw.ErrorHandler(w, r, err)
@@ -330,7 +334,7 @@ func TestIAMRequest_MissingAccountID(t *testing.T) {
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), ctxService, "iam")
r = r.WithContext(ctx)
r = withTestIdentity(r.WithContext(ctx))
if err := gw.IAM_Request(w, r); err != nil {
gw.ErrorHandler(w, r, err)
}
+1 -1
View File
@@ -38,7 +38,7 @@ func seedDiscoveryCluster(t *testing.T) http.Handler {
t.Fatalf("put jwks: %v", err)
}
gw := &GatewayConfig{DisableLogging: true, NATSConn: nc}
gw := &GatewayConfig{DisableLogging: true, NATSConn: nc, IAMService: allowAllIAMService()}
return gw.SetupRoutes()
}
@@ -0,0 +1,38 @@
package gateway
import (
"context"
"net/http"
handlers_iam "github.com/mulgadc/spinifex/spinifex/handlers/iam"
)
// allowAllIAMService resolves an allow-everything policy for every principal.
// Tests that exercise handler behaviour rather than authorization use it so the
// policy gate passes on a real evaluation instead of a bypass.
func allowAllIAMService() *policyMockIAMService {
allow := []handlers_iam.PolicyDocument{{
Version: "2012-10-17",
Statement: []handlers_iam.Statement{
{Effect: "Allow", Action: handlers_iam.StringOrArr{"*"}, Resource: handlers_iam.StringOrArr{"*"}},
},
}}
return &policyMockIAMService{
getUserPoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return allow, nil },
getRolePoliciesFn: func(_, _ string) ([]handlers_iam.PolicyDocument, error) { return allow, nil },
}
}
// withTestIdentity attaches the identity half of the auth context the SigV4
// middleware sets, so a hand-built request reaches the handler instead of being
// denied. Existing values are preserved for tests that set their own principal.
func withTestIdentity(req *http.Request) *http.Request {
ctx := req.Context()
if ctx.Value(ctxIdentity) == nil {
ctx = context.WithValue(ctx, ctxIdentity, "alice")
}
if ctx.Value(ctxPrincipalType) == nil {
ctx = context.WithValue(ctx, ctxPrincipalType, principalTypeUser)
}
return req.WithContext(ctx)
}
+18 -14
View File
@@ -23,7 +23,7 @@ func setupRDSRequest(body string) *http.Request {
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
ctx := context.WithValue(req.Context(), ctxService, "rds")
ctx = context.WithValue(ctx, ctxAccountID, rdsTestAccountID)
return req.WithContext(ctx)
return withTestIdentity(req.WithContext(ctx))
}
// A signed customer request: an IAM user with a resolvable identity, which is
@@ -83,14 +83,14 @@ func newRDSGatewayWithResolver(
}
func TestRDSRequest_MissingAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err := gw.RDS_Request(httptest.NewRecorder(), setupRDSRequest(""))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorMissingAction, err.Error())
}
func TestRDSRequest_UnknownAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err := gw.RDS_Request(httptest.NewRecorder(), setupRDSRequest("Action=FakeAction"))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorInvalidAction, err.Error())
@@ -98,7 +98,7 @@ func TestRDSRequest_UnknownAction(t *testing.T) {
// A malformed percent-encoding is a client error, not an internal one.
func TestRDSRequest_MalformedQueryString(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err := gw.RDS_Request(httptest.NewRecorder(), setupRDSRequest("Action=%zz"))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorMalformedQueryString, err.Error())
@@ -106,7 +106,7 @@ func TestRDSRequest_MalformedQueryString(t *testing.T) {
func TestRDSRequest_MalformedScalarReturnsHTTP400(t *testing.T) {
_, nc, _ := testutil.StartTestJetStream(t)
gw := &GatewayConfig{DisableLogging: true, NATSConn: nc}
gw := &GatewayConfig{DisableLogging: true, NATSConn: nc, IAMService: allowAllIAMService()}
for _, body := range []string{
"Action=CreateDBInstance&AllocatedStorage=abc",
"Action=CreateDBInstance&MultiAZ=yes",
@@ -128,7 +128,7 @@ func TestRDSRequest_MalformedScalarReturnsHTTP400(t *testing.T) {
// A known action still needs a NATS connection to reach the control plane, so
// the disconnected case must fail rather than answer from the gateway alone.
func TestRDSRequest_NoNATSConn(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
err := gw.RDS_Request(httptest.NewRecorder(), setupRDSRequest("Action=DescribeDBInstances"))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
@@ -217,20 +217,24 @@ func TestRDSRequest_CrossAccountARNIsRejectedNotEvaluated(t *testing.T) {
assert.Equal(t, awserrors.ErrorInvalidParameterValue, awserrors.ValidErrorCodeFromError(err))
}
// An authenticated request with no resolvable identity predates per-principal
// enforcement and keeps its documented allow, rather than newly failing closed.
func TestRDSRequest_NoIdentityKeepsPreIAMAllow(t *testing.T) {
gw, probe := newRDSPolicyGateway(nil) // no policies: an evaluated request would be denied
err := gw.RDS_Request(httptest.NewRecorder(), setupRDSRequest("Action=DescribeDBInstances"))
// A request with no resolvable identity is denied outright: there is no
// principal to evaluate, so the gate refuses before consulting any policy.
func TestRDSRequest_NoIdentityDenied(t *testing.T) {
gw, probe := newRDSPolicyGateway(nil)
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("Action=DescribeDBInstances"))
ctx := context.WithValue(req.Context(), ctxService, "rds")
ctx = context.WithValue(ctx, ctxAccountID, rdsTestAccountID)
err := gw.RDS_Request(httptest.NewRecorder(), req.WithContext(ctx))
require.Error(t, err)
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
assert.Equal(t, awserrors.ErrorAccessDenied, err.Error())
assert.Zero(t, probe.consulted, "a request with no identity must not be evaluated at all")
}
// RDS errors must use the IAM-style <ErrorResponse> envelope: the aws-sdk-go
// query unmarshaler rejects the EC2 <Response><Errors> shape for this service.
func TestRDSErrorHandler_UsesIAMEnvelope(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
gw.ErrorHandler(w, setupRDSRequest("Action=CreateDBInstance"), errNotImplementedForTest)
@@ -240,7 +244,7 @@ func TestRDSErrorHandler_UsesIAMEnvelope(t *testing.T) {
}
func TestRDSErrorHandler_UsesRDSUnsupportedActionWording(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
gw.ErrorHandler(w, setupRDSRequest("Action=CreateDBCluster"),
errors.New(awserrors.ErrorOperationNotSupported))
@@ -29,6 +29,7 @@ func spinifexAccessRequest(t *testing.T, gw *GatewayConfig, action, accountID st
ctx = context.WithValue(ctx, ctxAccountID, accountID)
ctx = context.WithValue(ctx, ctxIdentity, "admin")
ctx = context.WithValue(ctx, ctxService, "spinifex")
ctx = context.WithValue(ctx, ctxPrincipalType, principalTypeUser)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -48,6 +49,7 @@ func accessGateway(t *testing.T) *GatewayConfig {
return &GatewayConfig{
DisableLogging: true,
BedrockAccessAdmin: gateway_bedrock.NewModelAccessStore(js, 1),
IAMService: allowAllIAMService(),
}
}
@@ -116,7 +118,7 @@ func TestSpinifex_ModelAccess_MissingParameters(t *testing.T) {
// TestSpinifex_ModelAccess_NoStoreIsServerError covers a gateway built without
// a grant store: the admin actions refuse rather than panicking on nil.
func TestSpinifex_ModelAccess_NoStoreIsServerError(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
adminAccount := admin.DefaultAccountID()
for _, action := range []string{"GrantModelAccess", "RevokeModelAccess", "ListModelAccess"} {
+13 -7
View File
@@ -21,6 +21,7 @@ func spinifexRequest(t *testing.T, gw *GatewayConfig, action, accountID, identit
ctx = context.WithValue(ctx, ctxAccountID, accountID)
ctx = context.WithValue(ctx, ctxIdentity, identity)
ctx = context.WithValue(ctx, ctxService, "spinifex")
ctx = context.WithValue(ctx, ctxPrincipalType, principalTypeUser)
req = req.WithContext(ctx)
w := httptest.NewRecorder()
@@ -38,6 +39,7 @@ func TestSpinifex_GetVersion_Admin(t *testing.T) {
DisableLogging: true,
Version: "v0.5.0-43-gae1deb5",
Commit: "ae1deb5",
IAMService: allowAllIAMService(),
}
w := spinifexRequest(t, gw, "GetVersion", admin.DefaultAccountID(), "admin")
assert.Equal(t, http.StatusOK, w.Code)
@@ -50,6 +52,7 @@ func TestSpinifex_GetVersion_NonAdmin_Denied(t *testing.T) {
DisableLogging: true,
Version: "v0.5.0",
Commit: "abc123",
IAMService: allowAllIAMService(),
}
w := spinifexRequest(t, gw, "GetVersion", "000000000002", "alice")
require.Equal(t, http.StatusForbidden, w.Code)
@@ -57,28 +60,28 @@ func TestSpinifex_GetVersion_NonAdmin_Denied(t *testing.T) {
}
func TestSpinifex_GetNodes_NonAdmin_Denied(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := spinifexRequest(t, gw, "GetNodes", "000000000002", "alice")
require.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), awserrors.ErrorAccessDenied)
}
func TestSpinifex_GetVMs_NonAdmin_Denied(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := spinifexRequest(t, gw, "GetVMs", "000000000002", "alice")
require.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), awserrors.ErrorAccessDenied)
}
func TestSpinifex_InvalidAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := spinifexRequest(t, gw, "DoesNotExist", admin.DefaultAccountID(), "admin")
require.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), awserrors.ErrorInvalidAction)
}
func TestSpinifex_MissingAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(""))
ctx := context.WithValue(req.Context(), ctxService, "spinifex")
ctx = context.WithValue(ctx, ctxAccountID, admin.DefaultAccountID())
@@ -92,22 +95,25 @@ func TestSpinifex_MissingAction(t *testing.T) {
}
func TestSpinifex_NoAccountID(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
body := "Action=GetVersion"
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
ctx := context.WithValue(req.Context(), ctxService, "spinifex")
req = req.WithContext(ctx)
req = withTestIdentity(req.WithContext(ctx))
w := httptest.NewRecorder()
// Account-less requests are rejected by the policy gate, which runs ahead of
// the handler's own account guard.
err := gw.Spinifex_Request(w, req)
require.Error(t, err)
assert.Equal(t, awserrors.ErrorServerInternal, err.Error())
assert.Equal(t, awserrors.ErrorInternalError, err.Error())
}
func TestSpinifex_GetNodes_NoNATS(t *testing.T) {
gw := &GatewayConfig{
DisableLogging: true,
NATSConn: nil,
IAMService: allowAllIAMService(),
}
w := spinifexRequest(t, gw, "GetNodes", admin.DefaultAccountID(), "admin")
require.Equal(t, http.StatusForbidden, w.Code)
+4 -4
View File
@@ -19,7 +19,7 @@ func setupTaggingRequest(target, body string) *http.Request {
}
ctx := context.WithValue(req.Context(), ctxService, "tagging")
ctx = context.WithValue(ctx, ctxAccountID, "123456789012")
return req.WithContext(ctx)
return withTestIdentity(req.WithContext(ctx))
}
func TestTaggingActionFromTarget(t *testing.T) {
@@ -40,7 +40,7 @@ func TestTaggingActionsMap_AllActionsRegistered(t *testing.T) {
}
func TestTaggingRequest_MissingTarget(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
err := gw.Tagging_Request(w, setupTaggingRequest("", ""))
require.Error(t, err)
@@ -48,7 +48,7 @@ func TestTaggingRequest_MissingTarget(t *testing.T) {
}
func TestTaggingRequest_UnknownAction(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
err := gw.Tagging_Request(w, setupTaggingRequest("ResourceGroupsTaggingAPI_20170126.TagResources", "{}"))
require.Error(t, err)
@@ -58,7 +58,7 @@ func TestTaggingRequest_UnknownAction(t *testing.T) {
// A known action with no NATS connection passes routing + policy and fails at
// the NATS-availability guard.
func TestTaggingRequest_KnownActionNoNATS(t *testing.T) {
gw := &GatewayConfig{DisableLogging: true}
gw := &GatewayConfig{DisableLogging: true, IAMService: allowAllIAMService()}
w := httptest.NewRecorder()
err := gw.Tagging_Request(w, setupTaggingRequest("ResourceGroupsTaggingAPI_20170126.GetResources", "{}"))
require.Error(t, err)