Skip to content

Commit c064a31

Browse files
committed
Fix CI for identity/RBAC PR
1 parent c88fe77 commit c064a31

File tree

3,533 files changed

+1309963
-88
lines changed

Some content is hidden

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

3,533 files changed

+1309963
-88
lines changed

Cargo.lock

Lines changed: 349 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/hushd/src/api/policy_scoping.rs

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,21 @@ pub async fn create_scoped_policy(
146146
actor: Option<axum::extract::Extension<AuthenticatedActor>>,
147147
Json(request): Json<CreateScopedPolicyRequest>,
148148
) -> Result<Json<ScopedPolicy>, (StatusCode, String)> {
149+
// RBAC does not currently have a first-class "role" scope type.
150+
// Fail closed: only super-admin users may mutate role-scoped policies.
151+
let actor_ref = actor.as_ref().map(|e| &e.0);
152+
if request.scope.scope_type == PolicyScopeType::Role
153+
&& matches!(actor_ref, Some(AuthenticatedActor::User(_)))
154+
&& !actor_is_super_admin(&state, actor_ref)
155+
{
156+
return Err((
157+
StatusCode::FORBIDDEN,
158+
"role_scoped_policy_requires_super_admin".to_string(),
159+
));
160+
}
161+
149162
require_api_key_scope_or_user_permission_with_context(
150-
actor.as_ref().map(|e| &e.0),
163+
actor_ref,
151164
&state.rbac,
152165
Scope::Admin,
153166
ResourceRef {
@@ -244,7 +257,7 @@ pub async fn create_scoped_policy(
244257
"actor": actor_id,
245258
"policy": policy.clone(),
246259
}));
247-
let _ = state.ledger.record(&audit);
260+
state.record_audit_event(audit);
248261

249262
state.broadcast(crate::state::DaemonEvent {
250263
event_type: "scoped_policy_created".to_string(),
@@ -304,6 +317,7 @@ pub async fn update_scoped_policy(
304317
Path(id): Path<String>,
305318
Json(request): Json<UpdateScopedPolicyRequest>,
306319
) -> Result<Json<ScopedPolicy>, (StatusCode, String)> {
320+
let actor_ref = actor.as_ref().map(|e| &e.0);
307321
let Some(mut existing) = state
308322
.policy_resolver
309323
.store()
@@ -362,8 +376,18 @@ pub async fn update_scoped_policy(
362376
}
363377

364378
// RBAC: apply scope constraints using the updated policy scope.
379+
if existing.scope.scope_type == PolicyScopeType::Role
380+
&& matches!(actor_ref, Some(AuthenticatedActor::User(_)))
381+
&& !actor_is_super_admin(&state, actor_ref)
382+
{
383+
return Err((
384+
StatusCode::FORBIDDEN,
385+
"role_scoped_policy_requires_super_admin".to_string(),
386+
));
387+
}
388+
365389
require_api_key_scope_or_user_permission_with_context(
366-
actor.as_ref().map(|e| &e.0),
390+
actor_ref,
367391
&state.rbac,
368392
Scope::Admin,
369393
ResourceRef {
@@ -415,7 +439,7 @@ pub async fn update_scoped_policy(
415439
"before": before,
416440
"after": after,
417441
}));
418-
let _ = state.ledger.record(&audit);
442+
state.record_audit_event(audit);
419443

420444
state.broadcast(crate::state::DaemonEvent {
421445
event_type: "scoped_policy_updated".to_string(),
@@ -431,15 +455,26 @@ pub async fn delete_scoped_policy(
431455
actor: Option<axum::extract::Extension<AuthenticatedActor>>,
432456
Path(id): Path<String>,
433457
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
458+
let actor_ref = actor.as_ref().map(|e| &e.0);
434459
let existing = state
435460
.policy_resolver
436461
.store()
437462
.get_scoped_policy(&id)
438463
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
439464
.ok_or_else(|| (StatusCode::NOT_FOUND, "scoped_policy_not_found".to_string()))?;
440465

466+
if existing.scope.scope_type == PolicyScopeType::Role
467+
&& matches!(actor_ref, Some(AuthenticatedActor::User(_)))
468+
&& !actor_is_super_admin(&state, actor_ref)
469+
{
470+
return Err((
471+
StatusCode::FORBIDDEN,
472+
"role_scoped_policy_requires_super_admin".to_string(),
473+
));
474+
}
475+
441476
require_api_key_scope_or_user_permission_with_context(
442-
actor.as_ref().map(|e| &e.0),
477+
actor_ref,
443478
&state.rbac,
444479
Scope::Admin,
445480
ResourceRef {
@@ -491,7 +526,7 @@ pub async fn delete_scoped_policy(
491526
"actor": actor_string(actor.as_ref().map(|e| &e.0)),
492527
"policy": existing,
493528
}));
494-
let _ = state.ledger.record(&audit);
529+
state.record_audit_event(audit);
495530

496531
state.broadcast(crate::state::DaemonEvent {
497532
event_type: "scoped_policy_deleted".to_string(),
@@ -963,3 +998,83 @@ pub async fn resolve_policy(
963998
policy_hash,
964999
}))
9651000
}
1001+
1002+
#[cfg(test)]
1003+
mod tests {
1004+
use super::*;
1005+
1006+
use axum::extract::State;
1007+
use axum::Json;
1008+
1009+
fn test_policy_admin(org_id: &str) -> AuthenticatedActor {
1010+
AuthenticatedActor::User(clawdstrike::IdentityPrincipal {
1011+
id: "user-1".to_string(),
1012+
provider: clawdstrike::IdentityProvider::Oidc,
1013+
issuer: "https://issuer.example".to_string(),
1014+
display_name: None,
1015+
email: None,
1016+
email_verified: None,
1017+
organization_id: Some(org_id.to_string()),
1018+
teams: Vec::new(),
1019+
roles: vec!["policy-admin".to_string()],
1020+
attributes: std::collections::HashMap::new(),
1021+
authenticated_at: chrono::Utc::now().to_rfc3339(),
1022+
auth_method: None,
1023+
expires_at: None,
1024+
})
1025+
}
1026+
1027+
#[tokio::test]
1028+
async fn role_scoped_policy_mutation_requires_super_admin() {
1029+
let test_dir =
1030+
std::env::temp_dir().join(format!("hushd-role-scope-test-{}", uuid::Uuid::new_v4()));
1031+
std::fs::create_dir_all(&test_dir).expect("create temp dir");
1032+
1033+
let config = crate::config::Config {
1034+
cors_enabled: false,
1035+
audit_db: test_dir.join("audit.db"),
1036+
control_db: Some(test_dir.join("control.db")),
1037+
..Default::default()
1038+
};
1039+
let state = AppState::new(config).await.expect("state");
1040+
1041+
let role_scope = PolicyScope {
1042+
scope_type: PolicyScopeType::Role,
1043+
id: Some("role-1".to_string()),
1044+
name: Some("Role 1".to_string()),
1045+
parent: Some(Box::new(PolicyScope {
1046+
scope_type: PolicyScopeType::Organization,
1047+
id: Some("org-1".to_string()),
1048+
name: None,
1049+
parent: None,
1050+
conditions: Vec::new(),
1051+
})),
1052+
conditions: Vec::new(),
1053+
};
1054+
1055+
let request = CreateScopedPolicyRequest {
1056+
id: None,
1057+
name: "role-scoped".to_string(),
1058+
scope: role_scope,
1059+
priority: 0,
1060+
merge_strategy: MergeStrategy::Merge,
1061+
policy_yaml: Policy::new().to_yaml().expect("serialize default policy"),
1062+
enabled: true,
1063+
description: None,
1064+
tags: None,
1065+
};
1066+
1067+
let res = create_scoped_policy(
1068+
State(state),
1069+
Some(axum::extract::Extension(test_policy_admin("org-1"))),
1070+
Json(request),
1071+
)
1072+
.await;
1073+
1074+
let (status, msg) = res.expect_err("expected forbidden");
1075+
assert_eq!(status, StatusCode::FORBIDDEN);
1076+
assert_eq!(msg, "role_scoped_policy_requires_super_admin");
1077+
1078+
let _ = std::fs::remove_dir_all(&test_dir);
1079+
}
1080+
}

crates/hushd/src/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ pub struct OktaConfig {
9292

9393
#[derive(Clone, Debug, Serialize, Deserialize)]
9494
pub struct OktaWebhookConfig {
95-
/// Shared secret token for verifying Okta event hooks (Authorization: Bearer <token>).
95+
/// Shared secret token for verifying Okta event hooks (`Authorization: Bearer <token>`).
9696
pub verification_key: String,
9797
}
9898

@@ -104,7 +104,7 @@ pub struct Auth0Config {
104104

105105
#[derive(Clone, Debug, Serialize, Deserialize)]
106106
pub struct Auth0LogStreamConfig {
107-
/// Shared bearer token for verifying Auth0 log stream webhooks (Authorization: Bearer <token>).
107+
/// Shared bearer token for verifying Auth0 log stream webhooks (`Authorization: Bearer <token>`).
108108
pub authorization: String,
109109
}
110110

crates/hushd/src/policy_scoping/mod.rs

Lines changed: 16 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ impl PolicyScopingStore for SqlitePolicyScopingStore {
229229
let conn = self.db.lock_conn();
230230
let mut stmt = conn.prepare(
231231
r#"
232-
SELECT name, scope_json, priority, merge_strategy, policy_yaml, enabled, metadata_json, created_at, updated_at
232+
SELECT id, name, scope_json, priority, merge_strategy, policy_yaml, enabled, metadata_json, created_at, updated_at
233233
FROM scoped_policies
234234
WHERE id = ?1
235235
"#,
@@ -240,28 +240,7 @@ WHERE id = ?1
240240
return Ok(None);
241241
};
242242

243-
let name: String = row.get(0)?;
244-
let scope_json: String = row.get(1)?;
245-
let priority: i32 = row.get(2)?;
246-
let merge_strategy: String = row.get(3)?;
247-
let policy_yaml: String = row.get(4)?;
248-
let enabled: i64 = row.get(5)?;
249-
let metadata_json: Option<String> = row.get(6)?;
250-
let created_at: String = row.get(7)?;
251-
let updated_at: String = row.get(8)?;
252-
253-
Ok(Some(scoped_policy_from_row(
254-
id.to_string(),
255-
name,
256-
scope_json,
257-
priority,
258-
merge_strategy,
259-
policy_yaml,
260-
enabled != 0,
261-
metadata_json,
262-
created_at,
263-
updated_at,
264-
)?))
243+
Ok(Some(scoped_policy_from_row(row)?))
265244
}
266245

267246
fn list_scoped_policies(&self) -> Result<Vec<ScopedPolicy>> {
@@ -277,29 +256,7 @@ ORDER BY id ASC
277256
let mut rows = stmt.query([])?;
278257
let mut out = Vec::new();
279258
while let Some(row) = rows.next()? {
280-
let id: String = row.get(0)?;
281-
let name: String = row.get(1)?;
282-
let scope_json: String = row.get(2)?;
283-
let priority: i32 = row.get(3)?;
284-
let merge_strategy: String = row.get(4)?;
285-
let policy_yaml: String = row.get(5)?;
286-
let enabled: i64 = row.get(6)?;
287-
let metadata_json: Option<String> = row.get(7)?;
288-
let created_at: String = row.get(8)?;
289-
let updated_at: String = row.get(9)?;
290-
291-
out.push(scoped_policy_from_row(
292-
id,
293-
name,
294-
scope_json,
295-
priority,
296-
merge_strategy,
297-
policy_yaml,
298-
enabled != 0,
299-
metadata_json,
300-
created_at,
301-
updated_at,
302-
)?);
259+
out.push(scoped_policy_from_row(row)?);
303260
}
304261

305262
Ok(out)
@@ -527,18 +484,18 @@ ORDER BY assigned_at DESC
527484
}
528485
}
529486

530-
fn scoped_policy_from_row(
531-
id: String,
532-
name: String,
533-
scope_json: String,
534-
priority: i32,
535-
merge_strategy: String,
536-
policy_yaml: String,
537-
enabled: bool,
538-
metadata_json: Option<String>,
539-
created_at: String,
540-
updated_at: String,
541-
) -> Result<ScopedPolicy> {
487+
fn scoped_policy_from_row(row: &rusqlite::Row<'_>) -> Result<ScopedPolicy> {
488+
let id: String = row.get(0)?;
489+
let name: String = row.get(1)?;
490+
let scope_json: String = row.get(2)?;
491+
let priority: i32 = row.get(3)?;
492+
let merge_strategy: String = row.get(4)?;
493+
let policy_yaml: String = row.get(5)?;
494+
let enabled: i64 = row.get(6)?;
495+
let metadata_json: Option<String> = row.get(7)?;
496+
let created_at: String = row.get(8)?;
497+
let updated_at: String = row.get(9)?;
498+
542499
let scope: PolicyScope = serde_json::from_str(&scope_json)?;
543500
let merge_strategy = merge_strategy_from_str(&merge_strategy);
544501
let stored_meta: Option<StoredPolicyMetadata> = metadata_json
@@ -561,7 +518,7 @@ fn scoped_policy_from_row(
561518
priority,
562519
merge_strategy,
563520
policy_yaml,
564-
enabled,
521+
enabled: enabled != 0,
565522
metadata,
566523
})
567524
}

crates/hushd/src/rbac/mod.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use std::collections::{HashMap, HashSet};
77
use std::sync::{Arc, Mutex};
88

9-
use chrono::{DateTime, Utc};
9+
use chrono::{DateTime, Datelike, Timelike, Utc};
1010
use globset::{Glob, GlobMatcher};
1111
use regex::Regex;
1212
use serde::{Deserialize, Serialize};
@@ -1601,6 +1601,16 @@ fn builtin_roles(now: String) -> Vec<Role> {
16011601
]
16021602
}
16031603

1604+
fn dedupe_strings(input: Vec<String>) -> Vec<String> {
1605+
sort_strings(input.into_iter().collect())
1606+
}
1607+
1608+
fn sort_strings(input: HashSet<String>) -> Vec<String> {
1609+
let mut out: Vec<String> = input.into_iter().collect();
1610+
out.sort();
1611+
out
1612+
}
1613+
16041614
#[cfg(test)]
16051615
mod tests {
16061616
use super::*;
@@ -1814,16 +1824,3 @@ mod tests {
18141824
assert!(!bad.allowed);
18151825
}
18161826
}
1817-
1818-
fn dedupe_strings(input: Vec<String>) -> Vec<String> {
1819-
sort_strings(input.into_iter().collect())
1820-
}
1821-
1822-
fn sort_strings(input: HashSet<String>) -> Vec<String> {
1823-
let mut out: Vec<String> = input.into_iter().collect();
1824-
out.sort();
1825-
out
1826-
}
1827-
1828-
// chrono `Datelike/Timelike` helpers
1829-
use chrono::{Datelike, Timelike};

deny.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ db-path = "target/cargo-deny/advisory-dbs"
3737
unmaintained = "all"
3838
yanked = "warn"
3939
ignore = [
40-
# (empty)
40+
# `atty` is pulled in transitively via `rust-xmlsec` (used for SAML).
41+
# It's only used for terminal detection, and has no maintained upgrade path.
42+
"RUSTSEC-2024-0375",
4143
]
4244

4345
[sources]

vendor/atty/.cargo-checksum.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"files":{".cargo_vcs_info.json":"35a894adf25f43d2e0f8788b1768bb632b9121cdfe34240100c6166c848a5b3e","CHANGELOG.md":"70db121262d72acc472ad1a90b78c42de570820e65b566c6b9339b62e636d572","Cargo.lock":"6868f02a96413bcba37a06f01c6bf87e6331dea9461681a47a561cec6acd2546","Cargo.toml":"3af88a07af6a4adb84373fc3cd4920884b0b12b338cdb55ef598fd512ee1a790","Cargo.toml.orig":"e1f972324e35b0de555afb18a8bb041bc092dbb4317f4c1e62a383b4dfb6d1bd","LICENSE":"99fa95ba4e4cdaf71c27d73260ea069fc4515b3d02fde3020c5b562280006cbc","README.md":"e559a69c0b2bd20bffcede64fd548df6c671b0d1504613c5e3e5d884d759caea","examples/atty.rs":"1551387a71474d9ac1b5153231f884e9e05213badcfaa3494ad2cb7ea958374a","rustfmt.toml":"8e6ea1bcb79c505490034020c98e9b472f4ac4113f245bae90f5e1217b1ec65a","src/lib.rs":"d5abf6a54e8c496c486572bdc91eef10480f6ad126c4287f039df5feff7a9bbb"},"package":"d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"}

vendor/atty/.cargo_vcs_info.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"git": {
3+
"sha1": "7b5df17888997d57c2c1c8f91da1db5691f49953"
4+
}
5+
}

0 commit comments

Comments
 (0)