This commit is contained in:
2025-10-31 19:13:11 +01:00
parent 2d8a4432cc
commit 175387f0d3
19 changed files with 463 additions and 107 deletions
+58 -29
View File
@@ -26,14 +26,17 @@ use crate::{
},
error::{AppError, AppResult},
models::{
MagicToken, MagicTokenKind, NewRefreshToken, NewUser, RefreshToken, Tenant, TenantStatus,
User, UserMembership,
MagicToken, MagicTokenKind, NewRefreshToken, NewUser, RefreshToken, TenantStatus, User,
},
schema::{
magic_tokens::dsl as magic_dsl, refresh_tokens, tenants::dsl as tenant_dsl,
user_memberships::dsl as memberships_dsl, user_passkeys::dsl as passkey_dsl, users::dsl,
},
state::AppState,
tenants::{
apply_refresh_token_hash, apply_tenant_guc, apply_user_guc, clear_refresh_token_hash,
clear_user_guc,
},
};
use crate::schema::refresh_tokens::dsl as refresh_dsl;
@@ -337,6 +340,7 @@ pub async fn refresh(
let now = Utc::now();
let now_naive = now.naive_utc();
apply_refresh_token_hash(&mut conn, &hashed)?;
let token = match refresh_dsl::refresh_tokens
.filter(refresh_dsl::token_hash.eq(&hashed))
.filter(refresh_dsl::revoked_at.is_null())
@@ -348,6 +352,10 @@ pub async fn refresh(
Err(err) => return Err(AppError::from(err)),
};
clear_refresh_token_hash(&mut conn)?;
apply_tenant_guc(&mut conn, token.tenant_id)?;
clear_user_guc(&mut conn)?;
diesel::update(refresh_dsl::refresh_tokens.filter(refresh_dsl::id.eq(token.id)))
.set((
refresh_dsl::revoked_at.eq(now_naive),
@@ -398,15 +406,17 @@ pub async fn select_tenant(
};
let mut conn = state.db_unscoped()?;
apply_user_guc(&mut conn, user_id)?;
let membership_exists = memberships_dsl::user_memberships
.filter(memberships_dsl::user_id.eq(user_id))
.filter(memberships_dsl::tenant_id.eq(payload.tenant_id))
.inner_join(tenant_dsl::tenants)
.select(memberships_dsl::id)
.select(memberships_dsl::tenant_id)
.first::<Uuid>(&mut conn)
.optional()?;
clear_user_guc(&mut conn)?;
if membership_exists.is_none() {
return Err(AppError::unauthorized());
}
@@ -430,7 +440,7 @@ pub async fn logout(
user: AuthenticatedUser,
jar: Option<TypedHeader<Cookie>>,
) -> AppResult<(HeaderMap, StatusCode)> {
let mut conn = state.db_unscoped()?;
let mut conn = state.db_for_tenant(user.tenant_id)?;
let now = Utc::now().naive_utc();
let mut rows_affected = 0;
@@ -505,15 +515,26 @@ pub async fn list_tenants(
};
let mut conn = state.db_unscoped()?;
apply_user_guc(&mut conn, user_id)?;
let tenants = memberships_dsl::user_memberships
.inner_join(tenant_dsl::tenants)
let tenant_ids: Vec<Uuid> = memberships_dsl::user_memberships
.filter(memberships_dsl::user_id.eq(user_id))
.select((tenant_dsl::id, tenant_dsl::name))
.load::<(Uuid, String)>(&mut conn)?
.into_iter()
.map(|(id, name)| TenantSnippet { id, name })
.collect();
.select(memberships_dsl::tenant_id)
.load(&mut conn)?;
clear_user_guc(&mut conn)?;
drop(conn);
let mut tenants = Vec::with_capacity(tenant_ids.len());
for tenant_id in tenant_ids {
let mut tenant_conn = state.db_for_tenant(tenant_id)?;
let name: String = tenant_dsl::tenants
.find(tenant_id)
.select(tenant_dsl::name)
.first(&mut tenant_conn)
.map_err(AppError::from)?;
tenants.push(TenantSnippet { id: tenant_id, name });
}
Ok(Json(TenantListResponse { tenants }))
}
@@ -642,26 +663,27 @@ fn complete_login(
user: &User,
preferred_tenant_id: Option<Uuid>,
) -> AppResult<Response> {
let memberships: Vec<(UserMembership, Tenant)> = memberships_dsl::user_memberships
.inner_join(tenant_dsl::tenants)
apply_user_guc(conn, user.id)?;
let tenant_ids: Vec<Uuid> = memberships_dsl::user_memberships
.filter(memberships_dsl::user_id.eq(user.id))
.select(memberships_dsl::tenant_id)
.load(conn)?;
clear_user_guc(conn)?;
if memberships.is_empty() {
tracing::debug!(user_id = %user.id, tenants = tenant_ids.len(), "passkey login memberships");
if tenant_ids.is_empty() {
return Err(AppError::unauthorized());
}
if let Some(preferred_id) = preferred_tenant_id {
if let Some((_, tenant)) = memberships
.iter()
.find(|(_, tenant)| tenant.id == preferred_id)
{
return issue_session(state, conn, user, tenant.id);
if tenant_ids.iter().any(|id| *id == preferred_id) {
return issue_session(state, conn, user, preferred_id);
}
}
if memberships.len() == 1 {
return issue_session(state, conn, user, memberships[0].1.id);
if tenant_ids.len() == 1 {
return issue_session(state, conn, user, tenant_ids[0]);
}
let selection_token = state
@@ -669,13 +691,16 @@ fn complete_login(
.generate_tenant_selector_token(user.id)
.map_err(AppError::from)?;
let tenants = memberships
.into_iter()
.map(|(_, tenant)| TenantSnippet {
id: tenant.id,
name: tenant.name,
})
.collect();
let mut tenants = Vec::with_capacity(tenant_ids.len());
for tenant_id in tenant_ids {
let mut tenant_conn = state.db_for_tenant(tenant_id)?;
let name: String = tenant_dsl::tenants
.find(tenant_id)
.select(tenant_dsl::name)
.first(&mut tenant_conn)
.map_err(AppError::from)?;
tenants.push(TenantSnippet { id: tenant_id, name });
}
Ok(Json(TenantSelectionResponse {
access_token: selection_token,
@@ -747,6 +772,10 @@ fn issue_session(
user: &User,
tenant_id: Uuid,
) -> AppResult<Response> {
apply_tenant_guc(conn, tenant_id)?;
clear_user_guc(conn)?;
clear_refresh_token_hash(conn)?;
let now = Utc::now();
let access_token = state
.jwt
+13 -8
View File
@@ -21,10 +21,11 @@ use crate::error::{AppError, AppResult};
use crate::models::{Document, DocumentVersion, Folder, User};
use crate::schema::{
document_versions::dsl as document_versions_dsl, documents::dsl as documents_dsl,
folders::dsl as folders_dsl, tenants::dsl as tenant_dsl,
user_memberships::dsl as memberships_dsl, users::dsl as users_dsl,
folders::dsl as folders_dsl, user_memberships::dsl as memberships_dsl,
users::dsl as users_dsl,
};
use crate::state::AppState;
use crate::tenants::{apply_tenant_guc, apply_user_guc, clear_user_guc};
use crate::utils::{error::StorageResultExt, http::inline_content_disposition, time::to_http_date};
const REALM: &str = "Papercrate WebDAV";
@@ -453,16 +454,19 @@ fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<Option<WebDavCo
}
};
let tenant_row = memberships_dsl::user_memberships
.inner_join(tenant_dsl::tenants)
apply_user_guc(&mut conn, user.id)?;
let membership_exists = memberships_dsl::user_memberships
.filter(memberships_dsl::user_id.eq(user.id))
.filter(memberships_dsl::tenant_id.eq(token.tenant_id))
.select((tenant_dsl::id, tenant_dsl::name))
.first::<(Uuid, String)>(&mut conn)
.select(memberships_dsl::tenant_id)
.first::<Uuid>(&mut conn)
.optional()?;
let (tenant_id, _name) = match tenant_row {
Some(row) => row,
clear_user_guc(&mut conn)?;
let tenant_id = match membership_exists {
Some(id) => id,
None => {
tracing::warn!(
%username,
@@ -473,6 +477,7 @@ fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<Option<WebDavCo
}
};
apply_tenant_guc(&mut conn, tenant_id)?;
touch_webdav_token(&mut conn, token.id)?;
tracing::debug!(