Skip to content

Commit f8d3fbf

Browse files
authored
feat: extend request logs with auth & DB info (#17497)
Closes #16903
1 parent 991d38c commit f8d3fbf

File tree

19 files changed

+334
-34
lines changed

19 files changed

+334
-34
lines changed

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,7 @@ GEN_FILES := \
582582
coderd/database/pubsub/psmock/psmock.go \
583583
agent/agentcontainers/acmock/acmock.go \
584584
agent/agentcontainers/dcspec/dcspec_gen.go \
585-
coderd/httpmw/loggermock/loggermock.go
585+
coderd/httpmw/loggermw/loggermock/loggermock.go
586586

587587
# all gen targets should be added here and to gen/mark-fresh
588588
gen: gen/db gen/golden-files $(GEN_FILES)
@@ -631,7 +631,7 @@ gen/mark-fresh:
631631
coderd/database/pubsub/psmock/psmock.go \
632632
agent/agentcontainers/acmock/acmock.go \
633633
agent/agentcontainers/dcspec/dcspec_gen.go \
634-
coderd/httpmw/loggermock/loggermock.go \
634+
coderd/httpmw/loggermw/loggermock/loggermock.go \
635635
"
636636

637637
for file in $$files; do
@@ -671,8 +671,8 @@ agent/agentcontainers/acmock/acmock.go: agent/agentcontainers/containers.go
671671
go generate ./agent/agentcontainers/acmock/
672672
touch "$@"
673673

674-
coderd/httpmw/loggermock/loggermock.go: coderd/httpmw/logger.go
675-
go generate ./coderd/httpmw/loggermock/
674+
coderd/httpmw/loggermw/loggermock/loggermock.go: coderd/httpmw/loggermw/logger.go
675+
go generate ./coderd/httpmw/loggermw/loggermock/
676676
touch "$@"
677677

678678
agent/agentcontainers/dcspec/dcspec_gen.go: \

coderd/coderd.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import (
6464
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
6565
"github.com/coder/coder/v2/coderd/httpapi"
6666
"github.com/coder/coder/v2/coderd/httpmw"
67+
"github.com/coder/coder/v2/coderd/httpmw/loggermw"
6768
"github.com/coder/coder/v2/coderd/metricscache"
6869
"github.com/coder/coder/v2/coderd/notifications"
6970
"github.com/coder/coder/v2/coderd/portsharing"
@@ -801,7 +802,7 @@ func New(options *Options) *API {
801802
tracing.Middleware(api.TracerProvider),
802803
httpmw.AttachRequestID,
803804
httpmw.ExtractRealIP(api.RealIPConfig),
804-
httpmw.Logger(api.Logger),
805+
loggermw.Logger(api.Logger),
805806
singleSlashMW,
806807
rolestore.CustomRoleMW,
807808
prometheusMW,

coderd/database/dbauthz/dbauthz.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/coder/coder/v2/coderd/database"
2525
"github.com/coder/coder/v2/coderd/database/dbtime"
2626
"github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints"
27+
"github.com/coder/coder/v2/coderd/httpmw/loggermw"
2728
"github.com/coder/coder/v2/coderd/rbac"
2829
"github.com/coder/coder/v2/coderd/util/slice"
2930
"github.com/coder/coder/v2/provisionersdk"
@@ -162,6 +163,7 @@ func ActorFromContext(ctx context.Context) (rbac.Subject, bool) {
162163

163164
var (
164165
subjectProvisionerd = rbac.Subject{
166+
Type: rbac.SubjectTypeProvisionerd,
165167
FriendlyName: "Provisioner Daemon",
166168
ID: uuid.Nil.String(),
167169
Roles: rbac.Roles([]rbac.Role{
@@ -196,6 +198,7 @@ var (
196198
}.WithCachedASTValue()
197199

198200
subjectAutostart = rbac.Subject{
201+
Type: rbac.SubjectTypeAutostart,
199202
FriendlyName: "Autostart",
200203
ID: uuid.Nil.String(),
201204
Roles: rbac.Roles([]rbac.Role{
@@ -219,6 +222,7 @@ var (
219222

220223
// See unhanger package.
221224
subjectHangDetector = rbac.Subject{
225+
Type: rbac.SubjectTypeHangDetector,
222226
FriendlyName: "Hang Detector",
223227
ID: uuid.Nil.String(),
224228
Roles: rbac.Roles([]rbac.Role{
@@ -239,6 +243,7 @@ var (
239243

240244
// See cryptokeys package.
241245
subjectCryptoKeyRotator = rbac.Subject{
246+
Type: rbac.SubjectTypeCryptoKeyRotator,
242247
FriendlyName: "Crypto Key Rotator",
243248
ID: uuid.Nil.String(),
244249
Roles: rbac.Roles([]rbac.Role{
@@ -257,6 +262,7 @@ var (
257262

258263
// See cryptokeys package.
259264
subjectCryptoKeyReader = rbac.Subject{
265+
Type: rbac.SubjectTypeCryptoKeyReader,
260266
FriendlyName: "Crypto Key Reader",
261267
ID: uuid.Nil.String(),
262268
Roles: rbac.Roles([]rbac.Role{
@@ -274,6 +280,7 @@ var (
274280
}.WithCachedASTValue()
275281

276282
subjectNotifier = rbac.Subject{
283+
Type: rbac.SubjectTypeNotifier,
277284
FriendlyName: "Notifier",
278285
ID: uuid.Nil.String(),
279286
Roles: rbac.Roles([]rbac.Role{
@@ -294,6 +301,7 @@ var (
294301
}.WithCachedASTValue()
295302

296303
subjectResourceMonitor = rbac.Subject{
304+
Type: rbac.SubjectTypeResourceMonitor,
297305
FriendlyName: "Resource Monitor",
298306
ID: uuid.Nil.String(),
299307
Roles: rbac.Roles([]rbac.Role{
@@ -312,6 +320,7 @@ var (
312320
}.WithCachedASTValue()
313321

314322
subjectSystemRestricted = rbac.Subject{
323+
Type: rbac.SubjectTypeSystemRestricted,
315324
FriendlyName: "System",
316325
ID: uuid.Nil.String(),
317326
Roles: rbac.Roles([]rbac.Role{
@@ -346,6 +355,7 @@ var (
346355
}.WithCachedASTValue()
347356

348357
subjectSystemReadProvisionerDaemons = rbac.Subject{
358+
Type: rbac.SubjectTypeSystemReadProvisionerDaemons,
349359
FriendlyName: "Provisioner Daemons Reader",
350360
ID: uuid.Nil.String(),
351361
Roles: rbac.Roles([]rbac.Role{
@@ -366,53 +376,53 @@ var (
366376
// AsProvisionerd returns a context with an actor that has permissions required
367377
// for provisionerd to function.
368378
func AsProvisionerd(ctx context.Context) context.Context {
369-
return context.WithValue(ctx, authContextKey{}, subjectProvisionerd)
379+
return As(ctx, subjectProvisionerd)
370380
}
371381

372382
// AsAutostart returns a context with an actor that has permissions required
373383
// for autostart to function.
374384
func AsAutostart(ctx context.Context) context.Context {
375-
return context.WithValue(ctx, authContextKey{}, subjectAutostart)
385+
return As(ctx, subjectAutostart)
376386
}
377387

378388
// AsHangDetector returns a context with an actor that has permissions required
379389
// for unhanger.Detector to function.
380390
func AsHangDetector(ctx context.Context) context.Context {
381-
return context.WithValue(ctx, authContextKey{}, subjectHangDetector)
391+
return As(ctx, subjectHangDetector)
382392
}
383393

384394
// AsKeyRotator returns a context with an actor that has permissions required for rotating crypto keys.
385395
func AsKeyRotator(ctx context.Context) context.Context {
386-
return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyRotator)
396+
return As(ctx, subjectCryptoKeyRotator)
387397
}
388398

389399
// AsKeyReader returns a context with an actor that has permissions required for reading crypto keys.
390400
func AsKeyReader(ctx context.Context) context.Context {
391-
return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyReader)
401+
return As(ctx, subjectCryptoKeyReader)
392402
}
393403

394404
// AsNotifier returns a context with an actor that has permissions required for
395405
// creating/reading/updating/deleting notifications.
396406
func AsNotifier(ctx context.Context) context.Context {
397-
return context.WithValue(ctx, authContextKey{}, subjectNotifier)
407+
return As(ctx, subjectNotifier)
398408
}
399409

400410
// AsResourceMonitor returns a context with an actor that has permissions required for
401411
// updating resource monitors.
402412
func AsResourceMonitor(ctx context.Context) context.Context {
403-
return context.WithValue(ctx, authContextKey{}, subjectResourceMonitor)
413+
return As(ctx, subjectResourceMonitor)
404414
}
405415

406416
// AsSystemRestricted returns a context with an actor that has permissions
407417
// required for various system operations (login, logout, metrics cache).
408418
func AsSystemRestricted(ctx context.Context) context.Context {
409-
return context.WithValue(ctx, authContextKey{}, subjectSystemRestricted)
419+
return As(ctx, subjectSystemRestricted)
410420
}
411421

412422
// AsSystemReadProvisionerDaemons returns a context with an actor that has permissions
413423
// to read provisioner daemons.
414424
func AsSystemReadProvisionerDaemons(ctx context.Context) context.Context {
415-
return context.WithValue(ctx, authContextKey{}, subjectSystemReadProvisionerDaemons)
425+
return As(ctx, subjectSystemReadProvisionerDaemons)
416426
}
417427

418428
var AsRemoveActor = rbac.Subject{
@@ -430,6 +440,9 @@ func As(ctx context.Context, actor rbac.Subject) context.Context {
430440
// should be removed from the context.
431441
return context.WithValue(ctx, authContextKey{}, nil)
432442
}
443+
if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil {
444+
rlogger.WithAuthContext(actor)
445+
}
433446
return context.WithValue(ctx, authContextKey{}, actor)
434447
}
435448

coderd/database/queries.sql.go

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/users.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,10 +271,10 @@ WHERE
271271
-- This function returns roles for authorization purposes. Implied member roles
272272
-- are included.
273273
SELECT
274-
-- username is returned just to help for logging purposes
274+
-- username and email are returned just to help for logging purposes
275275
-- status is used to enforce 'suspended' users, as all roles are ignored
276276
-- when suspended.
277-
id, username, status,
277+
id, username, status, email,
278278
-- All user roles, including their org roles.
279279
array_cat(
280280
-- All users are members

coderd/httpmw/apikey.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,9 @@ func UserRBACSubject(ctx context.Context, db database.Store, userID uuid.UUID, s
465465
}
466466

467467
actor := rbac.Subject{
468+
Type: rbac.SubjectTypeUser,
468469
FriendlyName: roles.Username,
470+
Email: roles.Email,
469471
ID: userID.String(),
470472
Roles: rbacRoles,
471473
Groups: roles.Groups,

coderd/httpmw/logger.go renamed to coderd/httpmw/loggermw/logger.go

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
package httpmw
1+
package loggermw
22

33
import (
44
"context"
55
"fmt"
66
"net/http"
7+
"sync"
78
"time"
89

10+
"github.com/go-chi/chi/v5"
11+
912
"cdr.dev/slog"
1013
"github.com/coder/coder/v2/coderd/httpapi"
14+
"github.com/coder/coder/v2/coderd/rbac"
1115
"github.com/coder/coder/v2/coderd/tracing"
1216
)
1317

@@ -62,13 +66,17 @@ func Logger(log slog.Logger) func(next http.Handler) http.Handler {
6266
type RequestLogger interface {
6367
WithFields(fields ...slog.Field)
6468
WriteLog(ctx context.Context, status int)
69+
WithAuthContext(actor rbac.Subject)
6570
}
6671

6772
type SlogRequestLogger struct {
6873
log slog.Logger
6974
written bool
7075
message string
7176
start time.Time
77+
// Protects actors map for concurrent writes.
78+
mu sync.RWMutex
79+
actors map[rbac.SubjectType]rbac.Subject
7280
}
7381

7482
var _ RequestLogger = &SlogRequestLogger{}
@@ -79,25 +87,93 @@ func NewRequestLogger(log slog.Logger, message string, start time.Time) RequestL
7987
written: false,
8088
message: message,
8189
start: start,
90+
actors: make(map[rbac.SubjectType]rbac.Subject),
8291
}
8392
}
8493

8594
func (c *SlogRequestLogger) WithFields(fields ...slog.Field) {
8695
c.log = c.log.With(fields...)
8796
}
8897

98+
func (c *SlogRequestLogger) WithAuthContext(actor rbac.Subject) {
99+
c.mu.Lock()
100+
defer c.mu.Unlock()
101+
c.actors[actor.Type] = actor
102+
}
103+
104+
func (c *SlogRequestLogger) addAuthContextFields() {
105+
c.mu.RLock()
106+
defer c.mu.RUnlock()
107+
108+
usr, ok := c.actors[rbac.SubjectTypeUser]
109+
if ok {
110+
c.log = c.log.With(
111+
slog.F("requestor_id", usr.ID),
112+
slog.F("requestor_name", usr.FriendlyName),
113+
slog.F("requestor_email", usr.Email),
114+
)
115+
} else {
116+
// If there is no user, we log the requestor name for the first
117+
// actor in a defined order.
118+
for _, v := range actorLogOrder {
119+
subj, ok := c.actors[v]
120+
if !ok {
121+
continue
122+
}
123+
c.log = c.log.With(
124+
slog.F("requestor_name", subj.FriendlyName),
125+
)
126+
break
127+
}
128+
}
129+
}
130+
131+
var actorLogOrder = []rbac.SubjectType{
132+
rbac.SubjectTypeAutostart,
133+
rbac.SubjectTypeCryptoKeyReader,
134+
rbac.SubjectTypeCryptoKeyRotator,
135+
rbac.SubjectTypeHangDetector,
136+
rbac.SubjectTypeNotifier,
137+
rbac.SubjectTypePrebuildsOrchestrator,
138+
rbac.SubjectTypeProvisionerd,
139+
rbac.SubjectTypeResourceMonitor,
140+
rbac.SubjectTypeSystemReadProvisionerDaemons,
141+
rbac.SubjectTypeSystemRestricted,
142+
}
143+
89144
func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) {
90145
if c.written {
91146
return
92147
}
93148
c.written = true
94149
end := time.Now()
95150

151+
// Right before we write the log, we try to find the user in the actors
152+
// and add the fields to the log.
153+
c.addAuthContextFields()
154+
96155
logger := c.log.With(
97156
slog.F("took", end.Sub(c.start)),
98157
slog.F("status_code", status),
99158
slog.F("latency_ms", float64(end.Sub(c.start)/time.Millisecond)),
100159
)
160+
161+
// If the request is routed, add the route parameters to the log.
162+
if chiCtx := chi.RouteContext(ctx); chiCtx != nil {
163+
urlParams := chiCtx.URLParams
164+
routeParamsFields := make([]slog.Field, 0, len(urlParams.Keys))
165+
166+
for k, v := range urlParams.Keys {
167+
if urlParams.Values[k] != "" {
168+
routeParamsFields = append(routeParamsFields, slog.F("params_"+v, urlParams.Values[k]))
169+
}
170+
}
171+
172+
if len(routeParamsFields) > 0 {
173+
logger = logger.With(routeParamsFields...)
174+
}
175+
}
176+
101177
// We already capture most of this information in the span (minus
102178
// the response body which we don't want to capture anyways).
103179
tracing.RunWithoutSpan(ctx, func(ctx context.Context) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy