caps and delete
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
http::{Request, StatusCode},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use tower::{Layer, Service};
|
||||
|
||||
use crate::{auth::AuthenticatedUser, error::AppError, models::ApiCapability};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CapabilityStrategy {
|
||||
All,
|
||||
Any,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RequireCapabilitiesLayer {
|
||||
required: Arc<Vec<ApiCapability>>,
|
||||
strategy: CapabilityStrategy,
|
||||
}
|
||||
|
||||
impl RequireCapabilitiesLayer {
|
||||
pub fn all<I>(caps: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = ApiCapability>,
|
||||
{
|
||||
Self {
|
||||
required: Arc::new(caps.into_iter().collect()),
|
||||
strategy: CapabilityStrategy::All,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn any<I>(caps: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = ApiCapability>,
|
||||
{
|
||||
Self {
|
||||
required: Arc::new(caps.into_iter().collect()),
|
||||
strategy: CapabilityStrategy::Any,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for RequireCapabilitiesLayer {
|
||||
type Service = RequireCapabilities<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
RequireCapabilities {
|
||||
inner,
|
||||
required: Arc::clone(&self.required),
|
||||
strategy: self.strategy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RequireCapabilities<S> {
|
||||
inner: S,
|
||||
required: Arc<Vec<ApiCapability>>,
|
||||
strategy: CapabilityStrategy,
|
||||
}
|
||||
|
||||
impl<S, B> Service<Request<B>> for RequireCapabilities<S>
|
||||
where
|
||||
S: Service<Request<B>, Response = axum::response::Response> + Send,
|
||||
S::Future: Send + 'static,
|
||||
B: Send + 'static,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = std::pin::Pin<
|
||||
Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>,
|
||||
>;
|
||||
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<B>) -> Self::Future {
|
||||
if self.required.is_empty() {
|
||||
let fut = self.inner.call(req);
|
||||
return Box::pin(async move { fut.await });
|
||||
}
|
||||
|
||||
let (parts, body) = req.into_parts();
|
||||
let user = match parts.extensions.get::<AuthenticatedUser>() {
|
||||
Some(user) => user,
|
||||
None => {
|
||||
let response = AppError::unauthorized().into_response();
|
||||
return Box::pin(async move { Ok(response) });
|
||||
}
|
||||
};
|
||||
|
||||
let allowed = match self.strategy {
|
||||
CapabilityStrategy::All => self
|
||||
.required
|
||||
.iter()
|
||||
.all(|cap| user.capabilities.contains(cap)),
|
||||
CapabilityStrategy::Any => self
|
||||
.required
|
||||
.iter()
|
||||
.any(|cap| user.capabilities.contains(cap)),
|
||||
};
|
||||
|
||||
if !allowed {
|
||||
let response = AppError::new(StatusCode::FORBIDDEN, "missing required capability")
|
||||
.with_code("missing_capability")
|
||||
.into_response();
|
||||
return Box::pin(async move { Ok(response) });
|
||||
}
|
||||
|
||||
let req = Request::from_parts(parts, body);
|
||||
let fut = self.inner.call(req);
|
||||
Box::pin(async move { fut.await })
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
+30
-4
@@ -6,6 +6,24 @@ use uuid::Uuid;
|
||||
|
||||
use crate::config::AppConfig;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PrincipalKind {
|
||||
UserSession,
|
||||
ApiToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AccessTokenContext {
|
||||
pub user_id: Uuid,
|
||||
pub tenant_id: Uuid,
|
||||
pub username: String,
|
||||
pub principal_kind: PrincipalKind,
|
||||
pub principal_id: Uuid,
|
||||
pub capability_set_id: Uuid,
|
||||
pub cap_version: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JwtService {
|
||||
encoding: EncodingKey,
|
||||
@@ -38,13 +56,17 @@ impl JwtService {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn generate_token(&self, user_id: Uuid, tenant_id: Uuid, username: &str) -> Result<String> {
|
||||
pub fn generate_token(&self, context: AccessTokenContext) -> Result<String> {
|
||||
let now = Utc::now();
|
||||
let exp = now + self.expiry;
|
||||
let claims = Claims {
|
||||
sub: user_id,
|
||||
tenant_id,
|
||||
username: username.to_owned(),
|
||||
sub: context.user_id,
|
||||
tenant_id: context.tenant_id,
|
||||
username: context.username,
|
||||
principal_kind: context.principal_kind,
|
||||
principal_id: context.principal_id,
|
||||
capability_set_id: context.capability_set_id,
|
||||
cap_version: context.cap_version,
|
||||
iss: self.issuer.clone(),
|
||||
aud: self.audience.clone(),
|
||||
iat: now.timestamp() as usize,
|
||||
@@ -148,6 +170,10 @@ pub struct Claims {
|
||||
pub sub: Uuid,
|
||||
pub tenant_id: Uuid,
|
||||
pub username: String,
|
||||
pub principal_kind: PrincipalKind,
|
||||
pub principal_id: Uuid,
|
||||
pub capability_set_id: Uuid,
|
||||
pub cap_version: i32,
|
||||
pub iss: String,
|
||||
pub aud: String,
|
||||
pub iat: usize,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod api_tokens;
|
||||
pub mod capability_guard;
|
||||
pub mod capability_sets;
|
||||
pub mod jwt;
|
||||
pub mod passkeys;
|
||||
pub mod password;
|
||||
@@ -10,16 +12,25 @@ use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::{
|
||||
auth::capability_sets::{load_capabilities_for_set, load_capability_set},
|
||||
error::AppError,
|
||||
models::ApiCapability,
|
||||
state::{AppState, PgPooledConnection},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::auth::jwt::PrincipalKind;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct AuthenticatedUser {
|
||||
pub user_id: uuid::Uuid,
|
||||
pub username: String,
|
||||
pub tenant_id: uuid::Uuid,
|
||||
pub principal_kind: PrincipalKind,
|
||||
pub principal_id: Uuid,
|
||||
pub capability_set_id: Uuid,
|
||||
pub cap_version: i32,
|
||||
pub capabilities: Vec<ApiCapability>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -44,10 +55,26 @@ impl FromRequestParts<AppState> for AuthenticatedUser {
|
||||
.verify_token(bearer.token())
|
||||
.map_err(|_| AppError::unauthorized())?;
|
||||
|
||||
let mut tenant_conn = state.db_for_tenant(claims.tenant_id)?;
|
||||
let capability_set = load_capability_set(&mut tenant_conn, claims.capability_set_id)
|
||||
.map_err(|_| AppError::unauthorized())?;
|
||||
|
||||
if capability_set.cap_version != claims.cap_version {
|
||||
return Err(AppError::unauthorized());
|
||||
}
|
||||
|
||||
let capabilities = load_capabilities_for_set(&mut tenant_conn, capability_set.id)
|
||||
.map_err(|_| AppError::unauthorized())?;
|
||||
|
||||
let user = AuthenticatedUser {
|
||||
user_id: claims.sub,
|
||||
username: claims.username,
|
||||
tenant_id: claims.tenant_id,
|
||||
principal_kind: claims.principal_kind,
|
||||
principal_id: claims.principal_id,
|
||||
capability_set_id: claims.capability_set_id,
|
||||
cap_version: claims.cap_version,
|
||||
capabilities,
|
||||
};
|
||||
|
||||
parts.extensions.insert(user.clone());
|
||||
|
||||
Reference in New Issue
Block a user