Record audit events for API token authorization

This commit is contained in:
Joe Julian
2026-03-29 23:19:54 -07:00
parent e9eca73336
commit 794eed04d4
4 changed files with 213 additions and 0 deletions
+50
View File
@@ -10,6 +10,7 @@ import (
"sync"
"time"
"git.julianfamily.org/keepassgo/apiaudit"
"git.julianfamily.org/keepassgo/apiapproval"
"git.julianfamily.org/keepassgo/apitokens"
"git.julianfamily.org/keepassgo/clipboard"
@@ -35,6 +36,7 @@ type Server struct {
profiles map[string]passwords.Profile
clipboard clipboard.Writer
approvals *apiapproval.Broker
audit *apiaudit.Log
}
type lifecycleBackend interface {
@@ -57,6 +59,7 @@ func NewServer(model vault.Model, profiles map[string]passwords.Profile, clipboa
profiles: profiles,
clipboard: clipboardWriter,
approvals: apiapproval.NewBroker(30 * time.Second),
audit: apiaudit.New(200),
}
}
@@ -70,6 +73,10 @@ func (s *Server) ApprovalBroker() *apiapproval.Broker {
return s.approvals
}
func (s *Server) AuditLog() *apiaudit.Log {
return s.audit
}
func (s *Server) ResolveApproval(id string, outcome apiapproval.Outcome) (apiapproval.Request, error) {
request, _, err := s.approvals.Resolve(id, outcome)
return request, err
@@ -777,10 +784,12 @@ func (s *Server) authenticateRequest(ctx context.Context) (apitokens.Token, erro
}
values := md.Get("authorization")
if len(values) == 0 {
s.audit.Record(apiaudit.Event{Type: apiaudit.EventAuthRejected, Message: "missing authorization"})
return apitokens.Token{}, status.Error(codes.Unauthenticated, "missing authorization")
}
const prefix = "Bearer "
if !strings.HasPrefix(values[0], prefix) {
s.audit.Record(apiaudit.Event{Type: apiaudit.EventAuthRejected, Message: "invalid bearer token"})
return apitokens.Token{}, status.Error(codes.Unauthenticated, "invalid bearer token")
}
s.mu.RLock()
@@ -793,6 +802,7 @@ func (s *Server) authenticateRequest(ctx context.Context) (apitokens.Token, erro
if err != nil {
switch err {
case apitokens.ErrInvalidToken, apitokens.ErrExpiredToken, apitokens.ErrDisabledToken:
s.audit.Record(apiaudit.Event{Type: apiaudit.EventAuthRejected, Message: err.Error()})
return apitokens.Token{}, status.Error(codes.Unauthenticated, err.Error())
default:
return apitokens.Token{}, status.Errorf(codes.Internal, "authenticate api token: %v", err)
@@ -824,6 +834,14 @@ func (s *Server) authorizeResourceRequest(ctx context.Context, token apitokens.T
case apitokens.DecisionDeny:
return apitokens.Token{}, status.Error(codes.PermissionDenied, "access is not allowed for this token")
case apitokens.DecisionPrompt:
s.audit.Record(apiaudit.Event{
Type: apiaudit.EventApprovalRequested,
TokenID: token.ID,
TokenName: token.Name,
ClientName: token.ClientName,
Operation: op,
Resource: resource,
})
result, err := s.approvals.Request(ctx, token, op, resource)
if result.Rule != nil {
if persistErr := s.persistApprovalRule(token.ID, *result.Rule); persistErr != nil {
@@ -832,12 +850,44 @@ func (s *Server) authorizeResourceRequest(ctx context.Context, token apitokens.T
}
switch {
case err == nil:
s.audit.Record(apiaudit.Event{
Type: apiaudit.EventApprovalAllowed,
TokenID: token.ID,
TokenName: token.Name,
ClientName: token.ClientName,
Operation: op,
Resource: resource,
})
return token, nil
case errors.Is(err, apiapproval.ErrRequestDenied):
s.audit.Record(apiaudit.Event{
Type: apiaudit.EventApprovalDenied,
TokenID: token.ID,
TokenName: token.Name,
ClientName: token.ClientName,
Operation: op,
Resource: resource,
})
return apitokens.Token{}, status.Error(codes.PermissionDenied, "access denied by user approval")
case errors.Is(err, apiapproval.ErrRequestCanceled):
s.audit.Record(apiaudit.Event{
Type: apiaudit.EventApprovalCanceled,
TokenID: token.ID,
TokenName: token.Name,
ClientName: token.ClientName,
Operation: op,
Resource: resource,
})
return apitokens.Token{}, status.Error(codes.Unauthenticated, "authorization request canceled")
case errors.Is(err, apiapproval.ErrRequestTimedOut):
s.audit.Record(apiaudit.Event{
Type: apiaudit.EventApprovalTimedOut,
TokenID: token.ID,
TokenName: token.Name,
ClientName: token.ClientName,
Operation: op,
Resource: resource,
})
return apitokens.Token{}, status.Error(codes.DeadlineExceeded, "authorization request timed out")
case errors.Is(err, context.Canceled):
return apitokens.Token{}, status.Error(codes.Canceled, "authorization request canceled")