feat: propagate admin audit identity

This commit is contained in:
youfak 2026-07-29 19:58:29 +08:00
parent 912db2aa18
commit 363d217c35
2 changed files with 62 additions and 18 deletions

View File

@ -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

View File

@ -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"`}},