mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat: add management token permissions
This commit is contained in:
@@ -54,6 +54,7 @@ pub(crate) struct GatewayAdminPrincipalContext {
|
||||
pub(crate) user_role: String,
|
||||
pub(crate) session_id: Option<String>,
|
||||
pub(crate) management_token_id: Option<String>,
|
||||
pub(crate) management_token_permissions: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub(in super::super) enum ControlDecisionAuthResolution {
|
||||
@@ -243,6 +244,7 @@ fn resolve_trusted_admin_principal(
|
||||
user_role: trusted_headers.user_role,
|
||||
session_id: trusted_headers.session_id,
|
||||
management_token_id: trusted_headers.management_token_id,
|
||||
management_token_permissions: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -328,6 +330,7 @@ async fn resolve_local_admin_principal_from_claims(
|
||||
user_role: "admin".to_string(),
|
||||
session_id: Some(session.id),
|
||||
management_token_id: None,
|
||||
management_token_permissions: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
518
apps/aether-gateway/src/control/management_token_permissions.rs
Normal file
518
apps/aether-gateway/src/control/management_token_permissions.rs
Normal file
@@ -0,0 +1,518 @@
|
||||
use axum::http;
|
||||
use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use super::GatewayControlDecision;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct PermissionGroup {
|
||||
scope: &'static str,
|
||||
label: &'static str,
|
||||
assignable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
pub(crate) struct ManagementTokenPermissionCatalogItem {
|
||||
pub(crate) key: &'static str,
|
||||
pub(crate) scope: &'static str,
|
||||
pub(crate) scope_label: &'static str,
|
||||
pub(crate) access: &'static str,
|
||||
pub(crate) access_label: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct ManagementTokenPermissionDenied {
|
||||
pub(crate) required_permission: String,
|
||||
}
|
||||
|
||||
const PERMISSION_GROUPS: &[PermissionGroup] = &[
|
||||
PermissionGroup {
|
||||
scope: "adaptive",
|
||||
label: "自适应调度",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "announcements",
|
||||
label: "公告",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "api_keys",
|
||||
label: "API 密钥",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "billing",
|
||||
label: "账单",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "endpoints_health",
|
||||
label: "端点健康",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "endpoints_manage",
|
||||
label: "端点配置",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "endpoints_rpm",
|
||||
label: "端点 RPM",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "gemini_files",
|
||||
label: "Gemini 文件",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "ldap",
|
||||
label: "LDAP",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "management_tokens",
|
||||
label: "访问令牌",
|
||||
assignable: false,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "models",
|
||||
label: "模型",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "modules",
|
||||
label: "模块管理",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "monitoring",
|
||||
label: "监控",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "oauth",
|
||||
label: "OAuth 配置",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "payments",
|
||||
label: "支付",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "pool",
|
||||
label: "号池",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "provider_ops",
|
||||
label: "Provider 运维",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "provider_oauth",
|
||||
label: "Provider OAuth",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "provider_query",
|
||||
label: "Provider 查询",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "provider_strategy",
|
||||
label: "Provider 策略",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "providers",
|
||||
label: "供应商与模型",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "proxy_nodes",
|
||||
label: "代理节点",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "security",
|
||||
label: "安全",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "stats",
|
||||
label: "统计",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "system",
|
||||
label: "系统",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "usage",
|
||||
label: "用量",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "users",
|
||||
label: "用户",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "video_tasks",
|
||||
label: "视频任务",
|
||||
assignable: true,
|
||||
},
|
||||
PermissionGroup {
|
||||
scope: "wallets",
|
||||
label: "钱包",
|
||||
assignable: true,
|
||||
},
|
||||
];
|
||||
|
||||
const ACCESS_LEVELS: &[(&str, &str)] = &[("read", "读取"), ("write", "写入"), ("admin", "管理")];
|
||||
|
||||
pub(crate) fn management_token_permission_catalog_items(
|
||||
) -> Vec<ManagementTokenPermissionCatalogItem> {
|
||||
PERMISSION_GROUPS
|
||||
.iter()
|
||||
.filter(|group| group.assignable)
|
||||
.flat_map(|group| {
|
||||
ACCESS_LEVELS.iter().map(move |(access, access_label)| {
|
||||
ManagementTokenPermissionCatalogItem {
|
||||
key: permission_key(group.scope, access),
|
||||
scope: group.scope,
|
||||
scope_label: group.label,
|
||||
access,
|
||||
access_label,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn management_token_permission_catalog_payload() -> Value {
|
||||
let items = management_token_permission_catalog_items();
|
||||
json!({
|
||||
"items": items,
|
||||
"all_permissions": all_assignable_management_token_permissions(),
|
||||
"read_only_permissions": read_only_management_token_permissions(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn all_assignable_management_token_permissions() -> Vec<String> {
|
||||
management_token_permission_catalog_items()
|
||||
.into_iter()
|
||||
.map(|item| item.key.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn read_only_management_token_permissions() -> Vec<String> {
|
||||
PERMISSION_GROUPS
|
||||
.iter()
|
||||
.filter(|group| group.assignable)
|
||||
.map(|group| permission_key(group.scope, "read").to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_assignable_management_token_permissions(
|
||||
value: Option<&Value>,
|
||||
) -> Result<Value, String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(json!(all_assignable_management_token_permissions()));
|
||||
};
|
||||
if value.is_null() {
|
||||
return Ok(json!(all_assignable_management_token_permissions()));
|
||||
}
|
||||
let Some(items) = value.as_array() else {
|
||||
return Err("permissions 必须是字符串数组".to_string());
|
||||
};
|
||||
if items.is_empty() {
|
||||
return Err("permissions 不能为空".to_string());
|
||||
}
|
||||
|
||||
let mut normalized = BTreeSet::new();
|
||||
for item in items {
|
||||
let Some(raw) = item.as_str() else {
|
||||
return Err("permissions 必须是字符串数组".to_string());
|
||||
};
|
||||
let key = raw.trim();
|
||||
if key.is_empty() {
|
||||
return Err("permissions 不能包含空字符串".to_string());
|
||||
}
|
||||
if !is_assignable_management_token_permission(key) {
|
||||
return Err(format!("无效的管理令牌权限: {key}"));
|
||||
}
|
||||
normalized.insert(key.to_string());
|
||||
}
|
||||
|
||||
Ok(json!(normalized.into_iter().collect::<Vec<_>>()))
|
||||
}
|
||||
|
||||
pub(crate) fn management_token_permission_keys_from_value(
|
||||
value: Option<&Value>,
|
||||
) -> Result<Option<Vec<String>>, String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
};
|
||||
if value.is_null() {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(items) = value.as_array() else {
|
||||
return Err("management token permissions must be an array".to_string());
|
||||
};
|
||||
if items.is_empty() {
|
||||
return Err("management token permissions must not be empty".to_string());
|
||||
}
|
||||
let mut keys = Vec::with_capacity(items.len());
|
||||
for item in items {
|
||||
let Some(key) = item.as_str() else {
|
||||
return Err("management token permissions must contain strings".to_string());
|
||||
};
|
||||
if !is_assignable_management_token_permission(key) {
|
||||
return Err(format!("unknown management token permission: {key}"));
|
||||
}
|
||||
keys.push(key.to_string());
|
||||
}
|
||||
Ok(Some(keys))
|
||||
}
|
||||
|
||||
pub(crate) fn management_token_permission_mode_and_summary(
|
||||
permissions: Option<&Value>,
|
||||
) -> (&'static str, String) {
|
||||
let keys = match management_token_permission_keys_from_value(permissions) {
|
||||
Ok(Some(keys)) => keys,
|
||||
Ok(None) => return ("legacy_full", "旧版全权限".to_string()),
|
||||
Err(_) => return ("custom", "权限配置异常".to_string()),
|
||||
};
|
||||
let key_set = keys.iter().map(String::as_str).collect::<BTreeSet<_>>();
|
||||
let all = all_assignable_management_token_permissions();
|
||||
let read_only = read_only_management_token_permissions();
|
||||
if all.iter().all(|key| key_set.contains(key.as_str())) {
|
||||
return ("full", "全权限".to_string());
|
||||
}
|
||||
if !keys.is_empty() && keys.iter().all(|key| key.ends_with(":read")) {
|
||||
return ("read_only", "只读".to_string());
|
||||
}
|
||||
if read_only.iter().all(|key| key_set.contains(key.as_str()))
|
||||
&& keys.iter().any(|key| !key.ends_with(":read"))
|
||||
{
|
||||
return ("custom", format!("自定义 {} 项(含全部读取)", keys.len()));
|
||||
}
|
||||
("custom", format!("自定义 {} 项", keys.len()))
|
||||
}
|
||||
|
||||
pub(crate) fn management_token_required_permission(
|
||||
method: &http::Method,
|
||||
decision: &GatewayControlDecision,
|
||||
) -> Option<String> {
|
||||
let signature = decision.auth_endpoint_signature.as_deref()?.trim();
|
||||
let scope = signature.strip_prefix("admin:")?.trim();
|
||||
if scope.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(format!("admin:{scope}:{}", access_for_method(method)))
|
||||
}
|
||||
|
||||
pub(crate) fn validate_management_token_admin_route_permission(
|
||||
method: &http::Method,
|
||||
decision: &GatewayControlDecision,
|
||||
token_permissions: Option<&[String]>,
|
||||
) -> Result<(), ManagementTokenPermissionDenied> {
|
||||
let Some(required_permission) = management_token_required_permission(method, decision) else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(token_permissions) = token_permissions else {
|
||||
return Ok(());
|
||||
};
|
||||
let scope = required_permission
|
||||
.strip_prefix("admin:")
|
||||
.and_then(|value| value.rsplit_once(':').map(|(scope, _)| scope))
|
||||
.unwrap_or_default();
|
||||
let admin_permission = format!("admin:{scope}:admin");
|
||||
if token_permissions
|
||||
.iter()
|
||||
.any(|permission| permission == &required_permission || permission == &admin_permission)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ManagementTokenPermissionDenied {
|
||||
required_permission,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn access_for_method(method: &http::Method) -> &'static str {
|
||||
if matches!(
|
||||
*method,
|
||||
http::Method::GET | http::Method::HEAD | http::Method::OPTIONS
|
||||
) {
|
||||
"read"
|
||||
} else {
|
||||
"write"
|
||||
}
|
||||
}
|
||||
|
||||
fn permission_key(scope: &str, access: &str) -> &'static str {
|
||||
match (scope, access) {
|
||||
("adaptive", "read") => "admin:adaptive:read",
|
||||
("adaptive", "write") => "admin:adaptive:write",
|
||||
("adaptive", "admin") => "admin:adaptive:admin",
|
||||
("announcements", "read") => "admin:announcements:read",
|
||||
("announcements", "write") => "admin:announcements:write",
|
||||
("announcements", "admin") => "admin:announcements:admin",
|
||||
("api_keys", "read") => "admin:api_keys:read",
|
||||
("api_keys", "write") => "admin:api_keys:write",
|
||||
("api_keys", "admin") => "admin:api_keys:admin",
|
||||
("billing", "read") => "admin:billing:read",
|
||||
("billing", "write") => "admin:billing:write",
|
||||
("billing", "admin") => "admin:billing:admin",
|
||||
("endpoints_health", "read") => "admin:endpoints_health:read",
|
||||
("endpoints_health", "write") => "admin:endpoints_health:write",
|
||||
("endpoints_health", "admin") => "admin:endpoints_health:admin",
|
||||
("endpoints_manage", "read") => "admin:endpoints_manage:read",
|
||||
("endpoints_manage", "write") => "admin:endpoints_manage:write",
|
||||
("endpoints_manage", "admin") => "admin:endpoints_manage:admin",
|
||||
("endpoints_rpm", "read") => "admin:endpoints_rpm:read",
|
||||
("endpoints_rpm", "write") => "admin:endpoints_rpm:write",
|
||||
("endpoints_rpm", "admin") => "admin:endpoints_rpm:admin",
|
||||
("gemini_files", "read") => "admin:gemini_files:read",
|
||||
("gemini_files", "write") => "admin:gemini_files:write",
|
||||
("gemini_files", "admin") => "admin:gemini_files:admin",
|
||||
("ldap", "read") => "admin:ldap:read",
|
||||
("ldap", "write") => "admin:ldap:write",
|
||||
("ldap", "admin") => "admin:ldap:admin",
|
||||
("management_tokens", "read") => "admin:management_tokens:read",
|
||||
("management_tokens", "write") => "admin:management_tokens:write",
|
||||
("management_tokens", "admin") => "admin:management_tokens:admin",
|
||||
("models", "read") => "admin:models:read",
|
||||
("models", "write") => "admin:models:write",
|
||||
("models", "admin") => "admin:models:admin",
|
||||
("modules", "read") => "admin:modules:read",
|
||||
("modules", "write") => "admin:modules:write",
|
||||
("modules", "admin") => "admin:modules:admin",
|
||||
("monitoring", "read") => "admin:monitoring:read",
|
||||
("monitoring", "write") => "admin:monitoring:write",
|
||||
("monitoring", "admin") => "admin:monitoring:admin",
|
||||
("oauth", "read") => "admin:oauth:read",
|
||||
("oauth", "write") => "admin:oauth:write",
|
||||
("oauth", "admin") => "admin:oauth:admin",
|
||||
("payments", "read") => "admin:payments:read",
|
||||
("payments", "write") => "admin:payments:write",
|
||||
("payments", "admin") => "admin:payments:admin",
|
||||
("pool", "read") => "admin:pool:read",
|
||||
("pool", "write") => "admin:pool:write",
|
||||
("pool", "admin") => "admin:pool:admin",
|
||||
("provider_ops", "read") => "admin:provider_ops:read",
|
||||
("provider_ops", "write") => "admin:provider_ops:write",
|
||||
("provider_ops", "admin") => "admin:provider_ops:admin",
|
||||
("provider_oauth", "read") => "admin:provider_oauth:read",
|
||||
("provider_oauth", "write") => "admin:provider_oauth:write",
|
||||
("provider_oauth", "admin") => "admin:provider_oauth:admin",
|
||||
("provider_query", "read") => "admin:provider_query:read",
|
||||
("provider_query", "write") => "admin:provider_query:write",
|
||||
("provider_query", "admin") => "admin:provider_query:admin",
|
||||
("provider_strategy", "read") => "admin:provider_strategy:read",
|
||||
("provider_strategy", "write") => "admin:provider_strategy:write",
|
||||
("provider_strategy", "admin") => "admin:provider_strategy:admin",
|
||||
("providers", "read") => "admin:providers:read",
|
||||
("providers", "write") => "admin:providers:write",
|
||||
("providers", "admin") => "admin:providers:admin",
|
||||
("proxy_nodes", "read") => "admin:proxy_nodes:read",
|
||||
("proxy_nodes", "write") => "admin:proxy_nodes:write",
|
||||
("proxy_nodes", "admin") => "admin:proxy_nodes:admin",
|
||||
("security", "read") => "admin:security:read",
|
||||
("security", "write") => "admin:security:write",
|
||||
("security", "admin") => "admin:security:admin",
|
||||
("stats", "read") => "admin:stats:read",
|
||||
("stats", "write") => "admin:stats:write",
|
||||
("stats", "admin") => "admin:stats:admin",
|
||||
("system", "read") => "admin:system:read",
|
||||
("system", "write") => "admin:system:write",
|
||||
("system", "admin") => "admin:system:admin",
|
||||
("usage", "read") => "admin:usage:read",
|
||||
("usage", "write") => "admin:usage:write",
|
||||
("usage", "admin") => "admin:usage:admin",
|
||||
("users", "read") => "admin:users:read",
|
||||
("users", "write") => "admin:users:write",
|
||||
("users", "admin") => "admin:users:admin",
|
||||
("video_tasks", "read") => "admin:video_tasks:read",
|
||||
("video_tasks", "write") => "admin:video_tasks:write",
|
||||
("video_tasks", "admin") => "admin:video_tasks:admin",
|
||||
("wallets", "read") => "admin:wallets:read",
|
||||
("wallets", "write") => "admin:wallets:write",
|
||||
("wallets", "admin") => "admin:wallets:admin",
|
||||
_ => "admin:unknown:read",
|
||||
}
|
||||
}
|
||||
|
||||
fn is_known_management_token_permission_scope(scope: &str) -> bool {
|
||||
PERMISSION_GROUPS.iter().any(|group| group.scope == scope)
|
||||
}
|
||||
|
||||
fn is_assignable_management_token_permission(key: &str) -> bool {
|
||||
management_token_permission_catalog_items()
|
||||
.iter()
|
||||
.any(|item| item.key == key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn catalog_covers_known_admin_auth_scopes() {
|
||||
let scopes = [
|
||||
"adaptive",
|
||||
"announcements",
|
||||
"api_keys",
|
||||
"billing",
|
||||
"endpoints_health",
|
||||
"endpoints_manage",
|
||||
"endpoints_rpm",
|
||||
"gemini_files",
|
||||
"ldap",
|
||||
"management_tokens",
|
||||
"models",
|
||||
"modules",
|
||||
"monitoring",
|
||||
"oauth",
|
||||
"payments",
|
||||
"pool",
|
||||
"provider_oauth",
|
||||
"provider_ops",
|
||||
"provider_query",
|
||||
"provider_strategy",
|
||||
"providers",
|
||||
"proxy_nodes",
|
||||
"security",
|
||||
"stats",
|
||||
"system",
|
||||
"usage",
|
||||
"users",
|
||||
"video_tasks",
|
||||
"wallets",
|
||||
];
|
||||
|
||||
for scope in scopes {
|
||||
assert!(
|
||||
is_known_management_token_permission_scope(scope),
|
||||
"missing scope {scope}"
|
||||
);
|
||||
if scope == "management_tokens" {
|
||||
continue;
|
||||
}
|
||||
for access in ["read", "write", "admin"] {
|
||||
let key = format!("admin:{scope}:{access}");
|
||||
assert!(
|
||||
is_assignable_management_token_permission(&key),
|
||||
"missing permission key {key}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use axum::http::Uri;
|
||||
|
||||
mod auth;
|
||||
mod execute;
|
||||
mod management_token_permissions;
|
||||
mod public;
|
||||
mod route;
|
||||
|
||||
@@ -12,6 +13,12 @@ pub(crate) use auth::{
|
||||
GatewayAdminPrincipalContext, GatewayControlAuthContext, GatewayLocalAuthRejection,
|
||||
};
|
||||
pub(crate) use execute::{allows_control_execute_emergency, maybe_execute_via_control};
|
||||
pub(crate) use management_token_permissions::{
|
||||
all_assignable_management_token_permissions, management_token_permission_catalog_payload,
|
||||
management_token_permission_keys_from_value, management_token_permission_mode_and_summary,
|
||||
management_token_required_permission, normalize_assignable_management_token_permissions,
|
||||
validate_management_token_admin_route_permission,
|
||||
};
|
||||
pub(crate) use public::{resolve_public_request_context, GatewayPublicRequestContext};
|
||||
#[cfg(test)]
|
||||
pub(crate) use route::classify_control_route;
|
||||
|
||||
@@ -8,6 +8,20 @@ pub(super) fn classify_admin_basic_family_route(
|
||||
normalized_path_no_trailing: &str,
|
||||
) -> Option<ClassifiedRoute> {
|
||||
if method == http::Method::GET
|
||||
&& matches!(
|
||||
normalized_path,
|
||||
"/api/admin/management-tokens/permissions/catalog"
|
||||
| "/api/admin/management-tokens/permissions/catalog/"
|
||||
)
|
||||
{
|
||||
Some(classified(
|
||||
"admin_proxy",
|
||||
"management_tokens_manage",
|
||||
"permissions_catalog",
|
||||
"admin:management_tokens",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::GET
|
||||
&& matches!(
|
||||
normalized_path,
|
||||
"/api/admin/management-tokens" | "/api/admin/management-tokens/"
|
||||
@@ -20,6 +34,19 @@ pub(super) fn classify_admin_basic_family_route(
|
||||
"admin:management_tokens",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::POST
|
||||
&& matches!(
|
||||
normalized_path,
|
||||
"/api/admin/management-tokens" | "/api/admin/management-tokens/"
|
||||
)
|
||||
{
|
||||
Some(classified(
|
||||
"admin_proxy",
|
||||
"management_tokens_manage",
|
||||
"create_token",
|
||||
"admin:management_tokens",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::GET
|
||||
&& normalized_path.starts_with("/api/admin/management-tokens/")
|
||||
{
|
||||
@@ -30,6 +57,16 @@ pub(super) fn classify_admin_basic_family_route(
|
||||
"admin:management_tokens",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::PUT
|
||||
&& normalized_path.starts_with("/api/admin/management-tokens/")
|
||||
{
|
||||
Some(classified(
|
||||
"admin_proxy",
|
||||
"management_tokens_manage",
|
||||
"update_token",
|
||||
"admin:management_tokens",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::DELETE
|
||||
&& normalized_path.starts_with("/api/admin/management-tokens/")
|
||||
{
|
||||
@@ -40,6 +77,17 @@ pub(super) fn classify_admin_basic_family_route(
|
||||
"admin:management_tokens",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::POST
|
||||
&& normalized_path.starts_with("/api/admin/management-tokens/")
|
||||
&& normalized_path.ends_with("/regenerate")
|
||||
{
|
||||
Some(classified(
|
||||
"admin_proxy",
|
||||
"management_tokens_manage",
|
||||
"regenerate_token",
|
||||
"admin:management_tokens",
|
||||
false,
|
||||
))
|
||||
} else if method == http::Method::PATCH
|
||||
&& normalized_path.starts_with("/api/admin/management-tokens/")
|
||||
&& normalized_path.ends_with("/status")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use http::Uri;
|
||||
|
||||
use crate::control::management_token_required_permission;
|
||||
|
||||
use super::{classify_control_route, headers};
|
||||
|
||||
#[test]
|
||||
@@ -497,6 +499,58 @@ fn classifies_admin_list_management_tokens_as_admin_proxy_route() {
|
||||
assert!(!decision.is_execution_runtime_candidate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_admin_management_token_write_routes_and_permission_catalog() {
|
||||
let headers = headers(&[]);
|
||||
let cases = [
|
||||
(
|
||||
http::Method::GET,
|
||||
"/api/admin/management-tokens/permissions/catalog",
|
||||
"permissions_catalog",
|
||||
"admin:management_tokens:read",
|
||||
),
|
||||
(
|
||||
http::Method::POST,
|
||||
"/api/admin/management-tokens",
|
||||
"create_token",
|
||||
"admin:management_tokens:write",
|
||||
),
|
||||
(
|
||||
http::Method::PUT,
|
||||
"/api/admin/management-tokens/token-123",
|
||||
"update_token",
|
||||
"admin:management_tokens:write",
|
||||
),
|
||||
(
|
||||
http::Method::POST,
|
||||
"/api/admin/management-tokens/token-123/regenerate",
|
||||
"regenerate_token",
|
||||
"admin:management_tokens:write",
|
||||
),
|
||||
];
|
||||
|
||||
for (method, path, expected_kind, expected_permission) in cases {
|
||||
let uri: Uri = path.parse().expect("uri should parse");
|
||||
let decision =
|
||||
classify_control_route(&method, &uri, &headers).expect("route should classify");
|
||||
|
||||
assert_eq!(decision.route_class.as_deref(), Some("admin_proxy"));
|
||||
assert_eq!(
|
||||
decision.route_family.as_deref(),
|
||||
Some("management_tokens_manage")
|
||||
);
|
||||
assert_eq!(decision.route_kind.as_deref(), Some(expected_kind));
|
||||
assert_eq!(
|
||||
decision.auth_endpoint_signature.as_deref(),
|
||||
Some("admin:management_tokens")
|
||||
);
|
||||
assert_eq!(
|
||||
management_token_required_permission(&method, &decision).as_deref(),
|
||||
Some(expected_permission)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_admin_ldap_config_get_as_admin_proxy_route() {
|
||||
let headers = headers(&[]);
|
||||
|
||||
Reference in New Issue
Block a user