diff --git a/apps/aether-gateway/src/data/state/auth.rs b/apps/aether-gateway/src/data/state/auth.rs index 8371bbd7d..7bc60e737 100644 --- a/apps/aether-gateway/src/data/state/auth.rs +++ b/apps/aether-gateway/src/data/state/auth.rs @@ -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( diff --git a/apps/aether-gateway/src/tests/control/admin/users.rs b/apps/aether-gateway/src/tests/control/admin/users.rs index 75c8da4f2..86558d82e 100644 --- a/apps/aether-gateway/src/tests/control/admin/users.rs +++ b/apps/aether-gateway/src/tests/control/admin/users.rs @@ -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(); }