Skip to content

Commit e984aed

Browse files
committed
security(auth): bind external identity authority
1 parent 8e1a605 commit e984aed

60 files changed

Lines changed: 1025 additions & 886 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.config/nextest.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ slow-timeout = { period = "30s", terminate-after = 6 }
5959
# The crash-recovery label test is included because its child server otherwise
6060
# competes with the same in-process graph fan-out during the full suite.
6161
[[profile.default.overrides]]
62-
filter = 'binary(graph_cross_core_bfs) | test(query_stream_returns_all_rows_across_chunks) | test(streaming_select_returns_all_rows_across_chunks) | test(graph_node_labels_survive_kill_9) | test(wal_replay_restart_keeps_governor_accounting_sane) | test(alter_add_column_does_not_stall_writes) | test(reindex_vector_concurrent_p99) | test(count_star_sees_flushed_partitions) | test(timeseries_flush_does_not_over_release_engine_budget) | test(timeseries_governor_reflects_resident_memtable_after_flush)'
62+
filter = 'binary(graph_cross_core_bfs) | test(query_stream_returns_all_rows_across_chunks) | test(streaming_select_returns_all_rows_across_chunks) | test(graph_node_labels_survive_kill_9) | test(spatial_document_geometry_survives_kill_9) | test(wal_replay_restart_keeps_governor_accounting_sane) | test(alter_add_column_does_not_stall_writes) | test(reindex_vector_concurrent_p99) | test(count_star_sees_flushed_partitions) | test(timeseries_flush_does_not_over_release_engine_budget) | test(timeseries_governor_reflects_resident_memtable_after_flush)'
6363
test-group = 'load-sensitive'
6464
threads-required = 'num-test-threads'
6565

nodedb-cluster-tests/tests/array_raft_partition_recovery.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ use nodedb_array::sync::op::{ArrayOp, ArrayOpHeader, ArrayOpKind};
2121
use nodedb_array::sync::op_codec;
2222
use nodedb_array::types::cell_value::value::CellValue;
2323
use nodedb_array::types::coord::value::CoordValue;
24-
use nodedb_types::TenantId;
2524
use nodedb_types::sync::wire::array::ArrayDeltaMsg;
2625

2726
use common::array_sync::{hlc, register_schema_on_all};
@@ -70,19 +69,7 @@ fn make_inbound(shared: &Arc<SharedState>) -> OriginArrayInbound {
7069
engine,
7170
Arc::clone(&shared.array_sync_schemas),
7271
Arc::clone(shared),
73-
nodedb::control::security::identity::AuthenticatedIdentity {
74-
user_id: 0,
75-
username: "array-sync-test".into(),
76-
tenant_id: TenantId::new(0),
77-
auth_method: nodedb::control::security::identity::AuthMethod::Trust,
78-
roles: Vec::new(),
79-
is_superuser: true,
80-
default_database: None,
81-
accessible_databases:
82-
nodedb::control::security::identity::AuthenticatedIdentity::default_database_set(
83-
true,
84-
),
85-
},
72+
nodedb_test_support::pgwire_auth_helpers::superuser(),
8673
)
8774
}
8875

nodedb-cluster-tests/tests/array_raft_replication.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ use nodedb_array::sync::op::{ArrayOp, ArrayOpHeader, ArrayOpKind};
2525
use nodedb_array::sync::op_codec;
2626
use nodedb_array::types::cell_value::value::CellValue;
2727
use nodedb_array::types::coord::value::CoordValue;
28-
use nodedb_types::TenantId;
2928
use nodedb_types::sync::wire::array::{ArrayDeltaMsg, ArraySchemaSyncMsg};
3029

3130
use common::array_sync::{
@@ -79,19 +78,7 @@ fn make_inbound(shared: &Arc<SharedState>) -> OriginArrayInbound {
7978
engine,
8079
Arc::clone(&shared.array_sync_schemas),
8180
Arc::clone(shared),
82-
nodedb::control::security::identity::AuthenticatedIdentity {
83-
user_id: 0,
84-
username: "array-sync-test".into(),
85-
tenant_id: TenantId::new(0),
86-
auth_method: nodedb::control::security::identity::AuthMethod::Trust,
87-
roles: Vec::new(),
88-
is_superuser: true,
89-
default_database: None,
90-
accessible_databases:
91-
nodedb::control::security::identity::AuthenticatedIdentity::default_database_set(
92-
true,
93-
),
94-
},
81+
nodedb_test_support::pgwire_auth_helpers::superuser(),
9582
)
9683
}
9784

nodedb-cluster-tests/tests/array_raft_snapshot_install.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ use nodedb_array::sync::op::{ArrayOp, ArrayOpHeader, ArrayOpKind};
2121
use nodedb_array::sync::op_codec;
2222
use nodedb_array::types::cell_value::value::CellValue;
2323
use nodedb_array::types::coord::value::CoordValue;
24-
use nodedb_types::TenantId;
2524
use nodedb_types::sync::wire::array::{ArrayDeltaMsg, ArraySchemaSyncMsg};
2625

2726
use common::array_sync::{
@@ -72,19 +71,7 @@ fn make_inbound(shared: &Arc<SharedState>) -> OriginArrayInbound {
7271
engine,
7372
Arc::clone(&shared.array_sync_schemas),
7473
Arc::clone(shared),
75-
nodedb::control::security::identity::AuthenticatedIdentity {
76-
user_id: 0,
77-
username: "array-sync-test".into(),
78-
tenant_id: TenantId::new(0),
79-
auth_method: nodedb::control::security::identity::AuthMethod::Trust,
80-
roles: Vec::new(),
81-
is_superuser: true,
82-
default_database: None,
83-
accessible_databases:
84-
nodedb::control::security::identity::AuthenticatedIdentity::default_database_set(
85-
true,
86-
),
87-
},
74+
nodedb_test_support::pgwire_auth_helpers::superuser(),
8875
)
8976
}
9077

nodedb-cluster-tests/tests/array_vshard_migration.rs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ use nodedb_array::sync::op::{ArrayOp, ArrayOpHeader, ArrayOpKind};
2222
use nodedb_array::sync::op_codec;
2323
use nodedb_array::types::cell_value::value::CellValue;
2424
use nodedb_array::types::coord::value::CoordValue;
25-
use nodedb_types::TenantId;
2625
use nodedb_types::sync::wire::array::ArrayDeltaMsg;
2726

2827
use common::array_sync::{hlc, register_schema_on_all};
@@ -71,19 +70,7 @@ fn make_inbound(shared: &Arc<SharedState>) -> OriginArrayInbound {
7170
engine,
7271
Arc::clone(&shared.array_sync_schemas),
7372
Arc::clone(shared),
74-
nodedb::control::security::identity::AuthenticatedIdentity {
75-
user_id: 0,
76-
username: "array-sync-test".into(),
77-
tenant_id: TenantId::new(0),
78-
auth_method: nodedb::control::security::identity::AuthMethod::Trust,
79-
roles: Vec::new(),
80-
is_superuser: true,
81-
default_database: None,
82-
accessible_databases:
83-
nodedb::control::security::identity::AuthenticatedIdentity::default_database_set(
84-
true,
85-
),
86-
},
73+
nodedb_test_support::pgwire_auth_helpers::superuser(),
8774
)
8875
}
8976

nodedb-cluster-tests/tests/common/mod.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,10 @@ pub fn authorize_gateway_plan(
1919
plan: nodedb_physical::physical_plan::PhysicalPlan,
2020
) -> nodedb::control::server::shared::authorization::AuthorizedTask {
2121
use nodedb::control::security::audit::NoopAuditEmitter;
22-
use nodedb::control::security::identity::{AuthMethod, AuthenticatedIdentity};
2322
use nodedb::control::server::shared::authorization::authorize_task_set;
2423
use nodedb_physical::physical_task::{PhysicalTask, PostSetOp};
2524

26-
let identity = AuthenticatedIdentity {
27-
user_id: 0,
28-
username: "cluster-gateway-test".into(),
29-
tenant_id: ctx.tenant_id,
30-
auth_method: AuthMethod::Trust,
31-
roles: Vec::new(),
32-
is_superuser: true,
33-
default_database: Some(ctx.database_id),
34-
accessible_databases: AuthenticatedIdentity::default_database_set(true),
35-
};
25+
let identity = nodedb_test_support::pgwire_auth_helpers::superuser();
3626
let task = PhysicalTask {
3727
tenant_id: ctx.tenant_id,
3828
vshard_id: nodedb::types::VShardId::new(0),

nodedb-cluster-tests/tests/sync_failover.rs

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ use common::cluster_harness::{TestCluster, wait::wait_for};
2727
use std::sync::Arc;
2828
use std::time::Duration;
2929

30-
use nodedb::control::security::identity::{AuthMethod, AuthenticatedIdentity};
3130
use nodedb::control::server::sync::columnar_handler::{
3231
ColumnarDispatcher, SharedStateColumnarDispatcher,
3332
};
@@ -185,16 +184,7 @@ async fn dispatch_columnar_seq(
185184
seq: u64,
186185
val: &str,
187186
) -> Result<AckStatus, String> {
188-
let identity = AuthenticatedIdentity {
189-
user_id: 0,
190-
username: "sync-failover-test".into(),
191-
tenant_id: ctx.tenant,
192-
auth_method: AuthMethod::Trust,
193-
roles: Vec::new(),
194-
is_superuser: true,
195-
default_database: Some(DatabaseId::DEFAULT),
196-
accessible_databases: AuthenticatedIdentity::default_database_set(true),
197-
};
187+
let identity = nodedb_test_support::pgwire_auth_helpers::superuser();
198188
let dispatcher =
199189
SharedStateColumnarDispatcher::new(ctx.shared.as_ref(), &identity, DatabaseId::DEFAULT);
200190
let rows = vec![vec![

nodedb-test-support/src/pgwire_auth_helpers.rs

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -49,32 +49,27 @@ pub fn make_state_with_catalog() -> Arc<SharedState> {
4949

5050
/// Superuser identity for DDL tests.
5151
pub fn superuser() -> AuthenticatedIdentity {
52-
AuthenticatedIdentity {
53-
user_id: 0,
54-
username: "nodedb".into(),
55-
tenant_id: TenantId::new(0),
56-
auth_method: AuthMethod::Trust,
57-
roles: vec![Role::Superuser],
58-
is_superuser: true,
59-
default_database: None,
60-
accessible_databases: DatabaseSet::All,
61-
}
52+
let store = nodedb::control::security::credential::store::CredentialStore::new()
53+
.expect("create test credential store");
54+
store
55+
.bootstrap_superuser("nodedb", "test-only-password")
56+
.expect("bootstrap test superuser");
57+
store
58+
.to_identity("nodedb", AuthMethod::Trust)
59+
.expect("build catalog-authenticated test superuser")
6260
}
6361

6462
/// Readonly identity for permission tests.
6563
pub fn readonly_user() -> AuthenticatedIdentity {
66-
AuthenticatedIdentity {
67-
user_id: 99,
68-
username: "viewer".into(),
69-
tenant_id: TenantId::new(1),
70-
auth_method: AuthMethod::Trust,
71-
roles: vec![Role::ReadOnly],
72-
is_superuser: false,
73-
default_database: None,
74-
accessible_databases: DatabaseSet::Some(smallvec::smallvec![
75-
nodedb_types::id::DatabaseId::DEFAULT
76-
]),
77-
}
64+
AuthenticatedIdentity::new_regular(
65+
99,
66+
"viewer",
67+
TenantId::new(1),
68+
AuthMethod::Trust,
69+
vec![Role::ReadOnly],
70+
None,
71+
DatabaseSet::Some(smallvec::smallvec![nodedb_types::id::DatabaseId::DEFAULT,]),
72+
)
7873
}
7974

8075
/// Run DDL, expect success.
@@ -144,32 +139,28 @@ pub async fn assert_readonly_denied(state: &SharedState, sql: &str) {
144139

145140
/// Cluster-admin identity (no implicit RLS bypass, no cross-DB data access).
146141
pub fn cluster_admin_user() -> AuthenticatedIdentity {
147-
AuthenticatedIdentity {
148-
user_id: 100,
149-
username: "cluster_admin".into(),
150-
tenant_id: nodedb::types::TenantId::new(1),
151-
auth_method: AuthMethod::Trust,
152-
roles: vec![Role::ClusterAdmin],
153-
is_superuser: false,
154-
default_database: None,
155-
accessible_databases: DatabaseSet::Some(smallvec::smallvec![
156-
nodedb_types::id::DatabaseId::DEFAULT
157-
]),
158-
}
142+
AuthenticatedIdentity::new_regular(
143+
100,
144+
"cluster_admin",
145+
nodedb::types::TenantId::new(1),
146+
AuthMethod::Trust,
147+
vec![Role::ClusterAdmin],
148+
None,
149+
DatabaseSet::Some(smallvec::smallvec![nodedb_types::id::DatabaseId::DEFAULT,]),
150+
)
159151
}
160152

161153
/// Database-owner identity for `db_id`.
162154
pub fn database_owner_user(db_id: nodedb_types::id::DatabaseId) -> AuthenticatedIdentity {
163-
AuthenticatedIdentity {
164-
user_id: 101,
165-
username: "db_owner".into(),
166-
tenant_id: nodedb::types::TenantId::new(1),
167-
auth_method: AuthMethod::Trust,
168-
roles: vec![Role::DatabaseOwner(db_id)],
169-
is_superuser: false,
170-
default_database: None,
171-
accessible_databases: DatabaseSet::Some(smallvec::smallvec![db_id]),
172-
}
155+
AuthenticatedIdentity::new_regular(
156+
101,
157+
"db_owner",
158+
nodedb::types::TenantId::new(1),
159+
AuthMethod::Trust,
160+
vec![Role::DatabaseOwner(db_id)],
161+
None,
162+
DatabaseSet::Some(smallvec::smallvec![db_id]),
163+
)
173164
}
174165

175166
/// Assert that the audit log contains at least one entry with `event` and `db_id`.

nodedb/src/config/auth/config.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,14 @@ pub struct JwtAuthConfig {
119119
#[serde(default = "default_allowed_algorithms")]
120120
pub allowed_algorithms: Vec<String>,
121121

122-
/// Clock skew tolerance in seconds for `exp`/`nbf` validation.
122+
/// Clock skew tolerance in seconds for `exp`/`nbf`/`iat` validation.
123123
#[serde(default = "default_clock_skew")]
124124
pub clock_skew_secs: u64,
125125

126+
/// Maximum accepted JWT lifetime (`exp - iat`) in seconds.
127+
#[serde(default = "default_max_token_lifetime")]
128+
pub max_token_lifetime_secs: u64,
129+
126130
/// Path to cache JWKS on disk for offline fallback.
127131
/// If set, JWKS responses are persisted and used when providers are unreachable.
128132
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -252,6 +256,11 @@ impl JwtAuthConfig {
252256
/// Validate all providers. Called by the server-config loader and JWKS
253257
/// registry construction so misconfiguration fails before authentication.
254258
pub fn validate(&self) -> crate::Result<()> {
259+
if self.max_token_lifetime_secs == 0 {
260+
return Err(crate::Error::Config {
261+
detail: "auth.jwt max_token_lifetime_secs must be greater than zero".into(),
262+
});
263+
}
255264
let policy = self.jwks_policy().map_err(|e| crate::Error::Config {
256265
detail: format!("auth.jwt allow-list is invalid: {e}"),
257266
})?;
@@ -307,6 +316,9 @@ fn default_jwks_min_refetch() -> u64 {
307316
fn default_clock_skew() -> u64 {
308317
60
309318
}
319+
fn default_max_token_lifetime() -> u64 {
320+
86_400
321+
}
310322
fn default_allowed_algorithms() -> Vec<String> {
311323
vec!["RS256".into(), "ES256".into()]
312324
}
@@ -321,6 +333,7 @@ impl Default for JwtAuthConfig {
321333
jwks_min_refetch_secs: default_jwks_min_refetch(),
322334
allowed_algorithms: default_allowed_algorithms(),
323335
clock_skew_secs: default_clock_skew(),
336+
max_token_lifetime_secs: default_max_token_lifetime(),
324337
jwks_cache_path: None,
325338
providers: Vec::new(),
326339
jit_provisioning: false,

nodedb/src/control/crdt_admission.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -953,19 +953,18 @@ mod tests {
953953
post_set_op: nodedb_physical::physical_task::PostSetOp::None,
954954
txn_id: None,
955955
};
956-
let identity = crate::control::security::identity::AuthenticatedIdentity {
957-
user_id: 1,
958-
username: "crdt-test".into(),
959-
tenant_id,
960-
auth_method: crate::control::security::identity::AuthMethod::Trust,
961-
roles: Vec::new(),
962-
is_superuser: true,
963-
default_database: None,
964-
accessible_databases:
956+
let identity =
957+
crate::control::security::identity::AuthenticatedIdentity::new_internal_service(
958+
1,
959+
"crdt-test",
960+
tenant_id,
961+
Vec::new(),
962+
true,
963+
None,
965964
crate::control::security::identity::AuthenticatedIdentity::default_database_set(
966965
true,
967966
),
968-
};
967+
);
969968
let authorized = crate::control::server::shared::authorization::authorize_task_set(
970969
&identity,
971970
std::slice::from_ref(&task),

0 commit comments

Comments
 (0)