fix(gateway): apply group policy to admin-owned keys

This commit is contained in:
ZheFox
2026-07-20 15:51:01 +08:00
parent f8778c4a23
commit c34ec7c1ee
2 changed files with 40 additions and 29 deletions
+1 -28
View File
@@ -1781,20 +1781,13 @@ impl GatewayDataState {
let Some(mut snapshot) = snapshot else {
return Ok(None);
};
if snapshot.user_role.eq_ignore_ascii_case("admin") && !snapshot.api_key_is_standalone {
apply_admin_unrestricted_auth_snapshot(&mut snapshot);
return Ok(Some(GatewayAuthApiKeySnapshot::from_stored(
snapshot,
now_unix_secs,
)));
}
let Some(repository) = self.user_reader.as_ref() else {
return Ok(Some(GatewayAuthApiKeySnapshot::from_stored(
snapshot,
now_unix_secs,
)));
};
let Some(user) = crate::request_diagnostics::observe_db_operation(
let Some(_) = crate::request_diagnostics::observe_db_operation(
"auth_user_policy",
self.database_pool_summary(),
repository.find_user_auth_by_id(&snapshot.user_id),
@@ -1806,14 +1799,6 @@ impl GatewayDataState {
now_unix_secs,
)));
};
if user.role.eq_ignore_ascii_case("admin") && !snapshot.api_key_is_standalone {
snapshot.user_role = user.role;
apply_admin_unrestricted_auth_snapshot(&mut snapshot);
return Ok(Some(GatewayAuthApiKeySnapshot::from_stored(
snapshot,
now_unix_secs,
)));
}
let groups = self
.effective_user_groups_for_user(&snapshot.user_id)
.await?;
@@ -1925,18 +1910,6 @@ impl GatewayDataState {
}
}
fn apply_admin_unrestricted_auth_snapshot(snapshot: &mut StoredAuthApiKeySnapshot) {
snapshot.user_allowed_providers = None;
snapshot.user_allowed_api_formats = None;
snapshot.user_allowed_models = None;
snapshot.user_rate_limit = None;
snapshot.api_key_allowed_providers = None;
snapshot.api_key_allowed_api_formats = None;
snapshot.api_key_allowed_models = None;
snapshot.api_key_rate_limit = None;
snapshot.api_key_concurrent_limit = None;
}
// Per-user list policy columns are retained only for legacy import/export compatibility.
// Runtime authorization and user-facing catalogs must both treat group policies as authoritative.
fn resolve_group_effective_list_policies(
@@ -1673,7 +1673,7 @@ async fn gateway_handles_admin_user_api_key_routes_locally_with_trusted_admin_pr
}
#[tokio::test]
async fn admin_created_user_key_inherits_target_user_policy_not_admin_policy() {
async fn admin_created_user_keys_inherit_owner_group_policy() {
let mut admin_snapshot = sample_admin_api_key_snapshot("admin-user", "admin-seed-key");
admin_snapshot.user_role = "admin".to_string();
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![
@@ -1855,6 +1855,44 @@ async fn admin_created_user_key_inherits_target_user_policy_not_admin_policy() {
"an omitted key override must inherit target settings"
);
let response = reqwest::Client::new()
.post(format!("{gateway_url}/api/admin/users/admin-user/api-keys"))
.header(crate::constants::GATEWAY_HEADER, "rust-phase3b")
.header(TRUSTED_ADMIN_USER_ID_HEADER, "admin-user")
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "admin-session")
.json(&json!({"name": "admin-user-key"}))
.send()
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let payload: serde_json::Value = response.json().await.expect("response should parse");
let plaintext_key = payload["key"].as_str().expect("plaintext key should exist");
let resolved = inspection_state
.read_cached_auth_api_key_snapshot_by_key_hash(
&hash_api_key(plaintext_key),
chrono::Utc::now().timestamp().max(0) as u64,
)
.await
.expect("admin-owned user key snapshot should resolve")
.expect("admin-owned user key snapshot should exist");
assert_eq!(resolved.user_id, "admin-user");
assert!(!resolved.api_key_is_standalone);
assert_eq!(
resolved.effective_allowed_providers(),
Some(&["openai".to_string()][..])
);
assert_eq!(
resolved.effective_allowed_api_formats(),
Some(&["openai:responses".to_string()][..])
);
assert_eq!(
resolved.effective_allowed_models(),
Some(&["gpt-5.4".to_string()][..])
);
assert_eq!(resolved.user_rate_limit, Some(100));
gateway_handle.abort();
}