caps and delete
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
use chrono::Utc;
|
||||
use diesel::{pg::Pg, prelude::*, Connection};
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
error::AppError,
|
||||
models::{ApiCapability, CapabilitySet, NewCapabilitySet, NewCapabilitySetCapability},
|
||||
schema::{
|
||||
capability_set_capabilities, capability_set_capabilities::dsl as csc_dsl, capability_sets,
|
||||
capability_sets::dsl as cs_dsl,
|
||||
},
|
||||
};
|
||||
|
||||
const OWNER_CAPABILITIES: [ApiCapability; 19] = [
|
||||
ApiCapability::CorrespondentsEdit,
|
||||
ApiCapability::CorrespondentsRead,
|
||||
ApiCapability::CorrespondentsWrite,
|
||||
ApiCapability::DocumentsEdit,
|
||||
ApiCapability::DocumentsRead,
|
||||
ApiCapability::DocumentsUpload,
|
||||
ApiCapability::DocumentsWrite,
|
||||
ApiCapability::FoldersEdit,
|
||||
ApiCapability::FoldersRead,
|
||||
ApiCapability::FoldersWrite,
|
||||
ApiCapability::ProfileRead,
|
||||
ApiCapability::ProfileWrite,
|
||||
ApiCapability::TagsEdit,
|
||||
ApiCapability::TagsRead,
|
||||
ApiCapability::TagsWrite,
|
||||
ApiCapability::WebdavRead,
|
||||
ApiCapability::WebdavWrite,
|
||||
ApiCapability::CapabilitySetsRead,
|
||||
ApiCapability::CapabilitySetsWrite,
|
||||
];
|
||||
|
||||
const USER_CAPABILITIES: [ApiCapability; 16] = [
|
||||
ApiCapability::CorrespondentsEdit,
|
||||
ApiCapability::CorrespondentsRead,
|
||||
ApiCapability::CorrespondentsWrite,
|
||||
ApiCapability::DocumentsEdit,
|
||||
ApiCapability::DocumentsRead,
|
||||
ApiCapability::DocumentsUpload,
|
||||
ApiCapability::DocumentsWrite,
|
||||
ApiCapability::FoldersEdit,
|
||||
ApiCapability::FoldersRead,
|
||||
ApiCapability::FoldersWrite,
|
||||
ApiCapability::ProfileRead,
|
||||
ApiCapability::ProfileWrite,
|
||||
ApiCapability::TagsEdit,
|
||||
ApiCapability::TagsRead,
|
||||
ApiCapability::TagsWrite,
|
||||
ApiCapability::WebdavRead,
|
||||
];
|
||||
|
||||
const WEBDAV_CAPABILITIES: [ApiCapability; 1] = [ApiCapability::WebdavRead];
|
||||
|
||||
pub fn owner_capabilities() -> &'static [ApiCapability] {
|
||||
&OWNER_CAPABILITIES
|
||||
}
|
||||
|
||||
pub fn user_capabilities() -> &'static [ApiCapability] {
|
||||
&USER_CAPABILITIES
|
||||
}
|
||||
|
||||
pub fn webdav_capabilities() -> &'static [ApiCapability] {
|
||||
&WEBDAV_CAPABILITIES
|
||||
}
|
||||
|
||||
pub fn is_system_slug(slug: &str) -> bool {
|
||||
matches!(slug, "owner" | "user" | "webdav")
|
||||
}
|
||||
|
||||
pub fn create_capability_set<C>(
|
||||
conn: &mut C,
|
||||
tenant_id: Uuid,
|
||||
slug: &str,
|
||||
capabilities: Vec<ApiCapability>,
|
||||
) -> Result<CapabilitySet, AppError>
|
||||
where
|
||||
C: Connection<Backend = Pg> + diesel::connection::LoadConnection,
|
||||
{
|
||||
let normalized = normalize_capabilities(capabilities)?;
|
||||
|
||||
conn.transaction::<CapabilitySet, AppError, _>(|conn| {
|
||||
if cs_dsl::capability_sets
|
||||
.filter(cs_dsl::tenant_id.eq(tenant_id))
|
||||
.filter(cs_dsl::slug.eq(slug))
|
||||
.first::<CapabilitySet>(conn)
|
||||
.optional()
|
||||
.map_err(AppError::from)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(AppError::conflict("capability set slug already exists"));
|
||||
}
|
||||
|
||||
let set = NewCapabilitySet {
|
||||
id: Uuid::new_v4(),
|
||||
tenant_id,
|
||||
slug: slug.to_owned(),
|
||||
cap_version: 1,
|
||||
is_system: false,
|
||||
};
|
||||
|
||||
diesel::insert_into(capability_sets::table)
|
||||
.values(&set)
|
||||
.execute(conn)
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
persist_capabilities(conn, set.id, &normalized)?;
|
||||
|
||||
capability_sets::table
|
||||
.find(set.id)
|
||||
.first::<CapabilitySet>(conn)
|
||||
.map_err(AppError::from)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn normalize_capabilities(
|
||||
mut capabilities: Vec<ApiCapability>,
|
||||
) -> Result<Vec<ApiCapability>, AppError> {
|
||||
if capabilities.is_empty() {
|
||||
return Err(AppError::bad_request("at least one capability is required"));
|
||||
}
|
||||
|
||||
capabilities.sort_by(|a, b| a.as_str().cmp(b.as_str()));
|
||||
capabilities.dedup();
|
||||
Ok(capabilities)
|
||||
}
|
||||
|
||||
pub fn load_capabilities_for_set<C>(
|
||||
conn: &mut C,
|
||||
capability_set_id: Uuid,
|
||||
) -> Result<Vec<ApiCapability>, AppError>
|
||||
where
|
||||
C: Connection<Backend = Pg> + diesel::connection::LoadConnection,
|
||||
{
|
||||
let mut capabilities: Vec<ApiCapability> = csc_dsl::capability_set_capabilities
|
||||
.filter(csc_dsl::capability_set_id.eq(capability_set_id))
|
||||
.select(csc_dsl::capability)
|
||||
.load(conn)
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
capabilities.sort_by(|a, b| a.as_str().cmp(b.as_str()));
|
||||
Ok(capabilities)
|
||||
}
|
||||
|
||||
pub fn ensure_capability_set<C>(
|
||||
conn: &mut C,
|
||||
tenant_id: Uuid,
|
||||
capabilities: &[ApiCapability],
|
||||
) -> Result<CapabilitySet, AppError>
|
||||
where
|
||||
C: Connection<Backend = Pg> + diesel::connection::LoadConnection,
|
||||
{
|
||||
if capabilities.is_empty() {
|
||||
return Err(AppError::bad_request("at least one capability is required"));
|
||||
}
|
||||
|
||||
let slug = compute_slug(capabilities);
|
||||
conn.transaction::<CapabilitySet, AppError, _>(|conn| {
|
||||
if let Some(existing) = cs_dsl::capability_sets
|
||||
.filter(cs_dsl::tenant_id.eq(tenant_id))
|
||||
.filter(cs_dsl::slug.eq(&slug))
|
||||
.first::<CapabilitySet>(conn)
|
||||
.optional()
|
||||
.map_err(AppError::from)?
|
||||
{
|
||||
ensure_capability_membership(conn, &existing, capabilities)?;
|
||||
return Ok(existing);
|
||||
}
|
||||
|
||||
let set = NewCapabilitySet {
|
||||
id: Uuid::new_v4(),
|
||||
tenant_id,
|
||||
slug: slug.clone(),
|
||||
cap_version: 1,
|
||||
is_system: slug == "owner" || slug == "user" || slug == "webdav",
|
||||
};
|
||||
|
||||
diesel::insert_into(capability_sets::table)
|
||||
.values(&set)
|
||||
.execute(conn)
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
persist_capabilities(conn, set.id, capabilities)?;
|
||||
|
||||
Ok(capability_sets::table
|
||||
.find(set.id)
|
||||
.first::<CapabilitySet>(conn)
|
||||
.map_err(AppError::from)?)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn refresh_capability_set<C>(
|
||||
conn: &mut C,
|
||||
set: &CapabilitySet,
|
||||
capabilities: &[ApiCapability],
|
||||
) -> Result<CapabilitySet, AppError>
|
||||
where
|
||||
C: Connection<Backend = Pg> + diesel::connection::LoadConnection,
|
||||
{
|
||||
conn.transaction::<CapabilitySet, AppError, _>(|conn| {
|
||||
diesel::delete(
|
||||
csc_dsl::capability_set_capabilities.filter(csc_dsl::capability_set_id.eq(set.id)),
|
||||
)
|
||||
.execute(conn)
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
persist_capabilities(conn, set.id, capabilities)?;
|
||||
|
||||
diesel::update(capability_sets::table.find(set.id))
|
||||
.set((
|
||||
cs_dsl::cap_version.eq(set.cap_version + 1),
|
||||
cs_dsl::updated_at.eq(Utc::now().naive_utc()),
|
||||
))
|
||||
.execute(conn)
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
capability_sets::table
|
||||
.find(set.id)
|
||||
.first::<CapabilitySet>(conn)
|
||||
.map_err(AppError::from)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_capability_set<C>(conn: &mut C, id: Uuid) -> Result<CapabilitySet, AppError>
|
||||
where
|
||||
C: Connection<Backend = Pg> + diesel::connection::LoadConnection,
|
||||
{
|
||||
capability_sets::table
|
||||
.find(id)
|
||||
.first::<CapabilitySet>(conn)
|
||||
.map_err(AppError::from)
|
||||
}
|
||||
|
||||
pub fn compute_slug(capabilities: &[ApiCapability]) -> String {
|
||||
if capabilities == owner_capabilities() {
|
||||
return "owner".to_string();
|
||||
}
|
||||
|
||||
if capabilities == user_capabilities() {
|
||||
return "user".to_string();
|
||||
}
|
||||
|
||||
if capabilities == webdav_capabilities() {
|
||||
return "webdav".to_string();
|
||||
}
|
||||
|
||||
let joined = capabilities
|
||||
.iter()
|
||||
.map(|cap| cap.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
let digest = Sha256::digest(joined.as_bytes());
|
||||
let hex = hex::encode(digest);
|
||||
format!("caps-{}", &hex[..12])
|
||||
}
|
||||
|
||||
fn ensure_capability_membership<C>(
|
||||
conn: &mut C,
|
||||
set: &CapabilitySet,
|
||||
desired: &[ApiCapability],
|
||||
) -> Result<(), AppError>
|
||||
where
|
||||
C: Connection<Backend = Pg> + diesel::connection::LoadConnection,
|
||||
{
|
||||
let current = load_capabilities_for_set(conn, set.id)?;
|
||||
if current == desired {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _ = refresh_capability_set(conn, set, desired)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn persist_capabilities<C>(
|
||||
conn: &mut C,
|
||||
set_id: Uuid,
|
||||
capabilities: &[ApiCapability],
|
||||
) -> Result<(), AppError>
|
||||
where
|
||||
C: Connection<Backend = Pg> + diesel::connection::LoadConnection,
|
||||
{
|
||||
if capabilities.is_empty() {
|
||||
return Err(AppError::bad_request("at least one capability is required"));
|
||||
}
|
||||
|
||||
let records: Vec<NewCapabilitySetCapability> = capabilities
|
||||
.iter()
|
||||
.map(|cap| NewCapabilitySetCapability {
|
||||
capability_set_id: set_id,
|
||||
capability: *cap,
|
||||
})
|
||||
.collect();
|
||||
|
||||
diesel::insert_into(capability_set_capabilities::table)
|
||||
.values(&records)
|
||||
.execute(conn)
|
||||
.map_err(AppError::from)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user