diff --git a/internal/controller/admin/handler.go b/internal/controller/admin/handler.go index e8b16d1..5d86aca 100644 --- a/internal/controller/admin/handler.go +++ b/internal/controller/admin/handler.go @@ -34,11 +34,11 @@ type Service interface { ReloadConfiguration(context.Context, ReloadCommand) (MutationResult, error) } -type Authorizer interface { - Check(context.Context, *http.Request) error +type IdentityResolver interface { + Resolve(*http.Request) (httpsecurity.Identity, error) } -var _ Authorizer = (*httpsecurity.Protection)(nil) +var _ IdentityResolver = (*httpsecurity.Protection)(nil) type Options struct { MaxBodyBytes int64 @@ -80,12 +80,16 @@ type MutationResult struct { type SetUpstreamCommand struct { RequestID string + ActorID string + SourceIP string Name string Enabled bool } type SwitchCommand struct { RequestID string `json:"-"` + ActorID string `json:"-"` + SourceIP string `json:"-"` Name string `json:"-"` ExpectedCurrent string `json:"expectedCurrent"` Target string `json:"target"` @@ -94,19 +98,21 @@ type SwitchCommand struct { type ReloadCommand struct { RequestID string + ActorID string + SourceIP string } type Handler struct { service Service - authorizer Authorizer + identity IdentityResolver maxBodyBytes int64 } -func NewHandler(service Service, authorizer Authorizer, options Options) (*Handler, error) { - if service == nil || authorizer == nil || options.MaxBodyBytes <= 0 { +func NewHandler(service Service, identity IdentityResolver, options Options) (*Handler, error) { + if service == nil || identity == nil || options.MaxBodyBytes <= 0 { return nil, ErrInvalidHandler } - return &Handler{service: service, authorizer: authorizer, maxBodyBytes: options.MaxBodyBytes}, nil + return &Handler{service: service, identity: identity, maxBodyBytes: options.MaxBodyBytes}, nil } func (handler *Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { @@ -115,7 +121,8 @@ func (handler *Handler) ServeHTTP(writer http.ResponseWriter, request *http.Requ writeTransportProblem(writer, http.StatusBadRequest, "INVALID_REQUEST_ID", "Invalid request ID", "X-Request-ID is invalid", requestID) return } - if err := handler.authorizer.Check(request.Context(), request); err != nil { + identity, err := handler.identity.Resolve(request) + if err != nil { if !httpsecurity.WriteProblem(writer, requestID, err) { writeTransportProblem(writer, http.StatusInternalServerError, "INTERNAL_ERROR", "Internal server error", "the request could not be completed", requestID) } @@ -133,7 +140,7 @@ func (handler *Handler) ServeHTTP(writer http.ResponseWriter, request *http.Requ if !requireMethod(writer, request, http.MethodPost, requestID) { return } - handler.reload(writer, request, requestID) + handler.reload(writer, request, requestID, identity) return } @@ -141,14 +148,14 @@ func (handler *Handler) ServeHTTP(writer http.ResponseWriter, request *http.Requ if !requireMethod(writer, request, http.MethodPost, requestID) { return } - handler.setUpstreamEnabled(writer, request, name, action == "enable", requestID) + handler.setUpstreamEnabled(writer, request, name, action == "enable", requestID, identity) return } if name, _, ok := matchNamedAction(request.URL.Path, routingPrefix, "switch"); ok { if !requireMethod(writer, request, http.MethodPost, requestID) { return } - handler.switchRouting(writer, request, name, requestID) + handler.switchRouting(writer, request, name, requestID, identity) return } @@ -171,9 +178,18 @@ func (handler *Handler) getStatus(writer http.ResponseWriter, request *http.Requ _ = httpapi.WriteJSON(writer, http.StatusOK, status) } -func (handler *Handler) setUpstreamEnabled(writer http.ResponseWriter, request *http.Request, name string, enabled bool, requestID string) { +func (handler *Handler) setUpstreamEnabled( + writer http.ResponseWriter, + request *http.Request, + name string, + enabled bool, + requestID string, + identity httpsecurity.Identity, +) { result, err := handler.service.SetUpstreamEnabled(request.Context(), SetUpstreamCommand{ RequestID: requestID, + ActorID: identity.ClientID, + SourceIP: identity.SourceIP, Name: name, Enabled: enabled, }) @@ -184,7 +200,13 @@ func (handler *Handler) setUpstreamEnabled(writer http.ResponseWriter, request * writeMutation(writer, result, requestID) } -func (handler *Handler) switchRouting(writer http.ResponseWriter, request *http.Request, name, requestID string) { +func (handler *Handler) switchRouting( + writer http.ResponseWriter, + request *http.Request, + name string, + requestID string, + identity httpsecurity.Identity, +) { var command SwitchCommand if err := httpapi.DecodeJSON(writer, request, handler.maxBodyBytes, &command); err != nil { writeDecodeProblem(writer, err, requestID) @@ -197,6 +219,8 @@ func (handler *Handler) switchRouting(writer http.ResponseWriter, request *http. return } command.RequestID = requestID + command.ActorID = identity.ClientID + command.SourceIP = identity.SourceIP command.Name = name result, err := handler.service.SwitchRouting(request.Context(), command) if err != nil { @@ -206,8 +230,17 @@ func (handler *Handler) switchRouting(writer http.ResponseWriter, request *http. writeMutation(writer, result, requestID) } -func (handler *Handler) reload(writer http.ResponseWriter, request *http.Request, requestID string) { - result, err := handler.service.ReloadConfiguration(request.Context(), ReloadCommand{RequestID: requestID}) +func (handler *Handler) reload( + writer http.ResponseWriter, + request *http.Request, + requestID string, + identity httpsecurity.Identity, +) { + result, err := handler.service.ReloadConfiguration(request.Context(), ReloadCommand{ + RequestID: requestID, + ActorID: identity.ClientID, + SourceIP: identity.SourceIP, + }) if err != nil { writeServiceProblem(writer, err, requestID) return diff --git a/internal/controller/admin/handler_test.go b/internal/controller/admin/handler_test.go index 660b4a0..fdefaf4 100644 --- a/internal/controller/admin/handler_test.go +++ b/internal/controller/admin/handler_test.go @@ -128,6 +128,9 @@ func TestHandlerEnablesAndDisablesUpstream(t *testing.T) { if service.lastUpstream.Name != "provider-a" || service.lastUpstream.Enabled != test.enabled || service.lastUpstream.RequestID != "req-admin" { t.Fatalf("unexpected service call: %+v", service.lastUpstream) } + if service.lastUpstream.ActorID != "admin:test" || service.lastUpstream.SourceIP != "192.0.2.10" { + t.Fatalf("upstream actor = (%q, %q)", service.lastUpstream.ActorID, service.lastUpstream.SourceIP) + } if requestID := recorder.Header().Get(httpapi.HeaderRequestID); requestID != "req-admin" { t.Fatalf("response request ID = %q, want req-admin", requestID) } @@ -152,6 +155,9 @@ func TestHandlerSwitchesRoutingWithStrictJSON(t *testing.T) { if service.lastSwitch.Name != "checkout" || service.lastSwitch.ExpectedCurrent != "provider-a" || service.lastSwitch.Target != "provider-b" { t.Fatalf("unexpected switch call: command=%+v", service.lastSwitch) } + if service.lastSwitch.ActorID != "admin:test" || service.lastSwitch.SourceIP != "192.0.2.10" { + t.Fatalf("switch actor = (%q, %q)", service.lastSwitch.ActorID, service.lastSwitch.SourceIP) + } } func TestHandlerReloadsConfigurationWithCommandRequestID(t *testing.T) { @@ -170,6 +176,9 @@ func TestHandlerReloadsConfigurationWithCommandRequestID(t *testing.T) { if service.lastReload.RequestID != "req-reload" { t.Fatalf("reload command = %+v", service.lastReload) } + if service.lastReload.ActorID != "admin:test" || service.lastReload.SourceIP != "192.0.2.10" { + t.Fatalf("reload actor = (%q, %q)", service.lastReload.ActorID, service.lastReload.SourceIP) + } if !strings.Contains(recorder.Body.String(), `"requestId":"req-reload"`) { t.Fatalf("unexpected reload response %q", recorder.Body.String()) } @@ -298,12 +307,14 @@ func (service *stubService) ReloadConfiguration(_ context.Context, command Reloa type allowAuthorizer struct{} -func (allowAuthorizer) Check(context.Context, *http.Request) error { return nil } +func (allowAuthorizer) Resolve(*http.Request) (httpsecurity.Identity, error) { + return httpsecurity.Identity{ClientID: "admin:test", SourceIP: "192.0.2.10"}, nil +} type rejectAuthorizer struct{} -func (rejectAuthorizer) Check(context.Context, *http.Request) error { - return &httpsecurity.HTTPError{ +func (rejectAuthorizer) Resolve(*http.Request) (httpsecurity.Identity, error) { + return httpsecurity.Identity{}, &httpsecurity.HTTPError{ StatusCode: http.StatusUnauthorized, Code: "UNAUTHORIZED", Header: http.Header{"WWW-Authenticate": []string{`Basic realm="proxy-pool"`}},