fix(saas): 统一权限体系 — check_permission 辅助函数 + admin:full 超级权限
- 新增 check_permission() 统一权限检查,admin:full 自动通过所有检查 - 统一种子角色权限名称与 handler 检查一致 (provider:manage, model:manage, config:write) - super_admin 拥有 admin:full + 所有模块管理权限 - 全部 handler 迁移到 check_permission(),消除手动 contains 检查
This commit is contained in:
@@ -5,16 +5,13 @@ use axum::{
|
|||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::error::{SaasError, SaasResult};
|
use crate::error::SaasResult;
|
||||||
use crate::auth::types::AuthContext;
|
use crate::auth::types::AuthContext;
|
||||||
use crate::auth::handlers::log_operation;
|
use crate::auth::handlers::{log_operation, check_permission};
|
||||||
use super::{types::*, service};
|
use super::{types::*, service};
|
||||||
|
|
||||||
fn require_admin(ctx: &AuthContext) -> SaasResult<()> {
|
fn require_admin(ctx: &AuthContext) -> SaasResult<()> {
|
||||||
if !ctx.permissions.contains(&"account:admin".to_string()) {
|
check_permission(ctx, "account:admin")
|
||||||
return Err(SaasError::Forbidden("需要 account:admin 权限".into()));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/v1/accounts (admin only)
|
/// GET /api/v1/accounts (admin only)
|
||||||
|
|||||||
@@ -152,6 +152,17 @@ async fn get_role_permissions(db: &sqlx::SqlitePool, role: &str) -> SaasResult<V
|
|||||||
Ok(permissions)
|
Ok(permissions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 检查权限 (admin:full 自动通过所有检查)
|
||||||
|
pub fn check_permission(ctx: &AuthContext, permission: &str) -> SaasResult<()> {
|
||||||
|
if ctx.permissions.contains(&"admin:full".to_string()) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if !ctx.permissions.contains(&permission.to_string()) {
|
||||||
|
return Err(SaasError::Forbidden(format!("需要 {} 权限", permission)));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 记录操作日志
|
/// 记录操作日志
|
||||||
pub async fn log_operation(
|
pub async fn log_operation(
|
||||||
db: &sqlx::SqlitePool,
|
db: &sqlx::SqlitePool,
|
||||||
|
|||||||
@@ -200,8 +200,8 @@ CREATE INDEX IF NOT EXISTS idx_sync_account ON config_sync_log(account_id);
|
|||||||
const SEED_ROLES: &str = r#"
|
const SEED_ROLES: &str = r#"
|
||||||
INSERT OR IGNORE INTO roles (id, name, description, permissions, is_system, created_at, updated_at)
|
INSERT OR IGNORE INTO roles (id, name, description, permissions, is_system, created_at, updated_at)
|
||||||
VALUES
|
VALUES
|
||||||
('super_admin', '超级管理员', '拥有所有权限', '["admin:full"]', 1, datetime('now'), datetime('now')),
|
('super_admin', '超级管理员', '拥有所有权限', '["admin:full","account:admin","provider:manage","model:manage","relay:admin","config:write"]', 1, datetime('now'), datetime('now')),
|
||||||
('admin', '管理员', '管理账号和配置', '["account:read","account:write","model:read","model:write","relay:use","relay:admin","config:read","config:write"]', 1, datetime('now'), datetime('now')),
|
('admin', '管理员', '管理账号和配置', '["account:read","account:admin","provider:manage","model:read","model:manage","relay:use","relay:admin","config:read","config:write"]', 1, datetime('now'), datetime('now')),
|
||||||
('user', '普通用户', '基础使用权限', '["model:read","relay:use","config:read"]', 1, datetime('now'), datetime('now'));
|
('user', '普通用户', '基础使用权限', '["model:read","relay:use","config:read"]', 1, datetime('now'), datetime('now'));
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use axum::{
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::error::SaasResult;
|
use crate::error::SaasResult;
|
||||||
use crate::auth::types::AuthContext;
|
use crate::auth::types::AuthContext;
|
||||||
|
use crate::auth::handlers::check_permission;
|
||||||
use super::{types::*, service};
|
use super::{types::*, service};
|
||||||
|
|
||||||
/// GET /api/v1/config/items?category=xxx&source=xxx
|
/// GET /api/v1/config/items?category=xxx&source=xxx
|
||||||
@@ -33,9 +34,7 @@ pub async fn create_config_item(
|
|||||||
Extension(ctx): Extension<AuthContext>,
|
Extension(ctx): Extension<AuthContext>,
|
||||||
Json(req): Json<CreateConfigItemRequest>,
|
Json(req): Json<CreateConfigItemRequest>,
|
||||||
) -> SaasResult<(StatusCode, Json<ConfigItemInfo>)> {
|
) -> SaasResult<(StatusCode, Json<ConfigItemInfo>)> {
|
||||||
if !ctx.permissions.contains(&"config:manage".to_string()) {
|
check_permission(&ctx, "config:write")?;
|
||||||
return Err(crate::error::SaasError::Forbidden("需要 config:manage 权限".into()));
|
|
||||||
}
|
|
||||||
let item = service::create_config_item(&state.db, &req).await?;
|
let item = service::create_config_item(&state.db, &req).await?;
|
||||||
Ok((StatusCode::CREATED, Json(item)))
|
Ok((StatusCode::CREATED, Json(item)))
|
||||||
}
|
}
|
||||||
@@ -47,9 +46,7 @@ pub async fn update_config_item(
|
|||||||
Extension(ctx): Extension<AuthContext>,
|
Extension(ctx): Extension<AuthContext>,
|
||||||
Json(req): Json<UpdateConfigItemRequest>,
|
Json(req): Json<UpdateConfigItemRequest>,
|
||||||
) -> SaasResult<Json<ConfigItemInfo>> {
|
) -> SaasResult<Json<ConfigItemInfo>> {
|
||||||
if !ctx.permissions.contains(&"config:manage".to_string()) {
|
check_permission(&ctx, "config:write")?;
|
||||||
return Err(crate::error::SaasError::Forbidden("需要 config:manage 权限".into()));
|
|
||||||
}
|
|
||||||
service::update_config_item(&state.db, &id, &req).await.map(Json)
|
service::update_config_item(&state.db, &id, &req).await.map(Json)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,9 +56,7 @@ pub async fn delete_config_item(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Extension(ctx): Extension<AuthContext>,
|
Extension(ctx): Extension<AuthContext>,
|
||||||
) -> SaasResult<Json<serde_json::Value>> {
|
) -> SaasResult<Json<serde_json::Value>> {
|
||||||
if !ctx.permissions.contains(&"config:manage".to_string()) {
|
check_permission(&ctx, "config:write")?;
|
||||||
return Err(crate::error::SaasError::Forbidden("需要 config:manage 权限".into()));
|
|
||||||
}
|
|
||||||
service::delete_config_item(&state.db, &id).await?;
|
service::delete_config_item(&state.db, &id).await?;
|
||||||
Ok(Json(serde_json::json!({"ok": true})))
|
Ok(Json(serde_json::json!({"ok": true})))
|
||||||
}
|
}
|
||||||
@@ -79,9 +74,7 @@ pub async fn seed_config(
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Extension(ctx): Extension<AuthContext>,
|
Extension(ctx): Extension<AuthContext>,
|
||||||
) -> SaasResult<Json<serde_json::Value>> {
|
) -> SaasResult<Json<serde_json::Value>> {
|
||||||
if !ctx.permissions.contains(&"config:manage".to_string()) {
|
check_permission(&ctx, "config:write")?;
|
||||||
return Err(crate::error::SaasError::Forbidden("需要 config:manage 权限".into()));
|
|
||||||
}
|
|
||||||
let count = service::seed_default_config_items(&state.db).await?;
|
let count = service::seed_default_config_items(&state.db).await?;
|
||||||
Ok(Json(serde_json::json!({"created": count})))
|
Ok(Json(serde_json::json!({"created": count})))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ use axum::{
|
|||||||
http::StatusCode, Json,
|
http::StatusCode, Json,
|
||||||
};
|
};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::error::{SaasError, SaasResult};
|
use crate::error::SaasResult;
|
||||||
use crate::auth::types::AuthContext;
|
use crate::auth::types::AuthContext;
|
||||||
use crate::auth::handlers::log_operation;
|
use crate::auth::handlers::{log_operation, check_permission};
|
||||||
use super::{types::*, service};
|
use super::{types::*, service};
|
||||||
|
|
||||||
// ============ Providers ============
|
// ============ Providers ============
|
||||||
@@ -35,9 +35,7 @@ pub async fn create_provider(
|
|||||||
Extension(ctx): Extension<AuthContext>,
|
Extension(ctx): Extension<AuthContext>,
|
||||||
Json(req): Json<CreateProviderRequest>,
|
Json(req): Json<CreateProviderRequest>,
|
||||||
) -> SaasResult<(StatusCode, Json<ProviderInfo>)> {
|
) -> SaasResult<(StatusCode, Json<ProviderInfo>)> {
|
||||||
if !ctx.permissions.contains(&"provider:manage".to_string()) {
|
check_permission(&ctx, "provider:manage")?;
|
||||||
return Err(SaasError::Forbidden("需要 provider:manage 权限".into()));
|
|
||||||
}
|
|
||||||
let provider = service::create_provider(&state.db, &req).await?;
|
let provider = service::create_provider(&state.db, &req).await?;
|
||||||
log_operation(&state.db, &ctx.account_id, "provider.create", "provider", &provider.id,
|
log_operation(&state.db, &ctx.account_id, "provider.create", "provider", &provider.id,
|
||||||
Some(serde_json::json!({"name": &req.name})), None).await?;
|
Some(serde_json::json!({"name": &req.name})), None).await?;
|
||||||
@@ -51,9 +49,7 @@ pub async fn update_provider(
|
|||||||
Extension(ctx): Extension<AuthContext>,
|
Extension(ctx): Extension<AuthContext>,
|
||||||
Json(req): Json<UpdateProviderRequest>,
|
Json(req): Json<UpdateProviderRequest>,
|
||||||
) -> SaasResult<Json<ProviderInfo>> {
|
) -> SaasResult<Json<ProviderInfo>> {
|
||||||
if !ctx.permissions.contains(&"provider:manage".to_string()) {
|
check_permission(&ctx, "provider:manage")?;
|
||||||
return Err(SaasError::Forbidden("需要 provider:manage 权限".into()));
|
|
||||||
}
|
|
||||||
let provider = service::update_provider(&state.db, &id, &req).await?;
|
let provider = service::update_provider(&state.db, &id, &req).await?;
|
||||||
log_operation(&state.db, &ctx.account_id, "provider.update", "provider", &id, None, None).await?;
|
log_operation(&state.db, &ctx.account_id, "provider.update", "provider", &id, None, None).await?;
|
||||||
Ok(Json(provider))
|
Ok(Json(provider))
|
||||||
@@ -65,9 +61,7 @@ pub async fn delete_provider(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Extension(ctx): Extension<AuthContext>,
|
Extension(ctx): Extension<AuthContext>,
|
||||||
) -> SaasResult<Json<serde_json::Value>> {
|
) -> SaasResult<Json<serde_json::Value>> {
|
||||||
if !ctx.permissions.contains(&"provider:manage".to_string()) {
|
check_permission(&ctx, "provider:manage")?;
|
||||||
return Err(SaasError::Forbidden("需要 provider:manage 权限".into()));
|
|
||||||
}
|
|
||||||
service::delete_provider(&state.db, &id).await?;
|
service::delete_provider(&state.db, &id).await?;
|
||||||
log_operation(&state.db, &ctx.account_id, "provider.delete", "provider", &id, None, None).await?;
|
log_operation(&state.db, &ctx.account_id, "provider.delete", "provider", &id, None, None).await?;
|
||||||
Ok(Json(serde_json::json!({"ok": true})))
|
Ok(Json(serde_json::json!({"ok": true})))
|
||||||
@@ -100,9 +94,7 @@ pub async fn create_model(
|
|||||||
Extension(ctx): Extension<AuthContext>,
|
Extension(ctx): Extension<AuthContext>,
|
||||||
Json(req): Json<CreateModelRequest>,
|
Json(req): Json<CreateModelRequest>,
|
||||||
) -> SaasResult<(StatusCode, Json<ModelInfo>)> {
|
) -> SaasResult<(StatusCode, Json<ModelInfo>)> {
|
||||||
if !ctx.permissions.contains(&"model:manage".to_string()) {
|
check_permission(&ctx, "model:manage")?;
|
||||||
return Err(SaasError::Forbidden("需要 model:manage 权限".into()));
|
|
||||||
}
|
|
||||||
let model = service::create_model(&state.db, &req).await?;
|
let model = service::create_model(&state.db, &req).await?;
|
||||||
log_operation(&state.db, &ctx.account_id, "model.create", "model", &model.id,
|
log_operation(&state.db, &ctx.account_id, "model.create", "model", &model.id,
|
||||||
Some(serde_json::json!({"model_id": &req.model_id, "provider_id": &req.provider_id})), None).await?;
|
Some(serde_json::json!({"model_id": &req.model_id, "provider_id": &req.provider_id})), None).await?;
|
||||||
@@ -116,9 +108,7 @@ pub async fn update_model(
|
|||||||
Extension(ctx): Extension<AuthContext>,
|
Extension(ctx): Extension<AuthContext>,
|
||||||
Json(req): Json<UpdateModelRequest>,
|
Json(req): Json<UpdateModelRequest>,
|
||||||
) -> SaasResult<Json<ModelInfo>> {
|
) -> SaasResult<Json<ModelInfo>> {
|
||||||
if !ctx.permissions.contains(&"model:manage".to_string()) {
|
check_permission(&ctx, "model:manage")?;
|
||||||
return Err(SaasError::Forbidden("需要 model:manage 权限".into()));
|
|
||||||
}
|
|
||||||
let model = service::update_model(&state.db, &id, &req).await?;
|
let model = service::update_model(&state.db, &id, &req).await?;
|
||||||
log_operation(&state.db, &ctx.account_id, "model.update", "model", &id, None, None).await?;
|
log_operation(&state.db, &ctx.account_id, "model.update", "model", &id, None, None).await?;
|
||||||
Ok(Json(model))
|
Ok(Json(model))
|
||||||
@@ -130,9 +120,7 @@ pub async fn delete_model(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
Extension(ctx): Extension<AuthContext>,
|
Extension(ctx): Extension<AuthContext>,
|
||||||
) -> SaasResult<Json<serde_json::Value>> {
|
) -> SaasResult<Json<serde_json::Value>> {
|
||||||
if !ctx.permissions.contains(&"model:manage".to_string()) {
|
check_permission(&ctx, "model:manage")?;
|
||||||
return Err(SaasError::Forbidden("需要 model:manage 权限".into()));
|
|
||||||
}
|
|
||||||
service::delete_model(&state.db, &id).await?;
|
service::delete_model(&state.db, &id).await?;
|
||||||
log_operation(&state.db, &ctx.account_id, "model.delete", "model", &id, None, None).await?;
|
log_operation(&state.db, &ctx.account_id, "model.delete", "model", &id, None, None).await?;
|
||||||
Ok(Json(serde_json::json!({"ok": true})))
|
Ok(Json(serde_json::json!({"ok": true})))
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use axum::{
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::error::{SaasError, SaasResult};
|
use crate::error::{SaasError, SaasResult};
|
||||||
use crate::auth::types::AuthContext;
|
use crate::auth::types::AuthContext;
|
||||||
use crate::auth::handlers::log_operation;
|
use crate::auth::handlers::{log_operation, check_permission};
|
||||||
use crate::model_config::service as model_service;
|
use crate::model_config::service as model_service;
|
||||||
use super::{types::*, service};
|
use super::{types::*, service};
|
||||||
|
|
||||||
@@ -21,10 +21,7 @@ pub async fn chat_completions(
|
|||||||
_headers: HeaderMap,
|
_headers: HeaderMap,
|
||||||
Json(req): Json<serde_json::Value>,
|
Json(req): Json<serde_json::Value>,
|
||||||
) -> SaasResult<Response> {
|
) -> SaasResult<Response> {
|
||||||
// 检查 relay:use 权限
|
check_permission(&ctx, "relay:use")?;
|
||||||
if !ctx.permissions.contains(&"relay:use".to_string()) {
|
|
||||||
return Err(SaasError::Forbidden("需要 relay:use 权限".into()));
|
|
||||||
}
|
|
||||||
|
|
||||||
let model_name = req.get("model")
|
let model_name = req.get("model")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
@@ -129,8 +126,8 @@ pub async fn get_task(
|
|||||||
) -> SaasResult<Json<RelayTaskInfo>> {
|
) -> SaasResult<Json<RelayTaskInfo>> {
|
||||||
let task = service::get_relay_task(&state.db, &id).await?;
|
let task = service::get_relay_task(&state.db, &id).await?;
|
||||||
// 只允许查看自己的任务 (admin 可查看全部)
|
// 只允许查看自己的任务 (admin 可查看全部)
|
||||||
if task.account_id != ctx.account_id && !ctx.permissions.contains(&"relay:admin".to_string()) {
|
if task.account_id != ctx.account_id {
|
||||||
return Err(SaasError::Forbidden("无权查看此任务".into()));
|
check_permission(&ctx, "relay:admin")?;
|
||||||
}
|
}
|
||||||
Ok(Json(task))
|
Ok(Json(task))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user