mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add auditing to user routes (#3961)
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user