@@ -3,6 +3,7 @@ package audit
33import (
44 "context"
55 "encoding/json"
6+ "fmt"
67 "net"
78 "net/http"
89
@@ -11,20 +12,17 @@ import (
1112
1213 "cdr.dev/slog"
1314 "github.com/coder/coder/coderd/database"
15+ "github.com/coder/coder/coderd/features"
1416 "github.com/coder/coder/coderd/httpapi"
1517 "github.com/coder/coder/coderd/httpmw"
1618)
1719
1820type RequestParams struct {
19- Audit Auditor
20- Log slog.Logger
21-
22- Request * http.Request
23- ResourceID uuid.UUID
24- ResourceTarget string
25- Action database.AuditAction
26- ResourceType database.ResourceType
27- Actor uuid.UUID
21+ Features features.Service
22+ Log slog.Logger
23+
24+ Request * http.Request
25+ Action database.AuditAction
2826}
2927
3028type Request [T Auditable ] struct {
@@ -34,6 +32,63 @@ type Request[T Auditable] struct {
3432 New T
3533}
3634
35+ func ResourceTarget [T Auditable ](tgt T ) string {
36+ switch typed := any (tgt ).(type ) {
37+ case database.Organization :
38+ return typed .Name
39+ case database.Template :
40+ return typed .Name
41+ case database.TemplateVersion :
42+ return typed .Name
43+ case database.User :
44+ return typed .Username
45+ case database.Workspace :
46+ return typed .Name
47+ case database.GitSSHKey :
48+ return typed .PublicKey
49+ default :
50+ panic (fmt .Sprintf ("unknown resource %T" , tgt ))
51+ }
52+ }
53+
54+ func ResourceID [T Auditable ](tgt T ) uuid.UUID {
55+ switch typed := any (tgt ).(type ) {
56+ case database.Organization :
57+ return typed .ID
58+ case database.Template :
59+ return typed .ID
60+ case database.TemplateVersion :
61+ return typed .ID
62+ case database.User :
63+ return typed .ID
64+ case database.Workspace :
65+ return typed .ID
66+ case database.GitSSHKey :
67+ return typed .UserID
68+ default :
69+ panic (fmt .Sprintf ("unknown resource %T" , tgt ))
70+ }
71+ }
72+
73+ func ResourceType [T Auditable ](tgt T ) database.ResourceType {
74+ switch any (tgt ).(type ) {
75+ case database.Organization :
76+ return database .ResourceTypeOrganization
77+ case database.Template :
78+ return database .ResourceTypeTemplate
79+ case database.TemplateVersion :
80+ return database .ResourceTypeTemplateVersion
81+ case database.User :
82+ return database .ResourceTypeUser
83+ case database.Workspace :
84+ return database .ResourceTypeWorkspace
85+ case database.GitSSHKey :
86+ return database .ResourceTypeGitSshKey
87+ default :
88+ panic (fmt .Sprintf ("unknown resource %T" , tgt ))
89+ }
90+ }
91+
3792// InitRequest initializes an audit log for a request. It returns a function
3893// that should be deferred, causing the audit log to be committed when the
3994// handler returns.
@@ -47,38 +102,64 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
47102 params : p ,
48103 }
49104
105+ feats := struct {
106+ Audit Auditor
107+ }{}
108+ err := p .Features .Get (& feats )
109+ if err != nil {
110+ p .Log .Error (p .Request .Context (), "unable to get auditor interface" , slog .Error (err ))
111+ return req , func () {}
112+ }
113+
50114 return req , func () {
51115 ctx := context .Background ()
116+ logCtx := p .Request .Context ()
52117
53- diff := Diff (p .Audit , req .Old , req .New )
118+ // If no resources were provided, there's nothing we can audit.
119+ if ResourceID (req .Old ) == uuid .Nil && ResourceID (req .New ) == uuid .Nil {
120+ return
121+ }
122+
123+ diff := Diff (feats .Audit , req .Old , req .New )
54124 diffRaw , _ := json .Marshal (diff )
55125
56126 ip , err := parseIP (p .Request .RemoteAddr )
57127 if err != nil {
58- p .Log .Warn (ctx , "parse ip" , slog .Error (err ))
128+ p .Log .Warn (logCtx , "parse ip" , slog .Error (err ))
59129 }
60130
61- err = p .Audit .Export (ctx , database.AuditLog {
62- ID : uuid .New (),
63- Time : database .Now (),
64- UserID : p .Actor ,
65- Ip : ip ,
66- UserAgent : p .Request .UserAgent (),
67- ResourceType : p .ResourceType ,
68- ResourceID : p .ResourceID ,
69- ResourceTarget : p .ResourceTarget ,
70- Action : p .Action ,
71- Diff : diffRaw ,
72- StatusCode : int32 (sw .Status ),
73- RequestID : httpmw .RequestID (p .Request ),
131+ err = feats .Audit .Export (ctx , database.AuditLog {
132+ ID : uuid .New (),
133+ Time : database .Now (),
134+ UserID : httpmw .APIKey (p .Request ).UserID ,
135+ Ip : ip ,
136+ UserAgent : p .Request .UserAgent (),
137+ ResourceType : either (req .Old , req .New , ResourceType [T ]),
138+ ResourceID : either (req .Old , req .New , ResourceID [T ]),
139+ ResourceTarget : either (req .Old , req .New , ResourceTarget [T ]),
140+ Action : p .Action ,
141+ Diff : diffRaw ,
142+ StatusCode : int32 (sw .Status ),
143+ RequestID : httpmw .RequestID (p .Request ),
144+ AdditionalFields : json .RawMessage ("{}" ),
74145 })
75146 if err != nil {
76- p .Log .Error (ctx , "export audit log" , slog .Error (err ))
147+ p .Log .Error (logCtx , "export audit log" , slog .Error (err ))
77148 return
78149 }
79150 }
80151}
81152
153+ func either [T Auditable , R any ](old , new T , fn func (T ) R ) R {
154+ if ResourceID (new ) != uuid .Nil {
155+ return fn (new )
156+ } else if ResourceID (old ) != uuid .Nil {
157+ return fn (old )
158+ } else {
159+ panic ("both old and new are nil" )
160+ }
161+ }
162+
82163func parseIP (ipStr string ) (pqtype.Inet , error ) {
83164 var err error
84165
0 commit comments