caps and delete

This commit is contained in:
2025-11-05 12:32:10 +01:00
parent 4ec19dbd70
commit 480bc20ae7
30 changed files with 2702 additions and 262 deletions
+30 -33
View File
@@ -9,8 +9,11 @@ use rand::RngCore;
use uuid::Uuid;
use crate::{
auth::capability_sets::{
ensure_capability_set, load_capabilities_for_set, normalize_capabilities,
},
error::AppError,
models::{ApiToken, ApiTokenCapability, NewApiToken},
models::{ApiCapability, ApiToken, NewApiToken},
schema::api_tokens,
state::PgPooledConnection,
tenants::{apply_api_token_prefix, clear_api_token_prefix},
@@ -34,9 +37,10 @@ pub fn create_api_token(
tenant_id: Uuid,
label: Option<String>,
expires_at: Option<NaiveDateTime>,
capabilities: Vec<ApiTokenCapability>,
capabilities: Vec<ApiCapability>,
) -> Result<IssuedApiToken, AppError> {
let capabilities = normalize_capabilities(capabilities)?;
let capability_set = ensure_capability_set(conn, tenant_id, &capabilities)?;
let raw_secret = generate_secret()?;
let token_prefix = raw_secret[..TOKEN_PREFIX_LENGTH].to_string();
@@ -49,7 +53,7 @@ pub fn create_api_token(
token_hash,
label,
expires_at,
capabilities,
capability_set_id: capability_set.id,
};
let record = diesel::insert_into(api_tokens::table)
@@ -122,7 +126,7 @@ pub fn update_api_token_capabilities(
token_id: Uuid,
user_id: Uuid,
tenant_id: Option<Uuid>,
capabilities: Vec<ApiTokenCapability>,
capabilities: Vec<ApiCapability>,
) -> Result<ApiToken, AppError> {
let capabilities = normalize_capabilities(capabilities)?;
@@ -134,8 +138,10 @@ pub fn update_api_token_capabilities(
));
}
let capability_set = ensure_capability_set(conn, token.tenant_id, &capabilities)?;
let updated = diesel::update(api_tokens::table.find(token.id))
.set(api_tokens::capabilities.eq(capabilities))
.set(api_tokens::capability_set_id.eq(capability_set.id))
.get_result::<ApiToken>(conn)?;
Ok(updated)
@@ -147,7 +153,7 @@ pub fn find_active_token_by_secret(
conn: &mut PgPooledConnection,
tenant_id: Option<Uuid>,
secret: &str,
required_capability: ApiTokenCapability,
required_capability: ApiCapability,
) -> Result<Option<ApiToken>, AppError> {
if secret.len() < TOKEN_PREFIX_LENGTH {
return Ok(None);
@@ -175,7 +181,8 @@ pub fn find_active_token_by_secret(
})?;
for token in candidates {
if !token.capabilities.contains(&required_capability) {
let capabilities = load_capabilities_for_set(conn, token.capability_set_id)?;
if !capabilities.contains(&required_capability) {
continue;
}
@@ -218,23 +225,6 @@ pub fn verify_token_secret(secret: &str, token_hash: &str) -> Result<bool, AppEr
})
}
fn normalize_capabilities(
capabilities: Vec<ApiTokenCapability>,
) -> Result<Vec<ApiTokenCapability>, AppError> {
if capabilities.is_empty() {
return Err(AppError::bad_request("at least one capability is required"));
}
let mut unique = Vec::new();
for capability in capabilities {
if !unique.contains(&capability) {
unique.push(capability);
}
}
Ok(unique)
}
fn find_user_token(
conn: &mut PgPooledConnection,
token_id: Uuid,
@@ -299,6 +289,9 @@ fn hash_secret(secret: &str) -> Result<String, AppError> {
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::capability_sets::{
compute_slug, normalize_capabilities, owner_capabilities, webdav_capabilities,
};
#[test]
fn generated_secret_has_expected_length() {
@@ -316,15 +309,10 @@ mod tests {
#[test]
fn normalize_capabilities_deduplicates() {
let caps = normalize_capabilities(vec![
ApiTokenCapability::Api,
ApiTokenCapability::Webdav,
ApiTokenCapability::Api,
])
.unwrap();
assert_eq!(caps.len(), 2);
assert!(caps.contains(&ApiTokenCapability::Api));
assert!(caps.contains(&ApiTokenCapability::Webdav));
let mut caps = owner_capabilities().to_vec();
caps.push(ApiCapability::DocumentsRead);
let normalized = normalize_capabilities(caps).unwrap();
assert_eq!(normalized.len(), owner_capabilities().len());
}
#[test]
@@ -332,6 +320,15 @@ mod tests {
assert!(normalize_capabilities(Vec::new()).is_err());
}
#[test]
fn compute_slug_matches_system_sets() {
let owner_slug = compute_slug(owner_capabilities());
assert_eq!(owner_slug, "owner");
let webdav_slug = compute_slug(webdav_capabilities());
assert_eq!(webdav_slug, "webdav");
}
#[test]
fn prefix_length_is_less_than_secret_length() {
assert!(TOKEN_PREFIX_LENGTH < TOKEN_SECRET_LENGTH * 2);