feat: add auditing to user routes (#3961)

This commit is contained in:
Colin Adler
2022-09-08 21:16:16 -05:00
committed by GitHub
parent c026464375
commit 4e26e325a6
11 changed files with 258 additions and 61 deletions
+15
View File
@@ -22,3 +22,18 @@ func (nop) Export(context.Context, database.AuditLog) error {
}
func (nop) diff(any, any) Map { return Map{} }
func NewMock() *MockAuditor {
return &MockAuditor{}
}
type MockAuditor struct {
AuditLogs []database.AuditLog
}
func (a *MockAuditor) Export(_ context.Context, alog database.AuditLog) error {
a.AuditLogs = append(a.AuditLogs, alog)
return nil
}
func (*MockAuditor) diff(any, any) Map { return Map{} }
+105 -24
View File
@@ -3,6 +3,7 @@ package audit
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
@@ -11,20 +12,17 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/coderd/features"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
)
type RequestParams struct {
Audit Auditor
Log slog.Logger
Features features.Service
Log slog.Logger
Request *http.Request
ResourceID uuid.UUID
ResourceTarget string
Action database.AuditAction
ResourceType database.ResourceType
Actor uuid.UUID
Request *http.Request
Action database.AuditAction
}
type Request[T Auditable] struct {
@@ -34,6 +32,63 @@ type Request[T Auditable] struct {
New T
}
func ResourceTarget[T Auditable](tgt T) string {
switch typed := any(tgt).(type) {
case database.Organization:
return typed.Name
case database.Template:
return typed.Name
case database.TemplateVersion:
return typed.Name
case database.User:
return typed.Username
case database.Workspace:
return typed.Name
case database.GitSSHKey:
return typed.PublicKey
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
}
func ResourceID[T Auditable](tgt T) uuid.UUID {
switch typed := any(tgt).(type) {
case database.Organization:
return typed.ID
case database.Template:
return typed.ID
case database.TemplateVersion:
return typed.ID
case database.User:
return typed.ID
case database.Workspace:
return typed.ID
case database.GitSSHKey:
return typed.UserID
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
}
func ResourceType[T Auditable](tgt T) database.ResourceType {
switch any(tgt).(type) {
case database.Organization:
return database.ResourceTypeOrganization
case database.Template:
return database.ResourceTypeTemplate
case database.TemplateVersion:
return database.ResourceTypeTemplateVersion
case database.User:
return database.ResourceTypeUser
case database.Workspace:
return database.ResourceTypeWorkspace
case database.GitSSHKey:
return database.ResourceTypeGitSshKey
default:
panic(fmt.Sprintf("unknown resource %T", tgt))
}
}
// InitRequest initializes an audit log for a request. It returns a function
// that should be deferred, causing the audit log to be committed when the
// handler returns.
@@ -47,38 +102,64 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
params: p,
}
feats := struct {
Audit Auditor
}{}
err := p.Features.Get(&feats)
if err != nil {
p.Log.Error(p.Request.Context(), "unable to get auditor interface", slog.Error(err))
return req, func() {}
}
return req, func() {
ctx := context.Background()
logCtx := p.Request.Context()
diff := Diff(p.Audit, req.Old, req.New)
// If no resources were provided, there's nothing we can audit.
if ResourceID(req.Old) == uuid.Nil && ResourceID(req.New) == uuid.Nil {
return
}
diff := Diff(feats.Audit, req.Old, req.New)
diffRaw, _ := json.Marshal(diff)
ip, err := parseIP(p.Request.RemoteAddr)
if err != nil {
p.Log.Warn(ctx, "parse ip", slog.Error(err))
p.Log.Warn(logCtx, "parse ip", slog.Error(err))
}
err = p.Audit.Export(ctx, database.AuditLog{
ID: uuid.New(),
Time: database.Now(),
UserID: p.Actor,
Ip: ip,
UserAgent: p.Request.UserAgent(),
ResourceType: p.ResourceType,
ResourceID: p.ResourceID,
ResourceTarget: p.ResourceTarget,
Action: p.Action,
Diff: diffRaw,
StatusCode: int32(sw.Status),
RequestID: httpmw.RequestID(p.Request),
err = feats.Audit.Export(ctx, database.AuditLog{
ID: uuid.New(),
Time: database.Now(),
UserID: httpmw.APIKey(p.Request).UserID,
Ip: ip,
UserAgent: p.Request.UserAgent(),
ResourceType: either(req.Old, req.New, ResourceType[T]),
ResourceID: either(req.Old, req.New, ResourceID[T]),
ResourceTarget: either(req.Old, req.New, ResourceTarget[T]),
Action: p.Action,
Diff: diffRaw,
StatusCode: int32(sw.Status),
RequestID: httpmw.RequestID(p.Request),
AdditionalFields: json.RawMessage("{}"),
})
if err != nil {
p.Log.Error(ctx, "export audit log", slog.Error(err))
p.Log.Error(logCtx, "export audit log", slog.Error(err))
return
}
}
}
func either[T Auditable, R any](old, new T, fn func(T) R) R {
if ResourceID(new) != uuid.Nil {
return fn(new)
} else if ResourceID(old) != uuid.Nil {
return fn(old)
} else {
panic("both old and new are nil")
}
}
func parseIP(ipStr string) (pqtype.Inet, error) {
var err error