Skip to content

Commit 357a09e

Browse files
authored
audit log creds [2/3]: convert audit log auth_method to an enum (#9655)
Built on #9654, which makes the source data for this column an enum. This PR converts the column to an enum. It's kind of alarming to do this migration, but the strings are hard-coded in the repo so there's no way there could be anything else in the DB. This column being an enum makes it work more nicely as a type field for the `credential_id` column added in #9656. ### Summary of migrations 1. **up1**: Create the `audit_log_auth_method` enum type with values `session_cookie`, `access_token`, `scim_token`, `spoof` 2. **up2**: Add a temporary column `auth_method_temp` of the new enum type 3. **up3**: Copy data from the old string column to the temp column, mapping `'token'` → `'access_token'` (the others map 1:1) 4. **up4**: Drop the `audit_log_complete` view (it depends on the column being dropped) 5. **up5**: Drop the old string `auth_method` column 6. **up6**: Add a new `auth_method` column with the enum type 7. **up7**: Copy data from temp column to the new enum column 8. **up8**: Drop the temp column 9. **up9**: Drop the view again (defensive, in case it exists) 10. **up10**: Recreate `audit_log_complete` view with the new column type The temp column thing is necessary because you can't change a column's type in place. The view must be dropped and recreated because it references the column.
1 parent 4e43c37 commit 357a09e

21 files changed

Lines changed: 29823 additions & 26 deletions

File tree

nexus/db-model/src/audit_log.rs

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub struct AuditLogEntryInitParams {
3838
pub source_ip: IpAddr,
3939
pub user_agent: Option<String>,
4040
pub actor: AuditLogActor,
41-
pub auth_method: Option<String>,
41+
pub auth_method: Option<AuditLogAuthMethod>,
4242
}
4343

4444
impl_enum_type!(
@@ -86,6 +86,54 @@ impl_enum_type!(
8686
Timeout => b"timeout"
8787
);
8888

89+
impl_enum_type!(
90+
AuditLogAuthMethodEnum:
91+
92+
#[derive(
93+
Clone,
94+
Copy,
95+
Debug,
96+
AsExpression,
97+
FromSqlRow,
98+
Serialize,
99+
Deserialize,
100+
PartialEq,
101+
Eq,
102+
)]
103+
pub enum AuditLogAuthMethod;
104+
105+
// Enum values
106+
SessionCookie => b"session_cookie"
107+
AccessToken => b"access_token"
108+
ScimToken => b"scim_token"
109+
Spoof => b"spoof"
110+
);
111+
112+
impl From<AuditLogAuthMethod> for views::AuthMethod {
113+
fn from(m: AuditLogAuthMethod) -> Self {
114+
match m {
115+
AuditLogAuthMethod::SessionCookie => {
116+
views::AuthMethod::SessionCookie
117+
}
118+
AuditLogAuthMethod::AccessToken => views::AuthMethod::AccessToken,
119+
AuditLogAuthMethod::ScimToken => views::AuthMethod::ScimToken,
120+
AuditLogAuthMethod::Spoof => views::AuthMethod::Spoof,
121+
}
122+
}
123+
}
124+
125+
impl From<&nexus_types::authn::SchemeName> for AuditLogAuthMethod {
126+
fn from(s: &nexus_types::authn::SchemeName) -> Self {
127+
use nexus_types::authn::SchemeName;
128+
match s {
129+
SchemeName::SessionCookie => AuditLogAuthMethod::SessionCookie,
130+
SchemeName::AccessToken => AuditLogAuthMethod::AccessToken,
131+
SchemeName::ScimToken => AuditLogAuthMethod::ScimToken,
132+
SchemeName::Spoof => AuditLogAuthMethod::Spoof,
133+
}
134+
}
135+
}
136+
89137
#[derive(Queryable, Insertable, Selectable, Clone, Debug)]
90138
#[diesel(table_name = audit_log)]
91139
pub struct AuditLogEntryInit {
@@ -115,7 +163,7 @@ pub struct AuditLogEntryInit {
115163

116164
/// API token or session cookie. Optional because it will not be defined
117165
/// on unauthenticated requests like login attempts.
118-
pub auth_method: Option<String>,
166+
pub auth_method: Option<AuditLogAuthMethod>,
119167
}
120168

121169
impl From<AuditLogEntryInitParams> for AuditLogEntryInit {
@@ -182,20 +230,20 @@ pub struct AuditLogEntry {
182230
/// Actor kind indicating builtin user, silo user, or unauthenticated
183231
pub actor_kind: AuditLogActorKind,
184232

185-
/// The name of the authn scheme used. None if unauthenticated.
186-
pub auth_method: Option<String>,
187-
188233
// Fields that are not present on init
189234
/// Time log entry was completed with info about result of operation
190235
pub time_completed: DateTime<Utc>,
191-
/// Result kind indicating success, error, or timeout
192-
pub result_kind: AuditLogResultKind,
193236
/// Optional because not present for timeout result
194237
pub http_status_code: Option<SqlU16>,
195238
/// Optional even if result is an error
196239
pub error_code: Option<String>,
197240
/// Always present if result is an error
198241
pub error_message: Option<String>,
242+
/// Result kind indicating success, error, or timeout
243+
pub result_kind: AuditLogResultKind,
244+
245+
/// The authn scheme used. None if unauthenticated.
246+
pub auth_method: Option<AuditLogAuthMethod>,
199247
}
200248

201249
/// Struct that we can use as a kind of constructor arg for our actual audit
@@ -320,7 +368,7 @@ impl TryFrom<AuditLogEntry> for views::AuditLogEntry {
320368
views::AuditLogEntryActor::Unauthenticated
321369
}
322370
},
323-
auth_method: entry.auth_method,
371+
auth_method: entry.auth_method.map(Into::into),
324372
time_completed: entry.time_completed,
325373
result: match entry.result_kind {
326374
AuditLogResultKind::Success => {

nexus/db-model/src/schema_versions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
1616
///
1717
/// This must be updated when you change the database schema. Refer to
1818
/// schema/crdb/README.adoc in the root of this repository for details.
19-
pub const SCHEMA_VERSION: Version = Version::new(220, 0, 0);
19+
pub const SCHEMA_VERSION: Version = Version::new(221, 0, 0);
2020

2121
/// List of all past database schema versions, in *reverse* order
2222
///
@@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
2828
// | leaving the first copy as an example for the next person.
2929
// v
3030
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
31+
KnownVersion::new(221, "audit-log-auth-method-enum"),
3132
KnownVersion::new(220, "multicast-implicit-lifecycle"),
3233
KnownVersion::new(219, "blueprint-sled-last-used-ip"),
3334
KnownVersion::new(218, "measurements"),

nexus/db-schema/src/enums.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ define_enums! {
2424
AffinityPolicyEnum => "affinity_policy",
2525
AlertClassEnum => "alert_class",
2626
AuditLogActorKindEnum => "audit_log_actor_kind",
27+
AuditLogAuthMethodEnum => "audit_log_auth_method",
2728
AuditLogResultKindEnum => "audit_log_result_kind",
2829
AlertDeliveryTriggerEnum => "alert_delivery_trigger",
2930
AlertDeliveryStateEnum => "alert_delivery_state",

nexus/db-schema/src/schema.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2885,12 +2885,12 @@ table! {
28852885
actor_id -> Nullable<Uuid>,
28862886
actor_silo_id -> Nullable<Uuid>,
28872887
actor_kind -> crate::enums::AuditLogActorKindEnum,
2888-
auth_method -> Nullable<Text>,
28892888
time_completed -> Nullable<Timestamptz>,
28902889
http_status_code -> Nullable<Int4>, // SqlU16
28912890
error_code -> Nullable<Text>,
28922891
error_message -> Nullable<Text>,
28932892
result_kind -> Nullable<crate::enums::AuditLogResultKindEnum>,
2893+
auth_method -> Nullable<crate::enums::AuditLogAuthMethodEnum>,
28942894
}
28952895
}
28962896

@@ -2906,12 +2906,12 @@ table! {
29062906
actor_id -> Nullable<Uuid>,
29072907
actor_silo_id -> Nullable<Uuid>,
29082908
actor_kind -> crate::enums::AuditLogActorKindEnum,
2909-
auth_method -> Nullable<Text>,
29102909
time_completed -> Timestamptz,
29112910
http_status_code -> Nullable<Int4>, // SqlU16
29122911
error_code -> Nullable<Text>,
29132912
error_message -> Nullable<Text>,
29142913
result_kind -> crate::enums::AuditLogResultKindEnum,
2914+
auth_method -> Nullable<crate::enums::AuditLogAuthMethodEnum>,
29152915
}
29162916
}
29172917

nexus/external-api/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ api_versions!([
7070
// | date-based version should be at the top of the list.
7171
// v
7272
// (next_yyyymmddnn, IDENT),
73+
(2026011500, AUDIT_LOG_AUTH_METHOD_ENUM),
7374
(2026011300, DOC_LINT_SUMMARY_TRAILING_PERIOD),
7475
(2026011100, MULTICAST_JOIN_LEAVE_DOCS),
7576
(2026010800, MULTICAST_IMPLICIT_LIFECYCLE_UPDATES),

nexus/src/app/audit_log.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ impl super::Nexus {
126126
AuditLogActor::UserBuiltin { .. }
127127
| AuditLogActor::SiloUser { .. }
128128
| AuditLogActor::Scim { .. } => {
129-
opctx.authn.scheme_used().map(|s| s.to_string())
129+
opctx.authn.scheme_used().map(Into::into)
130130
}
131131
// if we tried to pull it off the opctx this would be None anyway,
132132
// but it's better to be explicit

nexus/tests/integration_tests/audit_log.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ async fn test_audit_log_list(ctx: &ControlPlaneTestContext) {
101101
assert_eq!(e1.operation_id, "project_create");
102102
assert_eq!(e1.source_ip.to_string(), "127.0.0.1");
103103
assert_eq!(e1.user_agent, None); // no user agent passed by default
104-
assert_eq!(e1.auth_method, Some("spoof".to_string()));
104+
assert_eq!(e1.auth_method, Some(views::AuthMethod::Spoof));
105105
assert!(e1.time_started >= t1 && e1.time_started <= t2);
106106
assert!(e1.time_completed > e1.time_started);
107107
assert_eq!(
@@ -145,7 +145,7 @@ async fn test_audit_log_list(ctx: &ControlPlaneTestContext) {
145145
assert_eq!(e3.operation_id, "project_create");
146146
assert_eq!(e3.source_ip.to_string(), "127.0.0.1");
147147
assert_eq!(e3.user_agent.clone().unwrap(), "A".repeat(256));
148-
assert_eq!(e3.auth_method, Some("session_cookie".to_string()));
148+
assert_eq!(e3.auth_method, Some(views::AuthMethod::SessionCookie));
149149
assert!(e3.time_started >= t3 && e3.time_started <= t4);
150150
assert!(e3.time_completed > e3.time_started);
151151
assert_eq!(
@@ -396,6 +396,6 @@ fn verify_entry(
396396
}
397397
);
398398
assert_eq!(entry.source_ip.to_string(), "127.0.0.1");
399-
assert_eq!(entry.auth_method, Some("spoof".to_string()));
399+
assert_eq!(entry.auth_method, Some(views::AuthMethod::Spoof));
400400
assert!(entry.time_completed > entry.time_started);
401401
}

nexus/types/src/external_api/views.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,6 +1799,23 @@ pub enum AuditLogEntryActor {
17991799
Unauthenticated,
18001800
}
18011801

1802+
/// Authentication method used for a request
1803+
#[derive(
1804+
Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq, Eq,
1805+
)]
1806+
#[serde(rename_all = "snake_case")]
1807+
pub enum AuthMethod {
1808+
/// Console session cookie
1809+
SessionCookie,
1810+
/// Device access token (OAuth 2.0 device authorization flow)
1811+
AccessToken,
1812+
/// SCIM client bearer token
1813+
ScimToken,
1814+
/// Spoof authentication (test only)
1815+
#[schemars(skip)]
1816+
Spoof,
1817+
}
1818+
18021819
/// Result of an audit log entry
18031820
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
18041821
#[serde(tag = "kind", rename_all = "snake_case")]
@@ -1851,10 +1868,9 @@ pub struct AuditLogEntry {
18511868

18521869
pub actor: AuditLogEntryActor,
18531870

1854-
/// How the user authenticated the request. Possible values are
1855-
/// "session_cookie" and "access_token". Optional because it will not be
1856-
/// defined on unauthenticated requests like login attempts.
1857-
pub auth_method: Option<String>,
1871+
/// How the user authenticated the request (access token, session, or SCIM
1872+
/// token). Null for unauthenticated requests like login attempts.
1873+
pub auth_method: Option<AuthMethod>,
18581874

18591875
// Fields that are optional because they get filled in after the action completes
18601876
/// Time operation completed

0 commit comments

Comments
 (0)