@@ -1050,3 +1050,198 @@ func TestDeleteOldAIBridgeRecords(t *testing.T) {
10501050 require .NoError (t , err )
10511051 require .Len (t , newToolUsages , 1 , "near threshold tool usages should not be deleted" )
10521052}
1053+
1054+ func TestDeleteOldAuditLogs (t * testing.T ) {
1055+ t .Parallel ()
1056+
1057+ now := time .Date (2025 , 1 , 15 , 7 , 30 , 0 , 0 , time .UTC )
1058+ retentionPeriod := 30 * 24 * time .Hour
1059+ afterThreshold := now .Add (- retentionPeriod ).Add (- 24 * time .Hour ) // 31 days ago (older than threshold)
1060+ beforeThreshold := now .Add (- 15 * 24 * time .Hour ) // 15 days ago (newer than threshold)
1061+
1062+ testCases := []struct {
1063+ name string
1064+ retentionConfig codersdk.RetentionConfig
1065+ oldLogTime time.Time
1066+ recentLogTime * time.Time // nil means no recent log created
1067+ expectOldDeleted bool
1068+ expectedLogsRemaining int
1069+ }{
1070+ {
1071+ name : "RetentionEnabled" ,
1072+ retentionConfig : codersdk.RetentionConfig {
1073+ AuditLogs : serpent .Duration (retentionPeriod ),
1074+ },
1075+ oldLogTime : afterThreshold ,
1076+ recentLogTime : & beforeThreshold ,
1077+ expectOldDeleted : true ,
1078+ expectedLogsRemaining : 1 , // only recent log remains
1079+ },
1080+ {
1081+ name : "RetentionDisabled" ,
1082+ retentionConfig : codersdk.RetentionConfig {
1083+ AuditLogs : serpent .Duration (0 ),
1084+ },
1085+ oldLogTime : now .Add (- 365 * 24 * time .Hour ), // 1 year ago
1086+ recentLogTime : nil ,
1087+ expectOldDeleted : false ,
1088+ expectedLogsRemaining : 1 , // old log is kept
1089+ },
1090+ }
1091+
1092+ for _ , tc := range testCases {
1093+ t .Run (tc .name , func (t * testing.T ) {
1094+ t .Parallel ()
1095+
1096+ ctx := testutil .Context (t , testutil .WaitShort )
1097+ clk := quartz .NewMock (t )
1098+ clk .Set (now ).MustWait (ctx )
1099+
1100+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1101+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1102+
1103+ // Setup test fixtures.
1104+ user := dbgen .User (t , db , database.User {})
1105+ org := dbgen .Organization (t , db , database.Organization {})
1106+
1107+ // Create old audit log.
1108+ oldLog := dbgen .AuditLog (t , db , database.AuditLog {
1109+ UserID : user .ID ,
1110+ OrganizationID : org .ID ,
1111+ Time : tc .oldLogTime ,
1112+ Action : database .AuditActionCreate ,
1113+ ResourceType : database .ResourceTypeWorkspace ,
1114+ })
1115+
1116+ // Create recent audit log if specified.
1117+ var recentLog database.AuditLog
1118+ if tc .recentLogTime != nil {
1119+ recentLog = dbgen .AuditLog (t , db , database.AuditLog {
1120+ UserID : user .ID ,
1121+ OrganizationID : org .ID ,
1122+ Time : * tc .recentLogTime ,
1123+ Action : database .AuditActionCreate ,
1124+ ResourceType : database .ResourceTypeWorkspace ,
1125+ })
1126+ }
1127+
1128+ // Run the purge.
1129+ done := awaitDoTick (ctx , t , clk )
1130+ closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1131+ Retention : tc .retentionConfig ,
1132+ }, clk )
1133+ defer closer .Close ()
1134+ testutil .TryReceive (ctx , t , done )
1135+
1136+ // Verify results.
1137+ logs , err := db .GetAuditLogsOffset (ctx , database.GetAuditLogsOffsetParams {
1138+ LimitOpt : 100 ,
1139+ })
1140+ require .NoError (t , err )
1141+ require .Len (t , logs , tc .expectedLogsRemaining , "unexpected number of logs remaining" )
1142+
1143+ logIDs := make ([]uuid.UUID , len (logs ))
1144+ for i , log := range logs {
1145+ logIDs [i ] = log .AuditLog .ID
1146+ }
1147+
1148+ if tc .expectOldDeleted {
1149+ require .NotContains (t , logIDs , oldLog .ID , "old audit log should be deleted" )
1150+ } else {
1151+ require .Contains (t , logIDs , oldLog .ID , "old audit log should NOT be deleted" )
1152+ }
1153+
1154+ if tc .recentLogTime != nil {
1155+ require .Contains (t , logIDs , recentLog .ID , "recent audit log should be kept" )
1156+ }
1157+ })
1158+ }
1159+
1160+ // ConnectionEventsNotDeleted is a special case that tests multiple audit
1161+ // action types, so it's kept as a separate subtest.
1162+ t .Run ("ConnectionEventsNotDeleted" , func (t * testing.T ) {
1163+ t .Parallel ()
1164+
1165+ ctx := testutil .Context (t , testutil .WaitShort )
1166+ clk := quartz .NewMock (t )
1167+ clk .Set (now ).MustWait (ctx )
1168+
1169+ db , _ := dbtestutil .NewDB (t , dbtestutil .WithDumpOnFailure ())
1170+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true })
1171+ user := dbgen .User (t , db , database.User {})
1172+ org := dbgen .Organization (t , db , database.Organization {})
1173+
1174+ // Create old connection events (should NOT be deleted by audit logs retention).
1175+ oldConnectLog := dbgen .AuditLog (t , db , database.AuditLog {
1176+ UserID : user .ID ,
1177+ OrganizationID : org .ID ,
1178+ Time : afterThreshold ,
1179+ Action : database .AuditActionConnect ,
1180+ ResourceType : database .ResourceTypeWorkspace ,
1181+ })
1182+
1183+ oldDisconnectLog := dbgen .AuditLog (t , db , database.AuditLog {
1184+ UserID : user .ID ,
1185+ OrganizationID : org .ID ,
1186+ Time : afterThreshold ,
1187+ Action : database .AuditActionDisconnect ,
1188+ ResourceType : database .ResourceTypeWorkspace ,
1189+ })
1190+
1191+ oldOpenLog := dbgen .AuditLog (t , db , database.AuditLog {
1192+ UserID : user .ID ,
1193+ OrganizationID : org .ID ,
1194+ Time : afterThreshold ,
1195+ Action : database .AuditActionOpen ,
1196+ ResourceType : database .ResourceTypeWorkspace ,
1197+ })
1198+
1199+ oldCloseLog := dbgen .AuditLog (t , db , database.AuditLog {
1200+ UserID : user .ID ,
1201+ OrganizationID : org .ID ,
1202+ Time : afterThreshold ,
1203+ Action : database .AuditActionClose ,
1204+ ResourceType : database .ResourceTypeWorkspace ,
1205+ })
1206+
1207+ // Create old non-connection audit log (should be deleted).
1208+ oldCreateLog := dbgen .AuditLog (t , db , database.AuditLog {
1209+ UserID : user .ID ,
1210+ OrganizationID : org .ID ,
1211+ Time : afterThreshold ,
1212+ Action : database .AuditActionCreate ,
1213+ ResourceType : database .ResourceTypeWorkspace ,
1214+ })
1215+
1216+ // Run the purge with audit logs retention enabled.
1217+ done := awaitDoTick (ctx , t , clk )
1218+ closer := dbpurge .New (ctx , logger , db , & codersdk.DeploymentValues {
1219+ Retention : codersdk.RetentionConfig {
1220+ AuditLogs : serpent .Duration (retentionPeriod ),
1221+ },
1222+ }, clk )
1223+ defer closer .Close ()
1224+ testutil .TryReceive (ctx , t , done )
1225+
1226+ // Verify results.
1227+ logs , err := db .GetAuditLogsOffset (ctx , database.GetAuditLogsOffsetParams {
1228+ LimitOpt : 100 ,
1229+ })
1230+ require .NoError (t , err )
1231+ require .Len (t , logs , 4 , "should have 4 connection event logs remaining" )
1232+
1233+ logIDs := make ([]uuid.UUID , len (logs ))
1234+ for i , log := range logs {
1235+ logIDs [i ] = log .AuditLog .ID
1236+ }
1237+
1238+ // Connection events should NOT be deleted by audit logs retention.
1239+ require .Contains (t , logIDs , oldConnectLog .ID , "old connect log should NOT be deleted by audit logs retention" )
1240+ require .Contains (t , logIDs , oldDisconnectLog .ID , "old disconnect log should NOT be deleted by audit logs retention" )
1241+ require .Contains (t , logIDs , oldOpenLog .ID , "old open log should NOT be deleted by audit logs retention" )
1242+ require .Contains (t , logIDs , oldCloseLog .ID , "old close log should NOT be deleted by audit logs retention" )
1243+
1244+ // Non-connection event should be deleted.
1245+ require .NotContains (t , logIDs , oldCreateLog .ID , "old create log should be deleted by audit logs retention" )
1246+ })
1247+ }
0 commit comments