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