Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ac7dc71

Browse files
committed
feat: extend request logs with auth & DB info (#17304)
Closes #16903
1 parent c8db6e4 commit ac7dc71

18 files changed

+332
-34
lines changed

Makefile

+4-5
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ GEN_FILES := \
565565
$(TAILNETTEST_MOCKS) \
566566
coderd/database/pubsub/psmock/psmock.go \
567567
agent/agentcontainers/acmock/acmock.go \
568-
coderd/httpmw/loggermock/loggermock.go
568+
coderd/httpmw/loggermw/loggermock/loggermock.go
569569

570570

571571
# all gen targets should be added here and to gen/mark-fresh
@@ -601,7 +601,7 @@ gen/mark-fresh:
601601
$(TAILNETTEST_MOCKS) \
602602
coderd/database/pubsub/psmock/psmock.go \
603603
agent/agentcontainers/acmock/acmock.go \
604-
coderd/httpmw/loggermock/loggermock.go
604+
coderd/httpmw/loggermw/loggermock/loggermock.go
605605
"
606606

607607
for file in $$files; do
@@ -636,9 +636,8 @@ coderd/database/pubsub/psmock/psmock.go: coderd/database/pubsub/pubsub.go
636636
agent/agentcontainers/acmock/acmock.go: agent/agentcontainers/containers.go
637637
go generate ./agent/agentcontainers/acmock/
638638

639-
coderd/httpmw/loggermock/loggermock.go: coderd/httpmw/logger.go
640-
go generate ./coderd/httpmw/loggermock/
641-
touch "$@"
639+
coderd/httpmw/loggermw/loggermock/loggermock.go: coderd/httpmw/loggermw/logger.go
640+
go generate ./coderd/httpmw/loggermw/loggermock/
642641

643642
$(TAILNETTEST_MOCKS): tailnet/coordinator.go tailnet/service.go
644643
go generate ./tailnet/tailnettest/

coderd/coderd.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import (
6363
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
6464
"github.com/coder/coder/v2/coderd/httpapi"
6565
"github.com/coder/coder/v2/coderd/httpmw"
66+
"github.com/coder/coder/v2/coderd/httpmw/loggermw"
6667
"github.com/coder/coder/v2/coderd/metricscache"
6768
"github.com/coder/coder/v2/coderd/notifications"
6869
"github.com/coder/coder/v2/coderd/portsharing"
@@ -788,7 +789,7 @@ func New(options *Options) *API {
788789
tracing.Middleware(api.TracerProvider),
789790
httpmw.AttachRequestID,
790791
httpmw.ExtractRealIP(api.RealIPConfig),
791-
httpmw.Logger(api.Logger),
792+
loggermw.Logger(api.Logger),
792793
singleSlashMW,
793794
rolestore.CustomRoleMW,
794795
prometheusMW,

coderd/database/dbauthz/dbauthz.go

+22-9
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{
@@ -195,6 +197,7 @@ var (
195197
}.WithCachedASTValue()
196198

197199
subjectAutostart = rbac.Subject{
200+
Type: rbac.SubjectTypeAutostart,
198201
FriendlyName: "Autostart",
199202
ID: uuid.Nil.String(),
200203
Roles: rbac.Roles([]rbac.Role{
@@ -218,6 +221,7 @@ var (
218221

219222
// See unhanger package.
220223
subjectHangDetector = rbac.Subject{
224+
Type: rbac.SubjectTypeHangDetector,
221225
FriendlyName: "Hang Detector",
222226
ID: uuid.Nil.String(),
223227
Roles: rbac.Roles([]rbac.Role{
@@ -238,6 +242,7 @@ var (
238242

239243
// See cryptokeys package.
240244
subjectCryptoKeyRotator = rbac.Subject{
245+
Type: rbac.SubjectTypeCryptoKeyRotator,
241246
FriendlyName: "Crypto Key Rotator",
242247
ID: uuid.Nil.String(),
243248
Roles: rbac.Roles([]rbac.Role{
@@ -256,6 +261,7 @@ var (
256261

257262
// See cryptokeys package.
258263
subjectCryptoKeyReader = rbac.Subject{
264+
Type: rbac.SubjectTypeCryptoKeyReader,
259265
FriendlyName: "Crypto Key Reader",
260266
ID: uuid.Nil.String(),
261267
Roles: rbac.Roles([]rbac.Role{
@@ -273,6 +279,7 @@ var (
273279
}.WithCachedASTValue()
274280

275281
subjectNotifier = rbac.Subject{
282+
Type: rbac.SubjectTypeNotifier,
276283
FriendlyName: "Notifier",
277284
ID: uuid.Nil.String(),
278285
Roles: rbac.Roles([]rbac.Role{
@@ -290,6 +297,7 @@ var (
290297
}.WithCachedASTValue()
291298

292299
subjectResourceMonitor = rbac.Subject{
300+
Type: rbac.SubjectTypeResourceMonitor,
293301
FriendlyName: "Resource Monitor",
294302
ID: uuid.Nil.String(),
295303
Roles: rbac.Roles([]rbac.Role{
@@ -308,6 +316,7 @@ var (
308316
}.WithCachedASTValue()
309317

310318
subjectSystemRestricted = rbac.Subject{
319+
Type: rbac.SubjectTypeSystemRestricted,
311320
FriendlyName: "System",
312321
ID: uuid.Nil.String(),
313322
Roles: rbac.Roles([]rbac.Role{
@@ -342,6 +351,7 @@ var (
342351
}.WithCachedASTValue()
343352

344353
subjectSystemReadProvisionerDaemons = rbac.Subject{
354+
Type: rbac.SubjectTypeSystemReadProvisionerDaemons,
345355
FriendlyName: "Provisioner Daemons Reader",
346356
ID: uuid.Nil.String(),
347357
Roles: rbac.Roles([]rbac.Role{
@@ -362,53 +372,53 @@ var (
362372
// AsProvisionerd returns a context with an actor that has permissions required
363373
// for provisionerd to function.
364374
func AsProvisionerd(ctx context.Context) context.Context {
365-
return context.WithValue(ctx, authContextKey{}, subjectProvisionerd)
375+
return As(ctx, subjectProvisionerd)
366376
}
367377

368378
// AsAutostart returns a context with an actor that has permissions required
369379
// for autostart to function.
370380
func AsAutostart(ctx context.Context) context.Context {
371-
return context.WithValue(ctx, authContextKey{}, subjectAutostart)
381+
return As(ctx, subjectAutostart)
372382
}
373383

374384
// AsHangDetector returns a context with an actor that has permissions required
375385
// for unhanger.Detector to function.
376386
func AsHangDetector(ctx context.Context) context.Context {
377-
return context.WithValue(ctx, authContextKey{}, subjectHangDetector)
387+
return As(ctx, subjectHangDetector)
378388
}
379389

380390
// AsKeyRotator returns a context with an actor that has permissions required for rotating crypto keys.
381391
func AsKeyRotator(ctx context.Context) context.Context {
382-
return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyRotator)
392+
return As(ctx, subjectCryptoKeyRotator)
383393
}
384394

385395
// AsKeyReader returns a context with an actor that has permissions required for reading crypto keys.
386396
func AsKeyReader(ctx context.Context) context.Context {
387-
return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyReader)
397+
return As(ctx, subjectCryptoKeyReader)
388398
}
389399

390400
// AsNotifier returns a context with an actor that has permissions required for
391401
// creating/reading/updating/deleting notifications.
392402
func AsNotifier(ctx context.Context) context.Context {
393-
return context.WithValue(ctx, authContextKey{}, subjectNotifier)
403+
return As(ctx, subjectNotifier)
394404
}
395405

396406
// AsResourceMonitor returns a context with an actor that has permissions required for
397407
// updating resource monitors.
398408
func AsResourceMonitor(ctx context.Context) context.Context {
399-
return context.WithValue(ctx, authContextKey{}, subjectResourceMonitor)
409+
return As(ctx, subjectResourceMonitor)
400410
}
401411

402412
// AsSystemRestricted returns a context with an actor that has permissions
403413
// required for various system operations (login, logout, metrics cache).
404414
func AsSystemRestricted(ctx context.Context) context.Context {
405-
return context.WithValue(ctx, authContextKey{}, subjectSystemRestricted)
415+
return As(ctx, subjectSystemRestricted)
406416
}
407417

408418
// AsSystemReadProvisionerDaemons returns a context with an actor that has permissions
409419
// to read provisioner daemons.
410420
func AsSystemReadProvisionerDaemons(ctx context.Context) context.Context {
411-
return context.WithValue(ctx, authContextKey{}, subjectSystemReadProvisionerDaemons)
421+
return As(ctx, subjectSystemReadProvisionerDaemons)
412422
}
413423

414424
var AsRemoveActor = rbac.Subject{
@@ -426,6 +436,9 @@ func As(ctx context.Context, actor rbac.Subject) context.Context {
426436
// should be removed from the context.
427437
return context.WithValue(ctx, authContextKey{}, nil)
428438
}
439+
if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil {
440+
rlogger.WithAuthContext(actor)
441+
}
429442
return context.WithValue(ctx, authContextKey{}, actor)
430443
}
431444

coderd/database/queries.sql.go

+4-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/users.sql

+2-2
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,10 @@ WHERE
244244
-- This function returns roles for authorization purposes. Implied member roles
245245
-- are included.
246246
SELECT
247-
-- username is returned just to help for logging purposes
247+
-- username and email are returned just to help for logging purposes
248248
-- status is used to enforce 'suspended' users, as all roles are ignored
249249
-- when suspended.
250-
id, username, status,
250+
id, username, status, email,
251251
-- All user roles, including their org roles.
252252
array_cat(
253253
-- All users are members

coderd/httpmw/apikey.go

+2
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

+77-1
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)