fix(sql): fix sql script

This commit is contained in:
2026-01-31 11:11:55 +08:00
parent ce12b997f4
commit d071e1a27d
32 changed files with 1687 additions and 133 deletions

View File

@@ -1,4 +1,5 @@
use crate::models::{CreateUserRequest, LoginRequest, LoginResponse, User};
use crate::utils::authz::filter_permissions_by_enabled_apps;
use crate::utils::{hash_password, sign, verify_password};
use common_telemetry::AppError;
use rand::RngCore;
@@ -143,8 +144,29 @@ impl AuthService {
.fetch_all(&self.pool)
.await?;
let (enabled_apps, apps_version) = sqlx::query_as::<_, (Vec<String>, i32)>(
r#"
SELECT enabled_apps, version
FROM tenant_entitlements
WHERE tenant_id = $1
"#,
)
.bind(user.tenant_id)
.fetch_optional(&self.pool)
.await?
.unwrap_or_else(|| (vec![], 0));
let permissions = filter_permissions_by_enabled_apps(permissions, &enabled_apps);
// 3. 签发 Access Token
let access_token = sign(user.id, user.tenant_id, roles, permissions)?;
let access_token = sign(
user.id,
user.tenant_id,
roles,
permissions,
enabled_apps,
apps_version,
)?;
// 4. 生成 Refresh Token
let mut refresh_bytes = [0u8; 32];
@@ -197,11 +219,14 @@ impl AuthService {
sqlx::query(
r#"
INSERT INTO role_permissions (role_id, permission_id)
SELECT $1, p.id FROM permissions p
SELECT $1, p.id
FROM permissions p
WHERE ($2::uuid = '00000000-0000-0000-0000-000000000001' OR p.code NOT LIKE 'iam:%')
ON CONFLICT DO NOTHING
"#,
)
.bind(role_id)
.bind(tenant_id)
.execute(&mut **tx)
.await?;

View File

@@ -1,3 +1,4 @@
use crate::utils::authz::filter_permissions_by_enabled_apps;
use common_telemetry::AppError;
use sqlx::PgPool;
use tracing::instrument;
@@ -34,6 +35,13 @@ impl AuthorizationService {
tenant_id: Uuid,
user_id: Uuid,
) -> Result<Vec<String>, AppError> {
let enabled_apps: Vec<String> =
sqlx::query_scalar("SELECT enabled_apps FROM tenant_entitlements WHERE tenant_id = $1")
.bind(tenant_id)
.fetch_optional(&self.pool)
.await?
.unwrap_or_default();
let query = r#"
SELECT DISTINCT p.code
FROM permissions p
@@ -47,7 +55,7 @@ impl AuthorizationService {
.bind(user_id)
.fetch_all(&self.pool)
.await?;
Ok(rows)
Ok(filter_permissions_by_enabled_apps(rows, &enabled_apps))
}
#[instrument(skip(self))]
@@ -76,4 +84,38 @@ impl AuthorizationService {
Err(AppError::PermissionDenied(permission_code.to_string()))
}
}
#[instrument(skip(self))]
pub async fn list_platform_permissions_for_user(
&self,
user_id: Uuid,
) -> Result<Vec<String>, AppError> {
let query = r#"
SELECT DISTINCT p.code
FROM permissions p
JOIN role_permissions rp ON rp.permission_id = p.id
JOIN user_roles ur ON ur.role_id = rp.role_id
JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = $1 AND r.is_system = TRUE
"#;
let rows = sqlx::query_scalar::<_, String>(query)
.bind(user_id)
.fetch_all(&self.pool)
.await?;
Ok(rows)
}
#[instrument(skip(self))]
pub async fn require_platform_permission(
&self,
user_id: Uuid,
permission_code: &str,
) -> Result<(), AppError> {
let permissions = self.list_platform_permissions_for_user(user_id).await?;
if permissions.iter().any(|p| p == permission_code) {
Ok(())
} else {
Err(AppError::PermissionDenied(permission_code.to_string()))
}
}
}

View File

@@ -56,4 +56,123 @@ impl RoleService {
.await
.map_err(|e| AppError::DbError(e))
}
#[instrument(skip(self, role_ids))]
pub async fn list_roles_by_ids(
&self,
tenant_id: Uuid,
role_ids: &[Uuid],
) -> Result<Vec<Role>, AppError> {
if role_ids.is_empty() {
return Ok(vec![]);
}
let query = "SELECT id, tenant_id, name, description FROM roles WHERE tenant_id = $1 AND id = ANY($2)";
sqlx::query_as::<_, Role>(query)
.bind(tenant_id)
.bind(role_ids)
.fetch_all(&self.pool)
.await
.map_err(|e| AppError::DbError(e))
}
#[instrument(skip(self))]
pub async fn list_roles_for_user(
&self,
tenant_id: Uuid,
target_user_id: Uuid,
) -> Result<Vec<Role>, AppError> {
let query = r#"
SELECT r.id, r.tenant_id, r.name, r.description
FROM roles r
JOIN user_roles ur ON ur.role_id = r.id
WHERE r.tenant_id = $1 AND ur.user_id = $2
"#;
sqlx::query_as::<_, Role>(query)
.bind(tenant_id)
.bind(target_user_id)
.fetch_all(&self.pool)
.await
.map_err(|e| AppError::DbError(e))
}
#[instrument(skip(self, role_ids))]
pub async fn set_roles_for_user(
&self,
tenant_id: Uuid,
target_user_id: Uuid,
role_ids: Vec<Uuid>,
) -> Result<Vec<Role>, AppError> {
let unique: Vec<Uuid> = {
let mut s = std::collections::HashSet::new();
let mut out = Vec::new();
for id in role_ids {
if s.insert(id) {
out.push(id);
}
}
out
};
let mut tx = self.pool.begin().await?;
let exists: Option<Uuid> = sqlx::query_scalar(
"SELECT id FROM users WHERE tenant_id = $1 AND id = $2",
)
.bind(tenant_id)
.bind(target_user_id)
.fetch_optional(&mut *tx)
.await?;
if exists.is_none() {
return Err(AppError::NotFound("User not found".into()));
}
let roles = if unique.is_empty() {
vec![]
} else {
let found = sqlx::query_as::<_, Role>(
"SELECT id, tenant_id, name, description FROM roles WHERE tenant_id = $1 AND id = ANY($2)",
)
.bind(tenant_id)
.bind(&unique)
.fetch_all(&mut *tx)
.await
.map_err(AppError::DbError)?;
if found.len() != unique.len() {
return Err(AppError::BadRequest("Invalid role_ids".into()));
}
found
};
sqlx::query(
r#"
DELETE FROM user_roles ur
USING roles r
WHERE ur.role_id = r.id
AND r.tenant_id = $1
AND ur.user_id = $2
"#,
)
.bind(tenant_id)
.bind(target_user_id)
.execute(&mut *tx)
.await?;
if !unique.is_empty() {
sqlx::query(
r#"
INSERT INTO user_roles (user_id, role_id)
SELECT $1, UNNEST($2::uuid[])
ON CONFLICT DO NOTHING
"#,
)
.bind(target_user_id)
.bind(&unique)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(roles)
}
}

View File

@@ -1,21 +1,35 @@
use crate::models::{
CreateTenantRequest, Tenant, UpdateTenantRequest, UpdateTenantStatusRequest,
};
use crate::models::{CreateTenantRequest, Tenant, UpdateTenantRequest, UpdateTenantStatusRequest};
use common_telemetry::AppError;
use serde_json::Value;
use sqlx::PgPool;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing::instrument;
use uuid::Uuid;
#[derive(Clone)]
struct EnabledAppsCacheEntry {
enabled_apps: Vec<String>,
version: i32,
updated_at: String,
expires_at: Instant,
}
#[derive(Clone)]
pub struct TenantService {
pool: PgPool,
enabled_apps_cache: Arc<RwLock<HashMap<Uuid, EnabledAppsCacheEntry>>>,
}
impl TenantService {
/// 创建租户服务实例。
pub fn new(pool: PgPool) -> Self {
Self { pool }
Self {
pool,
enabled_apps_cache: Arc::new(RwLock::new(HashMap::new())),
}
}
#[instrument(skip(self, req))]
@@ -31,17 +45,43 @@ impl TenantService {
/// 异常:
/// - 数据库写入失败(如连接异常、约束失败等)
pub async fn create_tenant(&self, req: CreateTenantRequest) -> Result<Tenant, AppError> {
let config = req.config.unwrap_or_else(|| Value::Object(Default::default()));
let mut config = req
.config
.unwrap_or_else(|| Value::Object(Default::default()));
if !config.is_object() {
config = Value::Object(Default::default());
}
if let Some(obj) = config.as_object_mut() {
obj.insert("enabled_apps".to_string(), Value::Array(vec![]));
obj.insert(
"enabled_apps_version".to_string(),
Value::Number(serde_json::Number::from(0)),
);
}
let query = r#"
INSERT INTO tenants (name, status, config)
VALUES ($1, 'active', $2)
RETURNING id, name, status, config
"#;
let mut tx = self.pool.begin().await?;
let tenant = sqlx::query_as::<_, Tenant>(query)
.bind(req.name)
.bind(config)
.fetch_one(&self.pool)
.fetch_one(&mut *tx)
.await?;
sqlx::query(
r#"
INSERT INTO tenant_entitlements (tenant_id, enabled_apps, version)
VALUES ($1, '{}'::text[], 0)
ON CONFLICT (tenant_id) DO NOTHING
"#,
)
.bind(tenant.id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(tenant)
}
@@ -131,4 +171,228 @@ impl TenantService {
}
Ok(())
}
#[instrument(skip(self))]
pub async fn get_enabled_apps(
&self,
tenant_id: Uuid,
) -> Result<(Vec<String>, i32, String), AppError> {
let now = Instant::now();
if let Some(hit) = self
.enabled_apps_cache
.read()
.await
.get(&tenant_id)
.cloned()
{
if hit.expires_at > now {
return Ok((hit.enabled_apps, hit.version, hit.updated_at));
}
}
let row = sqlx::query_as::<_, (Vec<String>, i32, chrono::DateTime<chrono::Utc>)>(
r#"
SELECT enabled_apps, version, updated_at
FROM tenant_entitlements
WHERE tenant_id = $1
"#,
)
.bind(tenant_id)
.fetch_optional(&self.pool)
.await?;
let (enabled_apps, version, updated_at) = match row {
Some((apps, v, ts)) => (apps, v, ts.to_rfc3339()),
None => {
let exists: Option<Uuid> =
sqlx::query_scalar("SELECT id FROM tenants WHERE id = $1")
.bind(tenant_id)
.fetch_optional(&self.pool)
.await?;
if exists.is_none() {
return Err(AppError::NotFound("Tenant not found".into()));
}
let ts = chrono::Utc::now().to_rfc3339();
(vec![], 0, ts)
}
};
let ttl = Duration::from_secs(60);
self.enabled_apps_cache.write().await.insert(
tenant_id,
EnabledAppsCacheEntry {
enabled_apps: enabled_apps.clone(),
version,
updated_at: updated_at.clone(),
expires_at: now + ttl,
},
);
Ok((enabled_apps, version, updated_at))
}
#[instrument(skip(self, enabled_apps))]
pub async fn set_enabled_apps(
&self,
tenant_id: Uuid,
enabled_apps: Vec<String>,
expected_version: Option<i32>,
actor_user_id: Uuid,
) -> Result<(Vec<String>, i32, String), AppError> {
let normalized = normalize_apps(enabled_apps);
self.validate_apps_exist(&normalized).await?;
let mut tx = self.pool.begin().await?;
let current = sqlx::query_as::<_, (Vec<String>, i32)>(
r#"
SELECT enabled_apps, version
FROM tenant_entitlements
WHERE tenant_id = $1
FOR UPDATE
"#,
)
.bind(tenant_id)
.fetch_optional(&mut *tx)
.await?;
if current.is_none() {
let exists: Option<Uuid> = sqlx::query_scalar("SELECT id FROM tenants WHERE id = $1")
.bind(tenant_id)
.fetch_optional(&mut *tx)
.await?;
if exists.is_none() {
return Err(AppError::NotFound("Tenant not found".into()));
}
sqlx::query(
r#"
INSERT INTO tenant_entitlements (tenant_id, enabled_apps, version)
VALUES ($1, '{}'::text[], 0)
ON CONFLICT (tenant_id) DO NOTHING
"#,
)
.bind(tenant_id)
.execute(&mut *tx)
.await?;
}
let (before_apps, before_version) = current.unwrap_or_else(|| (vec![], 0));
if let Some(ev) = expected_version {
if ev != before_version {
return Err(AppError::AlreadyExists(
"enabled_apps:version_conflict".into(),
));
}
}
let (new_version, updated_at): (i32, chrono::DateTime<chrono::Utc>) = sqlx::query_as(
r#"
UPDATE tenant_entitlements
SET enabled_apps = $1,
version = version + 1,
updated_at = NOW()
WHERE tenant_id = $2
RETURNING version, updated_at
"#,
)
.bind(&normalized)
.bind(tenant_id)
.fetch_one(&mut *tx)
.await?;
sqlx::query(
r#"
UPDATE tenants
SET config =
jsonb_set(
jsonb_set(COALESCE(config, '{}'::jsonb), '{enabled_apps}', to_jsonb($1::text[]), true),
'{enabled_apps_version}', to_jsonb($2::int), true
),
updated_at = NOW()
WHERE id = $3
"#,
)
.bind(&normalized)
.bind(new_version)
.bind(tenant_id)
.execute(&mut *tx)
.await?;
sqlx::query(
r#"
INSERT INTO tenant_enabled_apps_history (tenant_id, version, enabled_apps, actor_user_id)
VALUES ($1, $2, $3, $4)
"#,
)
.bind(tenant_id)
.bind(new_version)
.bind(&normalized)
.bind(actor_user_id)
.execute(&mut *tx)
.await?;
sqlx::query(
r#"
INSERT INTO audit_logs (tenant_id, user_id, action, resource, status, details)
VALUES ($1, $2, 'tenant.enabled_apps.update', 'tenant', 'allow', $3)
"#,
)
.bind(tenant_id)
.bind(actor_user_id)
.bind(serde_json::json!({
"before": { "enabled_apps": before_apps, "version": before_version },
"after": { "enabled_apps": normalized, "version": new_version }
}))
.execute(&mut *tx)
.await?;
tx.commit().await?;
let ttl = Duration::from_secs(60);
let now = Instant::now();
let updated_at = updated_at.to_rfc3339();
self.enabled_apps_cache.write().await.insert(
tenant_id,
EnabledAppsCacheEntry {
enabled_apps: normalized.clone(),
version: new_version,
updated_at: updated_at.clone(),
expires_at: now + ttl,
},
);
Ok((normalized, new_version, updated_at))
}
async fn validate_apps_exist(&self, enabled_apps: &[String]) -> Result<(), AppError> {
if enabled_apps.is_empty() {
return Ok(());
}
let rows: Vec<String> = sqlx::query_scalar("SELECT id FROM apps WHERE id = ANY($1)")
.bind(enabled_apps)
.fetch_all(&self.pool)
.await?;
let found: HashSet<String> = rows.into_iter().collect();
for app in enabled_apps {
if !found.contains(app) {
return Err(AppError::BadRequest(format!("Unknown app: {app}")));
}
}
Ok(())
}
}
fn normalize_apps(enabled_apps: Vec<String>) -> Vec<String> {
let mut out = Vec::new();
let mut seen = HashSet::new();
for a in enabled_apps {
let v = a.trim().to_lowercase();
if v.is_empty() {
continue;
}
if seen.insert(v.clone()) {
out.push(v);
}
}
out.sort();
out
}