From 1239656ab4a9e1bd13d82eb344956b7941cb3e5d Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:52:32 +0000 Subject: [PATCH 01/13] extended the user query to include email and added a handler for logger injection --- Makefile | 9 +++-- coderd/coderd.go | 3 +- coderd/database/dbauthz/dbauthz.go | 34 +++++++++++++------ coderd/database/queries.sql.go | 6 ++-- coderd/database/queries/users.sql | 4 +-- coderd/httpmw/apikey.go | 2 ++ coderd/httpmw/{ => loggermw}/logger.go | 33 +++++++++++++++++- .../{ => loggermw}/logger_internal_test.go | 4 +-- .../{ => loggermw}/loggermock/loggermock.go | 15 +++++++- coderd/inboxnotifications.go | 3 +- coderd/provisionerjobs.go | 3 +- coderd/provisionerjobs_internal_test.go | 6 ++-- coderd/rbac/authz.go | 28 +++++++++++++++ coderd/workspaceagents.go | 7 ++-- enterprise/coderd/provisionerdaemons.go | 3 +- enterprise/wsproxy/wsproxy.go | 3 +- tailnet/test/integration/integration.go | 4 +-- 17 files changed, 131 insertions(+), 36 deletions(-) rename coderd/httpmw/{ => loggermw}/logger.go (80%) rename coderd/httpmw/{ => loggermw}/logger_internal_test.go (98%) rename coderd/httpmw/{ => loggermw}/loggermock/loggermock.go (77%) diff --git a/Makefile b/Makefile index 6486f5cbed5fa..3efef044b86cf 100644 --- a/Makefile +++ b/Makefile @@ -582,7 +582,7 @@ GEN_FILES := \ coderd/database/pubsub/psmock/psmock.go \ agent/agentcontainers/acmock/acmock.go \ agent/agentcontainers/dcspec/dcspec_gen.go \ - coderd/httpmw/loggermock/loggermock.go + coderd/httpmw/loggermw/loggermock/loggermock.go # all gen targets should be added here and to gen/mark-fresh gen: gen/db gen/golden-files $(GEN_FILES) @@ -631,8 +631,7 @@ gen/mark-fresh: coderd/database/pubsub/psmock/psmock.go \ agent/agentcontainers/acmock/acmock.go \ agent/agentcontainers/dcspec/dcspec_gen.go \ - coderd/httpmw/loggermock/loggermock.go \ - " + coderd/httpmw/loggermw/loggermock/loggermock.go for file in $$files; do echo "$$file" @@ -671,8 +670,8 @@ agent/agentcontainers/acmock/acmock.go: agent/agentcontainers/containers.go go generate ./agent/agentcontainers/acmock/ touch "$@" -coderd/httpmw/loggermock/loggermock.go: coderd/httpmw/logger.go - go generate ./coderd/httpmw/loggermock/ +coderd/httpmw/loggermw/loggermock/loggermock.go: coderd/httpmw/loggermw/logger.go + go generate ./coderd/httpmw/loggermw/loggermock/ touch "$@" agent/agentcontainers/dcspec/dcspec_gen.go: \ diff --git a/coderd/coderd.go b/coderd/coderd.go index 43caf8b344edc..57aa4119ac44e 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -65,6 +65,7 @@ import ( "github.com/coder/coder/v2/coderd/healthcheck/derphealth" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/metricscache" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/portsharing" @@ -810,7 +811,7 @@ func New(options *Options) *API { tracing.Middleware(api.TracerProvider), httpmw.AttachRequestID, httpmw.ExtractRealIP(api.RealIPConfig), - httpmw.Logger(api.Logger), + loggermw.Logger(api.Logger), singleSlashMW, rolestore.CustomRoleMW, prometheusMW, diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index b9eb8b05e171e..ceb5ba7f2a15a 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -25,6 +25,7 @@ import ( "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/provisionersdk" @@ -163,6 +164,7 @@ func ActorFromContext(ctx context.Context) (rbac.Subject, bool) { var ( subjectProvisionerd = rbac.Subject{ + Type: rbac.SubjectTypeProvisionerd, FriendlyName: "Provisioner Daemon", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -197,6 +199,7 @@ var ( }.WithCachedASTValue() subjectAutostart = rbac.Subject{ + Type: rbac.SubjectTypeAutostart, FriendlyName: "Autostart", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -220,6 +223,7 @@ var ( // See unhanger package. subjectHangDetector = rbac.Subject{ + Type: rbac.SubjectTypeHangDetector, FriendlyName: "Hang Detector", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -240,6 +244,7 @@ var ( // See cryptokeys package. subjectCryptoKeyRotator = rbac.Subject{ + Type: rbac.SubjectTypeCryptoKeyRotator, FriendlyName: "Crypto Key Rotator", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -258,6 +263,7 @@ var ( // See cryptokeys package. subjectCryptoKeyReader = rbac.Subject{ + Type: rbac.SubjectTypeCryptoKeyReader, FriendlyName: "Crypto Key Reader", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -275,6 +281,7 @@ var ( }.WithCachedASTValue() subjectNotifier = rbac.Subject{ + Type: rbac.SubjectTypeNotifier, FriendlyName: "Notifier", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -295,6 +302,7 @@ var ( }.WithCachedASTValue() subjectResourceMonitor = rbac.Subject{ + Type: rbac.SubjectTypeResourceMonitor, FriendlyName: "Resource Monitor", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -313,6 +321,7 @@ var ( }.WithCachedASTValue() subjectSystemRestricted = rbac.Subject{ + Type: rbac.SubjectTypeSystemRestricted, FriendlyName: "System", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -347,6 +356,7 @@ var ( }.WithCachedASTValue() subjectSystemReadProvisionerDaemons = rbac.Subject{ + Type: rbac.SubjectTypeSystemReadProvisionerDaemons, FriendlyName: "Provisioner Daemons Reader", ID: uuid.Nil.String(), Roles: rbac.Roles([]rbac.Role{ @@ -364,6 +374,7 @@ var ( }.WithCachedASTValue() subjectPrebuildsOrchestrator = rbac.Subject{ + Type: rbac.SubjectTypePrebuildsOrchestrator, FriendlyName: "Prebuilds Orchestrator", ID: prebuilds.SystemUserID.String(), Roles: rbac.Roles([]rbac.Role{ @@ -388,59 +399,59 @@ var ( // AsProvisionerd returns a context with an actor that has permissions required // for provisionerd to function. func AsProvisionerd(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectProvisionerd) + return As(ctx, subjectProvisionerd) } // AsAutostart returns a context with an actor that has permissions required // for autostart to function. func AsAutostart(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectAutostart) + return As(ctx, subjectAutostart) } // AsHangDetector returns a context with an actor that has permissions required // for unhanger.Detector to function. func AsHangDetector(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectHangDetector) + return As(ctx, subjectHangDetector) } // AsKeyRotator returns a context with an actor that has permissions required for rotating crypto keys. func AsKeyRotator(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyRotator) + return As(ctx, subjectCryptoKeyRotator) } // AsKeyReader returns a context with an actor that has permissions required for reading crypto keys. func AsKeyReader(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectCryptoKeyReader) + return As(ctx, subjectCryptoKeyReader) } // AsNotifier returns a context with an actor that has permissions required for // creating/reading/updating/deleting notifications. func AsNotifier(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectNotifier) + return As(ctx, subjectNotifier) } // AsResourceMonitor returns a context with an actor that has permissions required for // updating resource monitors. func AsResourceMonitor(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectResourceMonitor) + return As(ctx, subjectResourceMonitor) } // AsSystemRestricted returns a context with an actor that has permissions // required for various system operations (login, logout, metrics cache). func AsSystemRestricted(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectSystemRestricted) + return As(ctx, subjectSystemRestricted) } // AsSystemReadProvisionerDaemons returns a context with an actor that has permissions // to read provisioner daemons. func AsSystemReadProvisionerDaemons(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectSystemReadProvisionerDaemons) + return As(ctx, subjectSystemReadProvisionerDaemons) } // AsPrebuildsOrchestrator returns a context with an actor that has permissions // to read orchestrator workspace prebuilds. func AsPrebuildsOrchestrator(ctx context.Context) context.Context { - return context.WithValue(ctx, authContextKey{}, subjectPrebuildsOrchestrator) + return As(ctx, subjectPrebuildsOrchestrator) } var AsRemoveActor = rbac.Subject{ @@ -458,6 +469,9 @@ func As(ctx context.Context, actor rbac.Subject) context.Context { // should be removed from the context. return context.WithValue(ctx, authContextKey{}, nil) } + if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil { + rlogger.WithAuthContext(actor) + } return context.WithValue(ctx, authContextKey{}, actor) } diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 0d5fa1bb7f060..5169a59dad299 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -12064,10 +12064,10 @@ func (q *sqlQuerier) GetActiveUserCount(ctx context.Context, includeSystem bool) const getAuthorizationUserRoles = `-- name: GetAuthorizationUserRoles :one SELECT - -- username is returned just to help for logging purposes + -- username and email are returned just to help for logging purposes -- status is used to enforce 'suspended' users, as all roles are ignored -- when suspended. - id, username, status, + id, username, status, email, -- All user roles, including their org roles. array_cat( -- All users are members @@ -12108,6 +12108,7 @@ type GetAuthorizationUserRolesRow struct { ID uuid.UUID `db:"id" json:"id"` Username string `db:"username" json:"username"` Status UserStatus `db:"status" json:"status"` + Email string `db:"email" json:"email"` Roles []string `db:"roles" json:"roles"` Groups []string `db:"groups" json:"groups"` } @@ -12121,6 +12122,7 @@ func (q *sqlQuerier) GetAuthorizationUserRoles(ctx context.Context, userID uuid. &i.ID, &i.Username, &i.Status, + &i.Email, pq.Array(&i.Roles), pq.Array(&i.Groups), ) diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index 8757b377728a3..eece2f96512ea 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -300,10 +300,10 @@ WHERE -- This function returns roles for authorization purposes. Implied member roles -- are included. SELECT - -- username is returned just to help for logging purposes + -- username and email are returned just to help for logging purposes -- status is used to enforce 'suspended' users, as all roles are ignored -- when suspended. - id, username, status, + id, username, status, email, -- All user roles, including their org roles. array_cat( -- All users are members diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index 1574affa30b65..d614b37a3d897 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -465,7 +465,9 @@ func UserRBACSubject(ctx context.Context, db database.Store, userID uuid.UUID, s } actor := rbac.Subject{ + Type: rbac.SubjectTypeUser, FriendlyName: roles.Username, + Email: roles.Email, ID: userID.String(), Roles: rbacRoles, Groups: roles.Groups, diff --git a/coderd/httpmw/logger.go b/coderd/httpmw/loggermw/logger.go similarity index 80% rename from coderd/httpmw/logger.go rename to coderd/httpmw/loggermw/logger.go index 0da964407b3e4..bccd55dadf501 100644 --- a/coderd/httpmw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -1,4 +1,4 @@ -package httpmw +package loggermw import ( "context" @@ -8,6 +8,7 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/tracing" ) @@ -62,6 +63,7 @@ func Logger(log slog.Logger) func(next http.Handler) http.Handler { type RequestLogger interface { WithFields(fields ...slog.Field) WriteLog(ctx context.Context, status int) + WithAuthContext(actor rbac.Subject) } type SlogRequestLogger struct { @@ -69,6 +71,7 @@ type SlogRequestLogger struct { written bool message string start time.Time + actors map[rbac.SubjectType]rbac.Subject } var _ RequestLogger = &SlogRequestLogger{} @@ -79,6 +82,7 @@ func NewRequestLogger(log slog.Logger, message string, start time.Time) RequestL written: false, message: message, start: start, + actors: make(map[rbac.SubjectType]rbac.Subject), } } @@ -86,6 +90,28 @@ func (c *SlogRequestLogger) WithFields(fields ...slog.Field) { c.log = c.log.With(fields...) } +func (c *SlogRequestLogger) WithAuthContext(actor rbac.Subject) { + c.actors[actor.Type] = actor +} + +func (c *SlogRequestLogger) addAuthContextFields() { + usr, ok := c.actors[rbac.SubjectTypeUser] + if ok { + c.log = c.log.With( + slog.F("requestor_id", usr.ID), + slog.F("requestor_name", usr.FriendlyName), + slog.F("requestor_email", usr.Email), + ) + } else if len(c.actors) > 0 { + for _, v := range c.actors { + c.log = c.log.With( + slog.F("requestor_name", v.FriendlyName), + ) + break + } + } +} + func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) { if c.written { return @@ -93,11 +119,16 @@ func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) { c.written = true end := time.Now() + // Right before we write the log, we try to find the user in the actors + // and add the fields to the log. + c.addAuthContextFields() + logger := c.log.With( slog.F("took", end.Sub(c.start)), slog.F("status_code", status), slog.F("latency_ms", float64(end.Sub(c.start)/time.Millisecond)), ) + // We already capture most of this information in the span (minus // the response body which we don't want to capture anyways). tracing.RunWithoutSpan(ctx, func(ctx context.Context) { diff --git a/coderd/httpmw/logger_internal_test.go b/coderd/httpmw/loggermw/logger_internal_test.go similarity index 98% rename from coderd/httpmw/logger_internal_test.go rename to coderd/httpmw/loggermw/logger_internal_test.go index d3035e50d98c9..b5b9b105628a0 100644 --- a/coderd/httpmw/logger_internal_test.go +++ b/coderd/httpmw/loggermw/logger_internal_test.go @@ -1,4 +1,4 @@ -package httpmw +package loggermw import ( "context" @@ -79,7 +79,7 @@ func TestLoggerMiddleware_SingleRequest(t *testing.T) { require.Equal(t, sink.entries[0].Message, "GET") - fieldsMap := make(map[string]interface{}) + fieldsMap := make(map[string]any) for _, field := range sink.entries[0].Fields { fieldsMap[field.Name] = field.Value } diff --git a/coderd/httpmw/loggermock/loggermock.go b/coderd/httpmw/loggermw/loggermock/loggermock.go similarity index 77% rename from coderd/httpmw/loggermock/loggermock.go rename to coderd/httpmw/loggermw/loggermock/loggermock.go index 47818ca11d9e6..008f862107ae6 100644 --- a/coderd/httpmw/loggermock/loggermock.go +++ b/coderd/httpmw/loggermw/loggermock/loggermock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/coder/coder/v2/coderd/httpmw (interfaces: RequestLogger) +// Source: github.com/coder/coder/v2/coderd/httpmw/loggermw (interfaces: RequestLogger) // // Generated by this command: // @@ -14,6 +14,7 @@ import ( reflect "reflect" slog "cdr.dev/slog" + rbac "github.com/coder/coder/v2/coderd/rbac" gomock "go.uber.org/mock/gomock" ) @@ -41,6 +42,18 @@ func (m *MockRequestLogger) EXPECT() *MockRequestLoggerMockRecorder { return m.recorder } +// WithAuthContext mocks base method. +func (m *MockRequestLogger) WithAuthContext(actor rbac.Subject) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "WithAuthContext", actor) +} + +// WithAuthContext indicates an expected call of WithAuthContext. +func (mr *MockRequestLoggerMockRecorder) WithAuthContext(actor any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithAuthContext", reflect.TypeOf((*MockRequestLogger)(nil).WithAuthContext), actor) +} + // WithFields mocks base method. func (m *MockRequestLogger) WithFields(fields ...slog.Field) { m.ctrl.T.Helper() diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index ea20c60de3cce..bc357bf2e35f2 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/pubsub" markdown "github.com/coder/coder/v2/coderd/render" @@ -220,7 +221,7 @@ func (api *API) watchInboxNotifications(rw http.ResponseWriter, r *http.Request) defer encoder.Close(websocket.StatusNormalClosure) // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) for { select { diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 335643390796f..6d75227a14ccd 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -20,6 +20,7 @@ import ( "github.com/coder/coder/v2/coderd/database/pubsub" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" "github.com/coder/coder/v2/coderd/util/slice" @@ -555,7 +556,7 @@ func (f *logFollower) follow() { } // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(f.ctx).WriteLog(f.ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(f.ctx).WriteLog(f.ctx, http.StatusAccepted) // no need to wait if the job is done if f.complete { diff --git a/coderd/provisionerjobs_internal_test.go b/coderd/provisionerjobs_internal_test.go index c2c0a60c75ba0..f3bc2eb1dea99 100644 --- a/coderd/provisionerjobs_internal_test.go +++ b/coderd/provisionerjobs_internal_test.go @@ -19,8 +19,8 @@ import ( "github.com/coder/coder/v2/coderd/database/dbmock" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/database/pubsub" - "github.com/coder/coder/v2/coderd/httpmw" - "github.com/coder/coder/v2/coderd/httpmw/loggermock" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw/loggermock" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/provisionersdk" "github.com/coder/coder/v2/testutil" @@ -309,7 +309,7 @@ func Test_logFollower_EndOfLogs(t *testing.T) { mockLogger := loggermock.NewMockRequestLogger(ctrl) mockLogger.EXPECT().WriteLog(gomock.Any(), http.StatusAccepted).Times(1) - ctx = httpmw.WithRequestLogger(ctx, mockLogger) + ctx = loggermw.WithRequestLogger(ctx, mockLogger) // we need an HTTP server to get a websocket srv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 3239ea3c42dc5..618b7e425e87a 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -58,6 +58,23 @@ func hashAuthorizeCall(actor Subject, action policy.Action, object Object) [32]b return hashOut } +// SubjectType represents the type of subject in the RBAC system. +type SubjectType string + +const ( + SubjectTypeUser SubjectType = "user" + SubjectTypeProvisionerd SubjectType = "provisionerd" + SubjectTypeAutostart SubjectType = "autostart" + SubjectTypeHangDetector SubjectType = "hang_detector" + SubjectTypeResourceMonitor SubjectType = "resource_monitor" + SubjectTypeCryptoKeyRotator SubjectType = "crypto_key_rotator" + SubjectTypeCryptoKeyReader SubjectType = "crypto_key_reader" + SubjectTypePrebuildsOrchestrator SubjectType = "prebuilds_orchestrator" + SubjectTypeSystemReadProvisionerDaemons SubjectType = "system_read_provisioner_daemons" + SubjectTypeSystemRestricted SubjectType = "system_restricted" + SubjectTypeNotifier SubjectType = "notifier" +) + // Subject is a struct that contains all the elements of a subject in an rbac // authorize. type Subject struct { @@ -67,6 +84,10 @@ type Subject struct { // external workspace proxy or other service type actor. FriendlyName string + // Email is entirely optional and is used for logging and debugging + // It is not used in any functional way. + Email string + ID string Roles ExpandableRoles Groups []string @@ -74,6 +95,10 @@ type Subject struct { // cachedASTValue is the cached ast value for this subject. cachedASTValue ast.Value + + // Type indicates what kind of subject this is (user, system, provisioner, etc.) + // It is not used in any functional way, only for logging. + Type SubjectType } // RegoValueOk is only used for unit testing. There is no easy way @@ -99,6 +124,9 @@ func (s Subject) WithCachedASTValue() Subject { } func (s Subject) Equal(b Subject) bool { + if s.Type != b.Type { + return false + } if s.ID != b.ID { return false } diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go index a4f8ed297b77a..fce4f7ef9a51c 100644 --- a/coderd/workspaceagents.go +++ b/coderd/workspaceagents.go @@ -33,6 +33,7 @@ import ( "github.com/coder/coder/v2/coderd/externalauth" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/jwtutils" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -556,7 +557,7 @@ func (api *API) workspaceAgentLogs(rw http.ResponseWriter, r *http.Request) { defer t.Stop() // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) go func() { defer func() { @@ -934,7 +935,7 @@ func (api *API) derpMapUpdates(rw http.ResponseWriter, r *http.Request) { defer encoder.Close(websocket.StatusGoingAway) // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) go func(ctx context.Context) { // TODO(mafredri): Is this too frequent? Use separate ping disconnect timeout? @@ -1324,7 +1325,7 @@ func (api *API) watchWorkspaceAgentMetadata( defer sendTicker.Stop() // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) // Send initial metadata. sendMetadata() diff --git a/enterprise/coderd/provisionerdaemons.go b/enterprise/coderd/provisionerdaemons.go index 15e3c3901ade3..6ffa15851214d 100644 --- a/enterprise/coderd/provisionerdaemons.go +++ b/enterprise/coderd/provisionerdaemons.go @@ -24,6 +24,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" @@ -378,7 +379,7 @@ func (api *API) provisionerDaemonServe(rw http.ResponseWriter, r *http.Request) }) // Log the request immediately instead of after it completes. - httpmw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) + loggermw.RequestLoggerFromContext(ctx).WriteLog(ctx, http.StatusAccepted) err = server.Serve(ctx, session) srvCancel() diff --git a/enterprise/wsproxy/wsproxy.go b/enterprise/wsproxy/wsproxy.go index 5dbf8ab6ea24d..bce49417fcd35 100644 --- a/enterprise/wsproxy/wsproxy.go +++ b/enterprise/wsproxy/wsproxy.go @@ -32,6 +32,7 @@ import ( "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/codersdk" @@ -336,7 +337,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) { tracing.Middleware(s.TracerProvider), httpmw.AttachRequestID, httpmw.ExtractRealIP(s.Options.RealIPConfig), - httpmw.Logger(s.Logger), + loggermw.Logger(s.Logger), prometheusMW, corsMW, diff --git a/tailnet/test/integration/integration.go b/tailnet/test/integration/integration.go index 08c66b515cd53..1190a3aa98b0d 100644 --- a/tailnet/test/integration/integration.go +++ b/tailnet/test/integration/integration.go @@ -33,7 +33,7 @@ import ( "cdr.dev/slog" "github.com/coder/coder/v2/coderd/httpapi" - "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/coderd/tracing" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/tailnet" @@ -200,7 +200,7 @@ func (o SimpleServerOptions) Router(t *testing.T, logger slog.Logger) *chi.Mux { }) }, tracing.StatusWriterMiddleware, - httpmw.Logger(logger), + loggermw.Logger(logger), ) r.Route("/derp", func(r chi.Router) { From 39f124c8f973c315f9b55773f40e04b78108e849 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 10 Apr 2025 14:30:10 +0000 Subject: [PATCH 02/13] removed Type from compare --- coderd/rbac/authz.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 618b7e425e87a..84a5e8ada6331 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -124,9 +124,6 @@ func (s Subject) WithCachedASTValue() Subject { } func (s Subject) Equal(b Subject) bool { - if s.Type != b.Type { - return false - } if s.ID != b.ID { return false } From a8b8b96f86e8293e5ab2f99cdf05ef4a2088128f Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:02:27 +0000 Subject: [PATCH 03/13] protected user map on request with mutex --- coderd/httpmw/loggermw/logger.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/coderd/httpmw/loggermw/logger.go b/coderd/httpmw/loggermw/logger.go index bccd55dadf501..23ce31edabc7d 100644 --- a/coderd/httpmw/loggermw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "sync" "time" "cdr.dev/slog" @@ -71,6 +72,7 @@ type SlogRequestLogger struct { written bool message string start time.Time + mu sync.RWMutex actors map[rbac.SubjectType]rbac.Subject } @@ -91,10 +93,15 @@ func (c *SlogRequestLogger) WithFields(fields ...slog.Field) { } func (c *SlogRequestLogger) WithAuthContext(actor rbac.Subject) { + c.mu.Lock() + defer c.mu.Unlock() c.actors[actor.Type] = actor } func (c *SlogRequestLogger) addAuthContextFields() { + c.mu.RLock() + defer c.mu.RUnlock() + usr, ok := c.actors[rbac.SubjectTypeUser] if ok { c.log = c.log.With( From 876a0d12bc778a6f649105ec9763e64979b38f36 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 10 Apr 2025 23:12:29 +0000 Subject: [PATCH 04/13] fixing missing closing quote --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3efef044b86cf..4ada1cd6d488c 100644 --- a/Makefile +++ b/Makefile @@ -631,7 +631,8 @@ gen/mark-fresh: coderd/database/pubsub/psmock/psmock.go \ agent/agentcontainers/acmock/acmock.go \ agent/agentcontainers/dcspec/dcspec_gen.go \ - coderd/httpmw/loggermw/loggermock/loggermock.go + coderd/httpmw/loggermw/loggermock/loggermock.go \ + " for file in $$files; do echo "$$file" From d1ee48e94efb92e89d69736b71b868c806ddab4c Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:47:28 +0000 Subject: [PATCH 05/13] added logging for workspace related parameters and generic chi context logger + tests --- coderd/httpmw/loggermw/logger.go | 17 +++++++ .../httpmw/loggermw/logger_internal_test.go | 49 +++++++++++++++++++ coderd/httpmw/workspaceagentparam.go | 11 +++++ coderd/httpmw/workspaceparam.go | 15 ++++++ 4 files changed, 92 insertions(+) diff --git a/coderd/httpmw/loggermw/logger.go b/coderd/httpmw/loggermw/logger.go index 23ce31edabc7d..f72adb0c0a633 100644 --- a/coderd/httpmw/loggermw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -7,6 +7,8 @@ import ( "sync" "time" + "github.com/go-chi/chi/v5" + "cdr.dev/slog" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/rbac" @@ -136,6 +138,21 @@ func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) { slog.F("latency_ms", float64(end.Sub(c.start)/time.Millisecond)), ) + if chiCtx := chi.RouteContext(ctx); chiCtx != nil { + urlParams := chiCtx.URLParams + routeParamsFields := make([]slog.Field, 0, len(urlParams.Keys)) + + for k, v := range urlParams.Keys { + if urlParams.Values[k] != "" { + routeParamsFields = append(routeParamsFields, slog.F(v, urlParams.Values[k])) + } + } + + if len(routeParamsFields) > 0 { + logger = logger.With(routeParamsFields...) + } + } + // We already capture most of this information in the span (minus // the response body which we don't want to capture anyways). tracing.RunWithoutSpan(ctx, func(ctx context.Context) { diff --git a/coderd/httpmw/loggermw/logger_internal_test.go b/coderd/httpmw/loggermw/logger_internal_test.go index b5b9b105628a0..36c30617b7fe0 100644 --- a/coderd/httpmw/loggermw/logger_internal_test.go +++ b/coderd/httpmw/loggermw/logger_internal_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -156,6 +157,54 @@ func TestLoggerMiddleware_WebSocket(t *testing.T) { require.Len(t, sink.entries, 1, "log was written twice") } +func TestRequestLogger_HTTPRouteParams(t *testing.T) { + t.Parallel() + + sink := &fakeSink{} + logger := slog.Make(sink) + logger = logger.Leveled(slog.LevelDebug) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort) + defer cancel() + + chiCtx := chi.NewRouteContext() + chiCtx.URLParams.Add("workspace", "test-workspace") + chiCtx.URLParams.Add("agent", "test-agent") + + ctx = context.WithValue(ctx, chi.RouteCtxKey, chiCtx) + + // Create a test handler to simulate an HTTP request + testHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte("OK")) + }) + + // Wrap the test handler with the Logger middleware + loggerMiddleware := Logger(logger) + wrappedHandler := loggerMiddleware(testHandler) + + // Create a test HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/test-path/}", nil) + require.NoError(t, err, "failed to create request") + + sw := &tracing.StatusWriter{ResponseWriter: httptest.NewRecorder()} + + // Serve the request + wrappedHandler.ServeHTTP(sw, req) + + fieldsMap := make(map[string]any) + for _, field := range sink.entries[0].Fields { + fieldsMap[field.Name] = field.Value + } + + // Check that the log contains the expected fields + requiredFields := []string{"workspace", "agent"} + for _, field := range requiredFields { + _, exists := fieldsMap[field] + require.True(t, exists, "field %q is missing in log fields", field) + } +} + type fakeSink struct { entries []slog.SinkEntry newEntries chan slog.SinkEntry diff --git a/coderd/httpmw/workspaceagentparam.go b/coderd/httpmw/workspaceagentparam.go index a47ce3c377ae0..434e057c0eccc 100644 --- a/coderd/httpmw/workspaceagentparam.go +++ b/coderd/httpmw/workspaceagentparam.go @@ -6,8 +6,11 @@ import ( "github.com/go-chi/chi/v5" + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/codersdk" ) @@ -81,6 +84,14 @@ func ExtractWorkspaceAgentParam(db database.Store) func(http.Handler) http.Handl ctx = context.WithValue(ctx, workspaceAgentParamContextKey{}, agent) chi.RouteContext(ctx).URLParams.Add("workspace", build.WorkspaceID.String()) + + if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil { + rlogger.WithFields( + slog.F("workspace_name", resource.Name), + slog.F("agent_name", agent.Name), + ) + } + next.ServeHTTP(rw, r.WithContext(ctx)) }) } diff --git a/coderd/httpmw/workspaceparam.go b/coderd/httpmw/workspaceparam.go index 21e8dcfd62863..0c4e4f77354fc 100644 --- a/coderd/httpmw/workspaceparam.go +++ b/coderd/httpmw/workspaceparam.go @@ -9,8 +9,11 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" + "cdr.dev/slog" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/coderd/httpmw/loggermw" "github.com/coder/coder/v2/codersdk" ) @@ -48,6 +51,11 @@ func ExtractWorkspaceParam(db database.Store) func(http.Handler) http.Handler { } ctx = context.WithValue(ctx, workspaceParamContextKey{}, workspace) + + if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil { + rlogger.WithFields(slog.F("workspace_name", workspace.Name)) + } + next.ServeHTTP(rw, r.WithContext(ctx)) }) } @@ -154,6 +162,13 @@ func ExtractWorkspaceAndAgentParam(db database.Store) func(http.Handler) http.Ha ctx = context.WithValue(ctx, workspaceParamContextKey{}, workspace) ctx = context.WithValue(ctx, workspaceAgentParamContextKey{}, agent) + + if rlogger := loggermw.RequestLoggerFromContext(ctx); rlogger != nil { + rlogger.WithFields( + slog.F("workspace_name", workspace.Name), + slog.F("agent_name", agent.Name), + ) + } next.ServeHTTP(rw, r.WithContext(ctx)) }) } From 8ef56461165eb68835896cde7b0aa13d783ea26f Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 11 Apr 2025 10:49:25 +0000 Subject: [PATCH 06/13] added comment for chi ctx extraction --- coderd/httpmw/loggermw/logger.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/httpmw/loggermw/logger.go b/coderd/httpmw/loggermw/logger.go index f72adb0c0a633..b97c25d956500 100644 --- a/coderd/httpmw/loggermw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -138,6 +138,7 @@ func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) { slog.F("latency_ms", float64(end.Sub(c.start)/time.Millisecond)), ) + // If the request is routed, add the route parameters to the log. if chiCtx := chi.RouteContext(ctx); chiCtx != nil { urlParams := chiCtx.URLParams routeParamsFields := make([]slog.Field, 0, len(urlParams.Keys)) From 6567965e3643511827dd0234129eac5642ee5f3f Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:00:23 +0000 Subject: [PATCH 07/13] added tests for logging handler --- .../httpmw/loggermw/logger_internal_test.go | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/coderd/httpmw/loggermw/logger_internal_test.go b/coderd/httpmw/loggermw/logger_internal_test.go index 36c30617b7fe0..6ba8349bb667f 100644 --- a/coderd/httpmw/loggermw/logger_internal_test.go +++ b/coderd/httpmw/loggermw/logger_internal_test.go @@ -205,6 +205,99 @@ func TestRequestLogger_HTTPRouteParams(t *testing.T) { } } +func TestRequestLogger_RouteParamsLogging(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + params map[string]string + expectedFields []string + }{ + { + name: "EmptyParams", + params: map[string]string{}, + expectedFields: []string{}, + }, + { + name: "SingleParam", + params: map[string]string{ + "workspace": "test-workspace", + }, + expectedFields: []string{"workspace"}, + }, + { + name: "MultipleParams", + params: map[string]string{ + "workspace": "test-workspace", + "agent": "test-agent", + "user": "test-user", + }, + expectedFields: []string{"workspace", "agent", "user"}, + }, + { + name: "EmptyValueParam", + params: map[string]string{ + "workspace": "test-workspace", + "agent": "", + }, + expectedFields: []string{"workspace"}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + sink := &fakeSink{} + logger := slog.Make(sink) + logger = logger.Leveled(slog.LevelDebug) + + // Create a route context with the test parameters + chiCtx := chi.NewRouteContext() + for key, value := range tt.params { + chiCtx.URLParams.Add(key, value) + } + + ctx := context.WithValue(context.Background(), chi.RouteCtxKey, chiCtx) + logCtx := NewRequestLogger(logger, "GET", time.Now()) + + // Write the log + logCtx.WriteLog(ctx, http.StatusOK) + + require.Len(t, sink.entries, 1, "expected exactly one log entry") + + // Convert fields to map for easier checking + fieldsMap := make(map[string]any) + for _, field := range sink.entries[0].Fields { + fieldsMap[field.Name] = field.Value + } + + // Verify expected fields are present + for _, field := range tt.expectedFields { + value, exists := fieldsMap[field] + require.True(t, exists, "field %q should be present in log", field) + require.Equal(t, tt.params[field], value, "field %q has incorrect value", field) + } + + // Verify no unexpected fields are present + for field := range fieldsMap { + if field == "took" || field == "status_code" || field == "latency_ms" { + continue // Skip standard fields + } + found := false + for _, expected := range tt.expectedFields { + if field == expected { + found = true + break + } + } + require.True(t, found, "unexpected field %q in log", field) + } + }) + } +} + type fakeSink struct { entries []slog.SinkEntry newEntries chan slog.SinkEntry From 4ecaf83f562367c11b3a8ac3ce3771a52c20a7b3 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 15 Apr 2025 06:53:33 +0000 Subject: [PATCH 08/13] added a 'params_' prefix --- coderd/httpmw/loggermw/logger.go | 2 +- .../httpmw/loggermw/logger_internal_test.go | 21 +++++++------------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/coderd/httpmw/loggermw/logger.go b/coderd/httpmw/loggermw/logger.go index b97c25d956500..cef8bf383ffbd 100644 --- a/coderd/httpmw/loggermw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -145,7 +145,7 @@ func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) { for k, v := range urlParams.Keys { if urlParams.Values[k] != "" { - routeParamsFields = append(routeParamsFields, slog.F(v, urlParams.Values[k])) + routeParamsFields = append(routeParamsFields, slog.F("params_"+v, urlParams.Values[k])) } } diff --git a/coderd/httpmw/loggermw/logger_internal_test.go b/coderd/httpmw/loggermw/logger_internal_test.go index 6ba8349bb667f..e88f8a69c178e 100644 --- a/coderd/httpmw/loggermw/logger_internal_test.go +++ b/coderd/httpmw/loggermw/logger_internal_test.go @@ -4,6 +4,8 @@ import ( "context" "net/http" "net/http/httptest" + "slices" + "strings" "sync" "testing" "time" @@ -200,7 +202,7 @@ func TestRequestLogger_HTTPRouteParams(t *testing.T) { // Check that the log contains the expected fields requiredFields := []string{"workspace", "agent"} for _, field := range requiredFields { - _, exists := fieldsMap[field] + _, exists := fieldsMap["params_"+field] require.True(t, exists, "field %q is missing in log fields", field) } } @@ -223,7 +225,7 @@ func TestRequestLogger_RouteParamsLogging(t *testing.T) { params: map[string]string{ "workspace": "test-workspace", }, - expectedFields: []string{"workspace"}, + expectedFields: []string{"params_workspace"}, }, { name: "MultipleParams", @@ -232,7 +234,7 @@ func TestRequestLogger_RouteParamsLogging(t *testing.T) { "agent": "test-agent", "user": "test-user", }, - expectedFields: []string{"workspace", "agent", "user"}, + expectedFields: []string{"params_workspace", "params_agent", "params_user"}, }, { name: "EmptyValueParam", @@ -240,7 +242,7 @@ func TestRequestLogger_RouteParamsLogging(t *testing.T) { "workspace": "test-workspace", "agent": "", }, - expectedFields: []string{"workspace"}, + expectedFields: []string{"params_workspace"}, }, } @@ -277,7 +279,7 @@ func TestRequestLogger_RouteParamsLogging(t *testing.T) { for _, field := range tt.expectedFields { value, exists := fieldsMap[field] require.True(t, exists, "field %q should be present in log", field) - require.Equal(t, tt.params[field], value, "field %q has incorrect value", field) + require.Equal(t, tt.params[strings.TrimPrefix(field, "params_")], value, "field %q has incorrect value", field) } // Verify no unexpected fields are present @@ -285,14 +287,7 @@ func TestRequestLogger_RouteParamsLogging(t *testing.T) { if field == "took" || field == "status_code" || field == "latency_ms" { continue // Skip standard fields } - found := false - for _, expected := range tt.expectedFields { - if field == expected { - found = true - break - } - } - require.True(t, found, "unexpected field %q in log", field) + require.True(t, slices.Contains(tt.expectedFields, field), "unexpected field %q in log", field) } }) } From c60cabc5589752611cdaa5a770fadc81c72e7fa0 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 15 Apr 2025 07:17:29 +0000 Subject: [PATCH 09/13] added ordering of subjecttypes on logging the actor --- coderd/httpmw/loggermw/logger.go | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/coderd/httpmw/loggermw/logger.go b/coderd/httpmw/loggermw/logger.go index cef8bf383ffbd..abfa908abb0d8 100644 --- a/coderd/httpmw/loggermw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -111,16 +111,35 @@ func (c *SlogRequestLogger) addAuthContextFields() { slog.F("requestor_name", usr.FriendlyName), slog.F("requestor_email", usr.Email), ) - } else if len(c.actors) > 0 { - for _, v := range c.actors { + } else { + // If there is no user, we log the requestor name for the first + // actor in a defined order. + for _, v := range actorLogOrder { + subj, ok := c.actors[v] + if !ok { + continue + } c.log = c.log.With( - slog.F("requestor_name", v.FriendlyName), + slog.F("requestor_name", subj.FriendlyName), ) break } } } +var actorLogOrder = []rbac.SubjectType{ + rbac.SubjectTypeAutostart, + rbac.SubjectTypeCryptoKeyReader, + rbac.SubjectTypeCryptoKeyRotator, + rbac.SubjectTypeHangDetector, + rbac.SubjectTypeNotifier, + rbac.SubjectTypePrebuildsOrchestrator, + rbac.SubjectTypeProvisionerd, + rbac.SubjectTypeResourceMonitor, + rbac.SubjectTypeSystemRestricted, + rbac.SubjectTypeSystemReadProvisionerDaemons, +} + func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) { if c.written { return From 9ddfd2db2c2ea017086a27466c522e9868eb5534 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 15 Apr 2025 09:57:57 +0200 Subject: [PATCH 10/13] changed ordering of actors in logs --- coderd/httpmw/loggermw/logger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/httpmw/loggermw/logger.go b/coderd/httpmw/loggermw/logger.go index abfa908abb0d8..e0f9ead8e29ae 100644 --- a/coderd/httpmw/loggermw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -136,8 +136,8 @@ var actorLogOrder = []rbac.SubjectType{ rbac.SubjectTypePrebuildsOrchestrator, rbac.SubjectTypeProvisionerd, rbac.SubjectTypeResourceMonitor, - rbac.SubjectTypeSystemRestricted, rbac.SubjectTypeSystemReadProvisionerDaemons, + rbac.SubjectTypeSystemRestricted, } func (c *SlogRequestLogger) WriteLog(ctx context.Context, status int) { From 9146744ea61acf2ce845bbc77e1c44d2b0d7cf8e Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 15 Apr 2025 09:58:39 +0200 Subject: [PATCH 11/13] updated comments --- coderd/httpmw/loggermw/logger.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/httpmw/loggermw/logger.go b/coderd/httpmw/loggermw/logger.go index e0f9ead8e29ae..eb0603b780946 100644 --- a/coderd/httpmw/loggermw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -74,6 +74,7 @@ type SlogRequestLogger struct { written bool message string start time.Time + // Protects actors map for concurrent writes. mu sync.RWMutex actors map[rbac.SubjectType]rbac.Subject } From bd5104c2433ba1c2e9b3f1056bf375d568175030 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 15 Apr 2025 08:00:35 +0000 Subject: [PATCH 12/13] changed field ordering --- coderd/rbac/authz.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 84a5e8ada6331..d2c6d5d0675be 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -88,6 +88,10 @@ type Subject struct { // It is not used in any functional way. Email string + // Type indicates what kind of subject this is (user, system, provisioner, etc.) + // It is not used in any functional way, only for logging. + Type SubjectType + ID string Roles ExpandableRoles Groups []string @@ -95,10 +99,6 @@ type Subject struct { // cachedASTValue is the cached ast value for this subject. cachedASTValue ast.Value - - // Type indicates what kind of subject this is (user, system, provisioner, etc.) - // It is not used in any functional way, only for logging. - Type SubjectType } // RegoValueOk is only used for unit testing. There is no easy way From 28dee4b64d43914e029b4bd88d0f6b0ce81ed5f9 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 15 Apr 2025 11:15:51 +0000 Subject: [PATCH 13/13] lint fixes --- coderd/httpmw/loggermw/logger.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/httpmw/loggermw/logger.go b/coderd/httpmw/loggermw/logger.go index eb0603b780946..9eeb07a5f10e5 100644 --- a/coderd/httpmw/loggermw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -75,8 +75,8 @@ type SlogRequestLogger struct { message string start time.Time // Protects actors map for concurrent writes. - mu sync.RWMutex - actors map[rbac.SubjectType]rbac.Subject + mu sync.RWMutex + actors map[rbac.SubjectType]rbac.Subject } var _ RequestLogger = &SlogRequestLogger{}
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: