Skip to content

Commit 8f5be54

Browse files
committed
test(security): add integration tests for OIDC providers and session lifecycle
oidc.rs: CREATE/DROP/ALTER OIDC PROVIDER DDL round-trips through the catalog, SHOW OIDC PROVIDERS, privilege enforcement, and apply_claim_mapping unit coverage for wildcard and ordered rules. session_lifecycle.rs: ALTER DATABASE SET IDLE_TIMEOUT cache update and catalog persistence; KILL SESSION authority matrix (superuser, cluster admin, database owner, unprivileged); idle-sweep deadline arithmetic exported via the public re-export path. security.rs: updated to pass KillReason::AdminKill to kill_sessions_for_user and assert the watch receiver is not Alive. catalog_integrity_verify.rs: PutOidcProvider / DeleteOidcProvider marked Exempt (cluster-level identity config; no per-object owner row). Mirror and kind_labels tests: propagate idle_session_timeout_secs = 0 into DatabaseDescriptor literals to satisfy the new required field.
1 parent 3588dae commit 8f5be54

10 files changed

Lines changed: 312 additions & 3 deletions

nodedb/src/control/catalog_entry/tests/kind_labels.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
use crate::control::catalog_entry::entry::CatalogEntry;
66
use crate::control::security::catalog::StoredCollection;
7+
use crate::control::security::catalog::oidc_providers::StoredOidcProvider;
78
use crate::control::security::catalog::sequence_types::StoredSequence;
89

910
#[test]
@@ -32,4 +33,23 @@ fn kind_label_is_stable() {
3233
.kind(),
3334
"delete_sequence"
3435
);
36+
let provider = StoredOidcProvider {
37+
provider_name: "test".into(),
38+
issuer: "https://example.com".into(),
39+
jwks_uri: "https://example.com/.well-known/jwks.json".into(),
40+
audience: None,
41+
claim_mapping: vec![],
42+
created_at_lsn: 0,
43+
};
44+
assert_eq!(
45+
CatalogEntry::PutOidcProvider(Box::new(provider)).kind(),
46+
"put_oidc_provider"
47+
);
48+
assert_eq!(
49+
CatalogEntry::DeleteOidcProvider {
50+
name: "test".into()
51+
}
52+
.kind(),
53+
"delete_oidc_provider"
54+
);
3555
}

nodedb/tests/catalog_integrity_verify.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,10 @@ fn classify(entry: &CatalogEntry) -> VariantClass {
175175
CatalogEntry::CloneDatabase { .. } => VariantClass::Exempt,
176176
// Move tenant cutover re-keys collections; no ownership object is created.
177177
CatalogEntry::MoveTenantCutover { .. } => VariantClass::Exempt,
178+
179+
// OIDC providers are cluster-level identity-provider config; no
180+
// per-object owner row required.
181+
CatalogEntry::PutOidcProvider(_) => VariantClass::Exempt,
182+
CatalogEntry::DeleteOidcProvider { .. } => VariantClass::Exempt,
178183
}
179184
}

nodedb/tests/mirror/helpers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ pub fn make_mirror_descriptor(
6767
status,
6868
}),
6969
audit_dml: nodedb_types::AuditDmlMode::None,
70+
idle_session_timeout_secs: 0,
7071
}
7172
}
7273

nodedb/tests/mirror/test_clone_from_mirror_rejected.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ fn clone_from_following_mirror_rejected() {
5454
status: MirrorStatus::Following,
5555
}),
5656
audit_dml: nodedb_types::AuditDmlMode::None,
57+
idle_session_timeout_secs: 0,
5758
};
5859
catalog.put_database(&descriptor).expect("inject mirror");
5960

@@ -84,6 +85,7 @@ fn clone_from_degraded_mirror_rejected() {
8485
status: MirrorStatus::Degraded { lag_ms: 9_000 },
8586
}),
8687
audit_dml: nodedb_types::AuditDmlMode::None,
88+
idle_session_timeout_secs: 0,
8789
};
8890
catalog.put_database(&descriptor).expect("inject mirror");
8991

@@ -115,6 +117,7 @@ fn clone_from_promoted_mirror_allowed() {
115117
status: MirrorStatus::Promoted,
116118
}),
117119
audit_dml: nodedb_types::AuditDmlMode::None,
120+
idle_session_timeout_secs: 0,
118121
};
119122
catalog.put_database(&descriptor).expect("inject promoted");
120123

@@ -139,6 +142,7 @@ fn clone_from_normal_database_allowed() {
139142
parent_clone: None,
140143
mirror_origin: None,
141144
audit_dml: nodedb_types::AuditDmlMode::None,
145+
idle_session_timeout_secs: 0,
142146
};
143147
catalog.put_database(&descriptor).expect("inject normal db");
144148

nodedb/tests/mirror/test_full_lifecycle.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ fn mirror_full_lifecycle() {
5252
},
5353
}),
5454
audit_dml: nodedb_types::AuditDmlMode::None,
55+
idle_session_timeout_secs: 0,
5556
};
5657
catalog
5758
.put_database(&descriptor)

nodedb/tests/mirror/test_promotion_durability.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ fn mirror_restart_does_not_reconnect_promoted() {
149149
status: MirrorStatus::Promoted,
150150
}),
151151
audit_dml: nodedb_types::AuditDmlMode::None,
152+
idle_session_timeout_secs: 0,
152153
};
153154
catalog.put_database(&descriptor).expect("inject promoted");
154155
}

nodedb/tests/mirror/test_restart_resume.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ fn mirror_restart_does_not_reconnect_promoted() {
158158
status: MirrorStatus::Promoted,
159159
}),
160160
audit_dml: nodedb_types::AuditDmlMode::None,
161+
idle_session_timeout_secs: 0,
161162
};
162163
catalog.put_database(&descriptor).expect("inject promoted");
163164
}

nodedb/tests/oidc.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
3+
//! Integration tests for OIDC provider DDL + claim mapping.
4+
5+
mod common;
6+
7+
use common::pgwire_auth_helpers::{
8+
ddl_err, ddl_ok, make_state_with_catalog, readonly_user, superuser,
9+
};
10+
use nodedb::control::security::oidc::claim_mapping::apply_claim_mapping;
11+
12+
// ── CREATE OIDC PROVIDER ────────────────────────────────────────────────────
13+
14+
#[tokio::test]
15+
async fn create_oidc_provider_persists_in_catalog() {
16+
let state = make_state_with_catalog();
17+
let su = superuser();
18+
ddl_ok(
19+
&state,
20+
&su,
21+
"CREATE OIDC PROVIDER okta \
22+
ISSUER 'https://acme.okta.com' \
23+
JWKS_URI 'https://acme.okta.com/.well-known/jwks.json' \
24+
AUDIENCE 'nodedb'",
25+
)
26+
.await;
27+
28+
let cat = state.credentials.catalog();
29+
let cat = cat.as_ref().expect("catalog must be present");
30+
let stored = cat
31+
.get_oidc_provider("okta")
32+
.expect("catalog read must succeed")
33+
.expect("provider must exist after CREATE");
34+
assert_eq!(stored.issuer, "https://acme.okta.com");
35+
assert_eq!(stored.audience.as_deref(), Some("nodedb"));
36+
}
37+
38+
#[tokio::test]
39+
async fn create_oidc_provider_requires_superuser() {
40+
let state = make_state_with_catalog();
41+
let viewer = readonly_user();
42+
let err = ddl_err(
43+
&state,
44+
&viewer,
45+
"CREATE OIDC PROVIDER bad \
46+
ISSUER 'https://x.example/' \
47+
JWKS_URI 'https://x.example/jwks'",
48+
)
49+
.await;
50+
assert!(
51+
err.contains("42501") || err.contains("permission denied"),
52+
"expected permission denied, got: {err}"
53+
);
54+
}
55+
56+
// ── DROP OIDC PROVIDER ──────────────────────────────────────────────────────
57+
58+
#[tokio::test]
59+
async fn drop_oidc_provider_removes_record() {
60+
let state = make_state_with_catalog();
61+
let su = superuser();
62+
ddl_ok(
63+
&state,
64+
&su,
65+
"CREATE OIDC PROVIDER auth0 \
66+
ISSUER 'https://x.auth0.com' \
67+
JWKS_URI 'https://x.auth0.com/.well-known/jwks.json'",
68+
)
69+
.await;
70+
ddl_ok(&state, &su, "DROP OIDC PROVIDER auth0").await;
71+
72+
let cat = state.credentials.catalog();
73+
let cat = cat.as_ref().expect("catalog must be present");
74+
let stored = cat
75+
.get_oidc_provider("auth0")
76+
.expect("catalog read must succeed");
77+
assert!(stored.is_none(), "provider must be absent after DROP");
78+
}
79+
80+
#[tokio::test]
81+
async fn drop_oidc_provider_unknown_returns_not_found() {
82+
let state = make_state_with_catalog();
83+
let su = superuser();
84+
let err = ddl_err(&state, &su, "DROP OIDC PROVIDER does_not_exist").await;
85+
assert!(
86+
err.contains("42704") || err.contains("does not exist"),
87+
"expected not-found error, got: {err}"
88+
);
89+
}
90+
91+
#[tokio::test]
92+
async fn drop_oidc_provider_if_exists_unknown_succeeds() {
93+
let state = make_state_with_catalog();
94+
let su = superuser();
95+
ddl_ok(&state, &su, "DROP OIDC PROVIDER IF EXISTS does_not_exist").await;
96+
}
97+
98+
// ── SHOW OIDC PROVIDERS ─────────────────────────────────────────────────────
99+
100+
#[tokio::test]
101+
async fn show_oidc_providers_lists_registered() {
102+
let state = make_state_with_catalog();
103+
let su = superuser();
104+
ddl_ok(
105+
&state,
106+
&su,
107+
"CREATE OIDC PROVIDER p1 \
108+
ISSUER 'https://p1.example/' \
109+
JWKS_URI 'https://p1.example/jwks'",
110+
)
111+
.await;
112+
ddl_ok(
113+
&state,
114+
&su,
115+
"CREATE OIDC PROVIDER p2 \
116+
ISSUER 'https://p2.example/' \
117+
JWKS_URI 'https://p2.example/jwks'",
118+
)
119+
.await;
120+
ddl_ok(&state, &su, "SHOW OIDC PROVIDERS").await;
121+
}
122+
123+
// ── Claim-mapping public surface smoke check ────────────────────────────────
124+
125+
#[test]
126+
fn claim_mapping_apply_function_is_public() {
127+
// Verifies that `apply_claim_mapping` is accessible from integration tests.
128+
let _ = apply_claim_mapping;
129+
}

nodedb/tests/security.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use std::sync::Arc;
1515

1616
use nodedb::bridge::dispatch::Dispatcher;
1717
use nodedb::control::security::identity::{AuthMethod, Role};
18-
use nodedb::control::security::sessions::{SessionParams, SessionRegistry};
18+
use nodedb::control::security::sessions::{KillReason, SessionParams, SessionRegistry};
1919
use nodedb::control::state::SharedState;
2020
use nodedb::types::TenantId;
2121
use nodedb::wal::WalManager;
@@ -38,6 +38,8 @@ fn sample_params(user_id: u64, username: &str) -> SessionParams {
3838
auth_method: "password".to_string(),
3939
tenant_id: 1,
4040
credential_version: 0,
41+
current_database: None,
42+
token_expiry_ms: None,
4143
}
4244
}
4345

@@ -71,10 +73,10 @@ fn session_hard_revoke_close() {
7173
let reg = SessionRegistry::new();
7274
let mut rx = reg.register("s1", &sample_params(99, "bob")).unwrap();
7375
assert!(!rx.has_changed().unwrap_or(false));
74-
let killed = reg.kill_sessions_for_user(99);
76+
let killed = reg.kill_sessions_for_user(99, KillReason::AdminKill);
7577
assert_eq!(killed, 1);
7678
assert!(rx.has_changed().unwrap_or(false));
77-
assert!(*rx.borrow_and_update());
79+
assert_ne!(*rx.borrow_and_update(), KillReason::Alive);
7880
}
7981

8082
#[test]

0 commit comments

Comments
 (0)