Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ef322d4

Browse files
committedSep 8, 2022
feat: add auditing to user routes
1 parent 9c5b879 commit ef322d4

File tree

7 files changed

+190
-48
lines changed

7 files changed

+190
-48
lines changed
 

‎coderd/audit/request.go

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package audit
33
import (
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

1820
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
2826
}
2927

3028
type 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 ResourceID(req.Old) == uuid.Nil && ResourceID(req.New) == uuid.Nil {
119+
p.Log.Error(logCtx, "both old and new are nil, cannot audit")
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+
82163
func parseIP(ipStr string) (pqtype.Inet, error) {
83164
var err error
84165

‎coderd/coderd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/coder/coder/buildinfo"
2828
"github.com/coder/coder/coderd/awsidentity"
2929
"github.com/coder/coder/coderd/database"
30+
"github.com/coder/coder/coderd/features"
3031
"github.com/coder/coder/coderd/gitsshkey"
3132
"github.com/coder/coder/coderd/httpapi"
3233
"github.com/coder/coder/coderd/httpmw"
@@ -72,7 +73,7 @@ type Options struct {
7273
TracerProvider *sdktrace.TracerProvider
7374
AutoImportTemplates []AutoImportTemplate
7475
LicenseHandler http.Handler
75-
FeaturesService FeaturesService
76+
FeaturesService features.Service
7677

7778
TailscaleEnable bool
7879
TailnetCoordinator *tailnet.Coordinator

‎coderd/features.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,6 @@ import (
1111
"github.com/coder/coder/codersdk"
1212
)
1313

14-
// FeaturesService is the interface for interacting with enterprise features.
15-
type FeaturesService interface {
16-
EntitlementsAPI(w http.ResponseWriter, r *http.Request)
17-
18-
// Get returns the implementations for feature interfaces. Parameter `s` must be a pointer to a
19-
// struct type containing feature interfaces as fields. The FeatureService sets all fields to
20-
// the correct implementations depending on whether the features are turned on.
21-
Get(s any) error
22-
}
23-
2414
type featuresService struct{}
2515

2616
func (featuresService) EntitlementsAPI(rw http.ResponseWriter, _ *http.Request) {

‎coderd/features/features.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package features
2+
3+
import "net/http"
4+
5+
// Service is the interface for interacting with enterprise features.
6+
type Service interface {
7+
EntitlementsAPI(w http.ResponseWriter, r *http.Request)
8+
9+
// Get returns the implementations for feature interfaces. Parameter `s` must be a pointer to a
10+
// struct type containing feature interfaces as fields. The FeatureService sets all fields to
11+
// the correct implementations depending on whether the features are turned on.
12+
Get(s any) error
13+
}

‎coderd/users.go

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"golang.org/x/xerrors"
2222

2323
"cdr.dev/slog"
24+
"github.com/coder/coder/coderd/audit"
2425
"github.com/coder/coder/coderd/database"
2526
"github.com/coder/coder/coderd/gitsshkey"
2627
"github.com/coder/coder/coderd/httpapi"
@@ -254,6 +255,14 @@ func (api *API) users(rw http.ResponseWriter, r *http.Request) {
254255

255256
// Creates a new user.
256257
func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
258+
aReq, commitAudit := audit.InitRequest[database.User](rw, &audit.RequestParams{
259+
Features: api.FeaturesService,
260+
Log: api.Logger,
261+
Request: r,
262+
Action: database.AuditActionCreate,
263+
})
264+
defer commitAudit()
265+
257266
// Create the user on the site.
258267
if !api.Authorize(r, rbac.ActionCreate, rbac.ResourceUser) {
259268
httpapi.Forbidden(rw)
@@ -319,6 +328,8 @@ func (api *API) postUser(rw http.ResponseWriter, r *http.Request) {
319328
return
320329
}
321330

331+
aReq.New = user
332+
322333
// Report when users are added!
323334
api.Telemetry.Report(&telemetry.Snapshot{
324335
Users: []telemetry.User{telemetry.ConvertUser(user)},
@@ -350,7 +361,17 @@ func (api *API) userByName(rw http.ResponseWriter, r *http.Request) {
350361
}
351362

352363
func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) {
353-
user := httpmw.UserParam(r)
364+
var (
365+
user = httpmw.UserParam(r)
366+
aReq, commitAudit = audit.InitRequest[database.User](rw, &audit.RequestParams{
367+
Features: api.FeaturesService,
368+
Log: api.Logger,
369+
Request: r,
370+
Action: database.AuditActionWrite,
371+
})
372+
)
373+
defer commitAudit()
374+
aReq.Old = user
354375

355376
if !api.Authorize(r, rbac.ActionUpdate, rbac.ResourceUser) {
356377
httpapi.ResourceNotFound(rw)
@@ -395,6 +416,7 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) {
395416
Username: params.Username,
396417
UpdatedAt: database.Now(),
397418
})
419+
aReq.New = updatedUserProfile
398420

399421
if err != nil {
400422
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
@@ -418,8 +440,18 @@ func (api *API) putUserProfile(rw http.ResponseWriter, r *http.Request) {
418440

419441
func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseWriter, r *http.Request) {
420442
return func(rw http.ResponseWriter, r *http.Request) {
421-
user := httpmw.UserParam(r)
422-
apiKey := httpmw.APIKey(r)
443+
var (
444+
user = httpmw.UserParam(r)
445+
apiKey = httpmw.APIKey(r)
446+
aReq, commitAudit = audit.InitRequest[database.User](rw, &audit.RequestParams{
447+
Features: api.FeaturesService,
448+
Log: api.Logger,
449+
Request: r,
450+
Action: database.AuditActionWrite,
451+
})
452+
)
453+
defer commitAudit()
454+
aReq.Old = user
423455

424456
if !api.Authorize(r, rbac.ActionDelete, rbac.ResourceUser) {
425457
httpapi.ResourceNotFound(rw)
@@ -451,14 +483,14 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW
451483
Status: status,
452484
UpdatedAt: database.Now(),
453485
})
454-
455486
if err != nil {
456487
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
457488
Message: fmt.Sprintf("Internal error updating user's status to %q.", status),
458489
Detail: err.Error(),
459490
})
460491
return
461492
}
493+
aReq.New = suspendedUser
462494

463495
organizations, err := userOrganizationIDs(r.Context(), api, user)
464496
if err != nil {
@@ -475,9 +507,17 @@ func (api *API) putUserStatus(status database.UserStatus) func(rw http.ResponseW
475507

476508
func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
477509
var (
478-
user = httpmw.UserParam(r)
479-
params codersdk.UpdateUserPasswordRequest
510+
user = httpmw.UserParam(r)
511+
params codersdk.UpdateUserPasswordRequest
512+
aReq, commitAudit = audit.InitRequest[database.User](rw, &audit.RequestParams{
513+
Features: api.FeaturesService,
514+
Log: api.Logger,
515+
Request: r,
516+
Action: database.AuditActionWrite,
517+
})
480518
)
519+
defer commitAudit()
520+
aReq.Old = user
481521

482522
if !api.Authorize(r, rbac.ActionUpdate, rbac.ResourceUserData.WithOwner(user.ID.String())) {
483523
httpapi.ResourceNotFound(rw)
@@ -552,6 +592,10 @@ func (api *API) putUserPassword(rw http.ResponseWriter, r *http.Request) {
552592
return
553593
}
554594

595+
newUser := user
596+
newUser.HashedPassword = []byte(hashedPassword)
597+
aReq.New = newUser
598+
555599
httpapi.Write(rw, http.StatusNoContent, nil)
556600
}
557601

@@ -598,10 +642,20 @@ func (api *API) userRoles(rw http.ResponseWriter, r *http.Request) {
598642
}
599643

600644
func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
601-
// User is the user to modify.
602-
user := httpmw.UserParam(r)
603-
actorRoles := httpmw.AuthorizationUserRoles(r)
604-
apiKey := httpmw.APIKey(r)
645+
var (
646+
// User is the user to modify.
647+
user = httpmw.UserParam(r)
648+
actorRoles = httpmw.AuthorizationUserRoles(r)
649+
apiKey = httpmw.APIKey(r)
650+
aReq, commitAudit = audit.InitRequest[database.User](rw, &audit.RequestParams{
651+
Features: api.FeaturesService,
652+
Log: api.Logger,
653+
Request: r,
654+
Action: database.AuditActionWrite,
655+
})
656+
)
657+
defer commitAudit()
658+
aReq.Old = user
605659

606660
if apiKey.UserID == user.ID {
607661
httpapi.Write(rw, http.StatusBadRequest, codersdk.Response{
@@ -654,6 +708,7 @@ func (api *API) putUserRoles(rw http.ResponseWriter, r *http.Request) {
654708
})
655709
return
656710
}
711+
aReq.New = updatedUser
657712

658713
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
659714
if err != nil {
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)