@@ -3,6 +3,7 @@ package audit
3
3
import (
4
4
"context"
5
5
"encoding/json"
6
+ "fmt"
6
7
"net"
7
8
"net/http"
8
9
@@ -11,20 +12,17 @@ import (
11
12
12
13
"cdr.dev/slog"
13
14
"github.com/coder/coder/coderd/database"
15
+ "github.com/coder/coder/coderd/features"
14
16
"github.com/coder/coder/coderd/httpapi"
15
17
"github.com/coder/coder/coderd/httpmw"
16
18
)
17
19
18
20
type 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
28
26
}
29
27
30
28
type Request [T Auditable ] struct {
@@ -34,6 +32,63 @@ type Request[T Auditable] struct {
34
32
New T
35
33
}
36
34
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
+
37
92
// InitRequest initializes an audit log for a request. It returns a function
38
93
// that should be deferred, causing the audit log to be committed when the
39
94
// handler returns.
@@ -47,38 +102,64 @@ func InitRequest[T Auditable](w http.ResponseWriter, p *RequestParams) (*Request
47
102
params : p ,
48
103
}
49
104
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
+
50
114
return req , func () {
51
115
ctx := context .Background ()
116
+ logCtx := p .Request .Context ()
52
117
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 )
54
124
diffRaw , _ := json .Marshal (diff )
55
125
56
126
ip , err := parseIP (p .Request .RemoteAddr )
57
127
if err != nil {
58
- p .Log .Warn (ctx , "parse ip" , slog .Error (err ))
128
+ p .Log .Warn (logCtx , "parse ip" , slog .Error (err ))
59
129
}
60
130
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 ("{}" ),
74
145
})
75
146
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 ))
77
148
return
78
149
}
79
150
}
80
151
}
81
152
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
+
82
163
func parseIP (ipStr string ) (pqtype.Inet , error ) {
83
164
var err error
84
165
0 commit comments