Skip to content

Commit e137e92

Browse files
committed
feat: add audit logs for dormancy events
An audit log will now be generated when Coder automatically modifies user statuses to dormant, or when a user logs in and are automatically converted to active.
1 parent 591cefa commit e137e92

File tree

8 files changed

+67
-9
lines changed

8 files changed

+67
-9
lines changed

coderd/database/queries.sql.go

Lines changed: 8 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ SET
286286
WHERE
287287
last_seen_at < @last_seen_after :: timestamp
288288
AND status = 'active'::user_status
289-
RETURNING id, email, last_seen_at;
289+
RETURNING id, email, username, last_seen_at;
290290

291291
-- AllUserIDs returns all UserIDs regardless of user status or deletion.
292292
-- name: AllUserIDs :many

coderd/provisionerdserver/provisionerdserver.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,7 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto.
10631063
wriBytes, err := json.Marshal(buildResourceInfo)
10641064
if err != nil {
10651065
s.Logger.Error(ctx, "marshal workspace resource info for failed job", slog.Error(err))
1066+
wriBytes = []byte("{}")
10661067
}
10671068

10681069
bag := audit.BaggageFromContext(ctx)

coderd/userauth.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,7 @@ func (api *API) loginRequest(ctx context.Context, rw http.ResponseWriter, req co
566566
}
567567

568568
if user.Status == database.UserStatusDormant {
569+
oldUser := user
569570
//nolint:gocritic // System needs to update status of the user account (dormant -> active).
570571
user, err = api.Database.UpdateUserStatus(dbauthz.AsSystemRestricted(ctx), database.UpdateUserStatusParams{
571572
ID: user.ID,
@@ -579,6 +580,28 @@ func (api *API) loginRequest(ctx context.Context, rw http.ResponseWriter, req co
579580
})
580581
return user, rbac.Subject{}, false
581582
}
583+
584+
af := map[string]string{
585+
"automatic_actor": "coder",
586+
"automatic_subsystem": "dormancy",
587+
}
588+
589+
wriBytes, err := json.Marshal(af)
590+
if err != nil {
591+
api.Logger.Error(ctx, "marshal additional fields for dormancy audit", slog.Error(err))
592+
wriBytes = []byte("{}")
593+
}
594+
595+
audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.User]{
596+
Audit: *api.Auditor.Load(),
597+
Log: api.Logger,
598+
UserID: user.ID,
599+
Action: database.AuditActionWrite,
600+
Old: oldUser,
601+
New: user,
602+
Status: http.StatusOK,
603+
AdditionalFields: wriBytes,
604+
})
582605
}
583606

584607
subject, userStatus, err := httpmw.UserRBACSubject(ctx, api.Database, user.ID, rbac.ScopeAll)

enterprise/cli/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func (r *RootCmd) Server(_ func()) *serpent.Command {
9595
DefaultQuietHoursSchedule: options.DeploymentValues.UserQuietHoursSchedule.DefaultSchedule.Value(),
9696
ProvisionerDaemonPSK: options.DeploymentValues.Provisioner.DaemonPSK.Value(),
9797

98-
CheckInactiveUsersCancelFunc: dormancy.CheckInactiveUsers(ctx, options.Logger, options.Database),
98+
CheckInactiveUsersCancelFunc: dormancy.CheckInactiveUsers(ctx, options.Logger, options.Database, options.Auditor),
9999
}
100100

101101
if encKeys := options.DeploymentValues.ExternalTokenEncryptionKeys.Value(); len(encKeys) != 0 {

enterprise/coderd/dormancy/dormantusersjob.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package dormancy
33
import (
44
"context"
55
"database/sql"
6+
"encoding/json"
7+
"net/http"
68
"time"
79

810
"golang.org/x/xerrors"
911

1012
"cdr.dev/slog"
1113

14+
"github.com/coder/coder/v2/coderd/audit"
1215
"github.com/coder/coder/v2/coderd/database"
1316
"github.com/coder/coder/v2/coderd/database/dbtime"
1417
)
@@ -22,13 +25,13 @@ const (
2225

2326
// CheckInactiveUsers function updates status of inactive users from active to dormant
2427
// using default parameters.
25-
func CheckInactiveUsers(ctx context.Context, logger slog.Logger, db database.Store) func() {
26-
return CheckInactiveUsersWithOptions(ctx, logger, db, jobInterval, accountDormancyPeriod)
28+
func CheckInactiveUsers(ctx context.Context, logger slog.Logger, db database.Store, auditor audit.Auditor) func() {
29+
return CheckInactiveUsersWithOptions(ctx, logger, db, auditor, jobInterval, accountDormancyPeriod)
2730
}
2831

2932
// CheckInactiveUsersWithOptions function updates status of inactive users from active to dormant
3033
// using provided parameters.
31-
func CheckInactiveUsersWithOptions(ctx context.Context, logger slog.Logger, db database.Store, checkInterval, dormancyPeriod time.Duration) func() {
34+
func CheckInactiveUsersWithOptions(ctx context.Context, logger slog.Logger, db database.Store, auditor audit.Auditor, checkInterval, dormancyPeriod time.Duration) func() {
3235
logger = logger.Named("dormancy")
3336

3437
ctx, cancelFunc := context.WithCancel(ctx)
@@ -57,8 +60,29 @@ func CheckInactiveUsersWithOptions(ctx context.Context, logger slog.Logger, db d
5760
continue
5861
}
5962

63+
af := map[string]string{
64+
"automatic_actor": "coder",
65+
"automatic_subsystem": "dormancy",
66+
}
67+
68+
wriBytes, err := json.Marshal(af)
69+
if err != nil {
70+
logger.Error(ctx, "marshal additional fields", slog.Error(err))
71+
wriBytes = []byte("{}")
72+
}
73+
6074
for _, u := range updatedUsers {
6175
logger.Info(ctx, "account has been marked as dormant", slog.F("email", u.Email), slog.F("last_seen_at", u.LastSeenAt))
76+
audit.BackgroundAudit(ctx, &audit.BackgroundAuditParams[database.User]{
77+
Audit: auditor,
78+
Log: logger,
79+
UserID: u.ID,
80+
Action: database.AuditActionWrite,
81+
Old: database.User{ID: u.ID, Username: u.Username, Status: database.UserStatusActive},
82+
New: database.User{ID: u.ID, Username: u.Username, Status: database.UserStatusDormant},
83+
Status: http.StatusOK,
84+
AdditionalFields: wriBytes,
85+
})
6286
}
6387
logger.Debug(ctx, "checking user accounts is done", slog.F("num_dormant_accounts", len(updatedUsers)), slog.F("execution_time", time.Since(startTime)))
6488
}

enterprise/coderd/dormancy/dormantusersjob_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"cdr.dev/slog/sloggers/slogtest"
1212

13+
"github.com/coder/coder/v2/coderd/audit"
1314
"github.com/coder/coder/v2/coderd/database"
1415
"github.com/coder/coder/v2/coderd/database/dbmem"
1516
"github.com/coder/coder/v2/enterprise/coderd/dormancy"
@@ -42,8 +43,9 @@ func TestCheckInactiveUsers(t *testing.T) {
4243
suspendedUser2 := setupUser(ctx, t, db, "suspended-user-2@coder.com", database.UserStatusSuspended, time.Now().Add(-dormancyPeriod).Add(-time.Hour))
4344
suspendedUser3 := setupUser(ctx, t, db, "suspended-user-3@coder.com", database.UserStatusSuspended, time.Now().Add(-dormancyPeriod).Add(-6*time.Hour))
4445

46+
mAudit := audit.NewMock()
4547
// Run the periodic job
46-
closeFunc := dormancy.CheckInactiveUsersWithOptions(ctx, logger, db, interval, dormancyPeriod)
48+
closeFunc := dormancy.CheckInactiveUsersWithOptions(ctx, logger, db, mAudit, interval, dormancyPeriod)
4749
t.Cleanup(closeFunc)
4850

4951
var rows []database.GetUsersRow
@@ -66,6 +68,8 @@ func TestCheckInactiveUsers(t *testing.T) {
6668
return len(rows) == 9 && dormant == 3 && suspended == 3
6769
}, testutil.WaitShort, testutil.IntervalMedium)
6870

71+
require.Len(t, mAudit.AuditLogs(), 3)
72+
6973
allUsers := ignoreUpdatedAt(database.ConvertUserRows(rows))
7074

7175
// Verify user status

site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/AuditLogDescription.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const AuditLogDescription: FC<AuditLogDescriptionProps> = ({
2323
target = "";
2424
}
2525

26-
// This occurs when SCIM creates a user.
26+
// This occurs when SCIM creates a user, or dormancy changes a users status.
2727
if (
2828
auditLog.resource_type === "user" &&
2929
auditLog.additional_fields?.automatic_actor === "coder"

0 commit comments

Comments
 (0)