Optimize storage, GC, and upload hot paths
This commit is contained in:
@@ -3,6 +3,7 @@ use crate::domain::entities::app_password::AppPassword;
|
||||
use crate::domain::entities::device_code::DeviceCode;
|
||||
use crate::domain::entities::session::Session;
|
||||
use crate::domain::entities::user::User;
|
||||
use crate::domain::repositories::user_repository::UserListEntry;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -129,6 +130,15 @@ pub trait UserStoragePort: Send + Sync + 'static {
|
||||
include_external: bool,
|
||||
) -> Result<Vec<User>, DomainError>;
|
||||
|
||||
/// Narrow user-list projection for management tables. Keeps heavyweight
|
||||
/// account-detail fields off the database and JSON hot path.
|
||||
async fn list_user_summaries(
|
||||
&self,
|
||||
limit: i64,
|
||||
offset: i64,
|
||||
include_external: bool,
|
||||
) -> Result<Vec<UserListEntry>, DomainError>;
|
||||
|
||||
/// Searches users by username or email (SQL ILIKE) with a limit.
|
||||
/// See [`list_users`] for the meaning of `include_external`.
|
||||
async fn search_users(
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::common::errors::DomainError;
|
||||
use crate::domain::entities::user::UserRole;
|
||||
use crate::domain::services::authorization::{
|
||||
Grant, GrantCursor, IncomingGrantSummary, OutgoingResourceSummary, Permission, Resource,
|
||||
ResourceKind, Role, Subject,
|
||||
@@ -29,6 +30,21 @@ pub enum AuthzDenialVisibility {
|
||||
Hidden,
|
||||
}
|
||||
|
||||
fn system_admin_denial_reason(
|
||||
subject: Subject,
|
||||
role: UserRole,
|
||||
is_external: bool,
|
||||
active: bool,
|
||||
) -> Option<&'static str> {
|
||||
match subject {
|
||||
Subject::User(_) if !active => Some("inactive"),
|
||||
Subject::User(_) if is_external => Some("external_account"),
|
||||
Subject::User(_) if role != UserRole::Admin => Some("not_admin"),
|
||||
Subject::User(_) => None,
|
||||
_ => Some("unsupported_subject"),
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthzDenialVisibility {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
@@ -39,6 +55,44 @@ impl AuthzDenialVisibility {
|
||||
}
|
||||
|
||||
pub trait AuthorizationEngine: Send + Sync + 'static {
|
||||
/// Require the authenticated principal to hold the deployment-wide admin
|
||||
/// role. System administration has no resource UUID, so it cannot be
|
||||
/// represented by [`Resource`]; it still belongs in this policy port rather
|
||||
/// than in an HTTP handler or an application-service role shortcut.
|
||||
///
|
||||
/// The application authentication service supplies its already cached,
|
||||
/// image-free live flags. This avoids a second database query/cache for the
|
||||
/// same caller while keeping the authorization decision and denial audit in
|
||||
/// the engine's single policy surface.
|
||||
fn require_system_admin(
|
||||
&self,
|
||||
subject: Subject,
|
||||
role: UserRole,
|
||||
is_external: bool,
|
||||
active: bool,
|
||||
) -> Result<(), DomainError> {
|
||||
let reason = system_admin_denial_reason(subject, role, is_external, active);
|
||||
let Some(reason) = reason else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
target: "audit",
|
||||
event = "authz.admin_denied",
|
||||
reason,
|
||||
subject_type = subject.type_str(),
|
||||
caller_id = %subject.id(),
|
||||
role = role.as_str(),
|
||||
is_external,
|
||||
active,
|
||||
"👮🏻♂️ system-administrator permission denied"
|
||||
);
|
||||
Err(DomainError::access_denied(
|
||||
"System",
|
||||
"Admin access required",
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns true if `subject` has `permission` on `resource`, considering
|
||||
/// owner short-circuit AND cascading from folder ancestors.
|
||||
///
|
||||
@@ -304,3 +358,33 @@ pub trait AuthorizationEngine: Send + Sync + 'static {
|
||||
/// cleanup this is the canonical role-revocation entry point.
|
||||
async fn clear_role(&self, subject: Subject, resource: Resource) -> Result<(), DomainError>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod system_admin_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_active_internal_admin_users_pass_the_system_gate() {
|
||||
let id = Uuid::new_v4();
|
||||
assert_eq!(
|
||||
system_admin_denial_reason(Subject::User(id), UserRole::Admin, false, true),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
system_admin_denial_reason(Subject::User(id), UserRole::User, false, true),
|
||||
Some("not_admin")
|
||||
);
|
||||
assert_eq!(
|
||||
system_admin_denial_reason(Subject::User(id), UserRole::Admin, true, true),
|
||||
Some("external_account")
|
||||
);
|
||||
assert_eq!(
|
||||
system_admin_denial_reason(Subject::User(id), UserRole::Admin, false, false),
|
||||
Some("inactive")
|
||||
);
|
||||
assert_eq!(
|
||||
system_admin_denial_reason(Subject::Token(id), UserRole::Admin, false, true),
|
||||
Some("unsupported_subject")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user