tenant gate active
This commit is contained in:
+16
-3
@@ -7,7 +7,10 @@ pub mod password;
|
|||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use axum::{extract::FromRequestParts, http::request::Parts};
|
use axum::{
|
||||||
|
extract::FromRequestParts,
|
||||||
|
http::{request::Parts, StatusCode},
|
||||||
|
};
|
||||||
use axum_extra::headers::{authorization::Bearer, Authorization};
|
use axum_extra::headers::{authorization::Bearer, Authorization};
|
||||||
use axum_extra::TypedHeader;
|
use axum_extra::TypedHeader;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -15,8 +18,8 @@ use utoipa::ToSchema;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::capability_sets::{load_capabilities_for_set, load_capability_set},
|
auth::capability_sets::{load_capabilities_for_set, load_capability_set},
|
||||||
error::AppError,
|
error::{AppError, AppResult},
|
||||||
models::ApiCapability,
|
models::{ApiCapability, TenantStatus},
|
||||||
state::{AppState, PgPooledConnection},
|
state::{AppState, PgPooledConnection},
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -133,6 +136,8 @@ impl FromRequestParts<AppState> for TenantScopedConn {
|
|||||||
async move {
|
async move {
|
||||||
let user = AuthenticatedUser::from_request_parts(parts, &state).await?;
|
let user = AuthenticatedUser::from_request_parts(parts, &state).await?;
|
||||||
let tenant_id = user.tenant_id;
|
let tenant_id = user.tenant_id;
|
||||||
|
ensure_active_tenant(&state, tenant_id)?;
|
||||||
|
|
||||||
let conn = if let Some(holder) = parts.extensions.remove::<TenantConnectionHolder>() {
|
let conn = if let Some(holder) = parts.extensions.remove::<TenantConnectionHolder>() {
|
||||||
holder
|
holder
|
||||||
.into_conn()
|
.into_conn()
|
||||||
@@ -150,3 +155,11 @@ impl FromRequestParts<AppState> for TenantScopedConn {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ensure_active_tenant(state: &AppState, tenant_id: Uuid) -> AppResult<()> {
|
||||||
|
let tenant = state.tenants.get_by_id(tenant_id)?;
|
||||||
|
if tenant.status != TenantStatus::Active {
|
||||||
|
return Err(AppError::new(StatusCode::FORBIDDEN, "tenant is not active"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::fmt::Display;
|
use std::fmt::{self, Display};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
pub type AppResult<T> = Result<T, AppError>;
|
pub type AppResult<T> = Result<T, AppError>;
|
||||||
@@ -59,6 +59,12 @@ impl AppError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for AppError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}: {}", self.status, self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IntoResponse for AppError {
|
impl IntoResponse for AppError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let status = self.status;
|
let status = self.status;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ pub const STATUS_FAILED: &str = "failed";
|
|||||||
pub const JOB_ANALYZE_DOCUMENT: &str = "analyze-document";
|
pub const JOB_ANALYZE_DOCUMENT: &str = "analyze-document";
|
||||||
pub const JOB_GENERATE_THUMBNAILS: &str = "generate-thumbnails";
|
pub const JOB_GENERATE_THUMBNAILS: &str = "generate-thumbnails";
|
||||||
pub const JOB_GENERATE_OCR_TEXT: &str = "generate-ocr-text";
|
pub const JOB_GENERATE_OCR_TEXT: &str = "generate-ocr-text";
|
||||||
pub const JOB_INDEX_DOCUMENT_TEXT: &str = "index-document-text";
|
|
||||||
pub const JOB_PROVISION_TENANT: &str = "provision-tenant";
|
pub const JOB_PROVISION_TENANT: &str = "provision-tenant";
|
||||||
pub const JOB_PURGE_DOCUMENT: &str = "purge-document";
|
pub const JOB_PURGE_DOCUMENT: &str = "purge-document";
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use tracing::{error, info};
|
|||||||
use utoipa::{IntoParams, ToSchema};
|
use utoipa::{IntoParams, ToSchema};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::auth::{jwt::DownloadSubject, TenantScopedConn};
|
use crate::auth::{ensure_active_tenant, jwt::DownloadSubject, TenantScopedConn};
|
||||||
use crate::documents::asset::{
|
use crate::documents::asset::{
|
||||||
asset_object_disposition, DocumentAssetDetailResponse, DocumentAssetResponse,
|
asset_object_disposition, DocumentAssetDetailResponse, DocumentAssetResponse,
|
||||||
DocumentVersionDetailResponse, DocumentVersionResponse,
|
DocumentVersionDetailResponse, DocumentVersionResponse,
|
||||||
@@ -573,6 +573,8 @@ pub async fn download_with_token(
|
|||||||
.verify_download_token(&token)
|
.verify_download_token(&token)
|
||||||
.map_err(|_| AppError::unauthorized())?;
|
.map_err(|_| AppError::unauthorized())?;
|
||||||
|
|
||||||
|
ensure_active_tenant(&state, claims.tenant_id)?;
|
||||||
|
|
||||||
let mut conn = state.db_for_tenant(claims.tenant_id)?;
|
let mut conn = state.db_for_tenant(claims.tenant_id)?;
|
||||||
|
|
||||||
let now = Utc::now().naive_utc();
|
let now = Utc::now().naive_utc();
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
|
|||||||
use quick_xml::Writer;
|
use quick_xml::Writer;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::auth::api_tokens::{find_active_token_by_secret, touch_api_token};
|
use crate::auth::{
|
||||||
|
api_tokens::{find_active_token_by_secret, touch_api_token},
|
||||||
|
ensure_active_tenant,
|
||||||
|
};
|
||||||
use crate::error::{AppError, AppResult};
|
use crate::error::{AppError, AppResult};
|
||||||
use crate::models::{ApiCapability, Document, DocumentVersion, Folder, User};
|
use crate::models::{ApiCapability, Document, DocumentVersion, Folder, User};
|
||||||
use crate::schema::{
|
use crate::schema::{
|
||||||
@@ -483,6 +486,11 @@ fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<Option<WebDavCo
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Err(err) = ensure_active_tenant(state, tenant_id) {
|
||||||
|
tracing::warn!(tenant_id = %tenant_id, error = ?err, "webdav tenant not active");
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
apply_tenant_guc(&mut conn, tenant_id)?;
|
apply_tenant_guc(&mut conn, tenant_id)?;
|
||||||
touch_api_token(&mut conn, token.id)?;
|
touch_api_token(&mut conn, token.id)?;
|
||||||
|
|
||||||
|
|||||||
@@ -458,11 +458,14 @@ impl<'a> AuthService<'a> {
|
|||||||
|
|
||||||
let mut tenants = Vec::with_capacity(tenant_ids.len());
|
let mut tenants = Vec::with_capacity(tenant_ids.len());
|
||||||
for tenant_id in tenant_ids {
|
for tenant_id in tenant_ids {
|
||||||
let name: String = tenant_dsl::tenants
|
let (name, status): (String, TenantStatus) = tenant_dsl::tenants
|
||||||
.find(tenant_id)
|
.find(tenant_id)
|
||||||
.select(tenant_dsl::name)
|
.select((tenant_dsl::name, tenant_dsl::status))
|
||||||
.first(&mut conn)
|
.first(&mut conn)
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
if status != TenantStatus::Active {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
tenants.push(TenantSnippet {
|
tenants.push(TenantSnippet {
|
||||||
id: tenant_id,
|
id: tenant_id,
|
||||||
name,
|
name,
|
||||||
@@ -575,14 +578,33 @@ impl<'a> AuthService<'a> {
|
|||||||
return Err(AppError::unauthorized());
|
return Err(AppError::unauthorized());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut active_tenants = Vec::new();
|
||||||
|
for tenant_id in tenant_ids {
|
||||||
|
let (name, status): (String, TenantStatus) = tenant_dsl::tenants
|
||||||
|
.find(tenant_id)
|
||||||
|
.select((tenant_dsl::name, tenant_dsl::status))
|
||||||
|
.first(conn)
|
||||||
|
.map_err(AppError::from)?;
|
||||||
|
if status == TenantStatus::Active {
|
||||||
|
active_tenants.push((tenant_id, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if active_tenants.is_empty() {
|
||||||
|
return Err(AppError::new(
|
||||||
|
StatusCode::FORBIDDEN,
|
||||||
|
"no active tenants available",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(preferred_id) = preferred_tenant_id {
|
if let Some(preferred_id) = preferred_tenant_id {
|
||||||
if tenant_ids.iter().any(|id| *id == preferred_id) {
|
if active_tenants.iter().any(|(id, _)| *id == preferred_id) {
|
||||||
return self.issue_session(conn, user, preferred_id);
|
return self.issue_session(conn, user, preferred_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tenant_ids.len() == 1 {
|
if active_tenants.len() == 1 {
|
||||||
return self.issue_session(conn, user, tenant_ids[0]);
|
return self.issue_session(conn, user, active_tenants[0].0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let selection_token = self
|
let selection_token = self
|
||||||
@@ -591,20 +613,10 @@ impl<'a> AuthService<'a> {
|
|||||||
.generate_tenant_selector_token(user.id)
|
.generate_tenant_selector_token(user.id)
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
|
|
||||||
let mut tenants = Vec::with_capacity(tenant_ids.len());
|
let tenants = active_tenants
|
||||||
for tenant_id in tenant_ids {
|
.into_iter()
|
||||||
apply_tenant_guc(conn, tenant_id)?;
|
.map(|(id, name)| TenantSnippet { id, name })
|
||||||
clear_user_guc(conn)?;
|
.collect();
|
||||||
let name: String = tenant_dsl::tenants
|
|
||||||
.find(tenant_id)
|
|
||||||
.select(tenant_dsl::name)
|
|
||||||
.first(conn)
|
|
||||||
.map_err(AppError::from)?;
|
|
||||||
tenants.push(TenantSnippet {
|
|
||||||
id: tenant_id,
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = ok_json(LoginResponseVariants::Selection(TenantSelectionResponse {
|
let response = ok_json(LoginResponseVariants::Selection(TenantSelectionResponse {
|
||||||
access_token: selection_token,
|
access_token: selection_token,
|
||||||
@@ -677,6 +689,7 @@ impl<'a> AuthService<'a> {
|
|||||||
user: &User,
|
user: &User,
|
||||||
tenant_id: Uuid,
|
tenant_id: Uuid,
|
||||||
) -> AppResult<Response> {
|
) -> AppResult<Response> {
|
||||||
|
crate::auth::ensure_active_tenant(&self.state, tenant_id)?;
|
||||||
apply_tenant_guc(conn, tenant_id)?;
|
apply_tenant_guc(conn, tenant_id)?;
|
||||||
clear_user_guc(conn)?;
|
clear_user_guc(conn)?;
|
||||||
clear_user_session_hash(conn)?;
|
clear_user_session_hash(conn)?;
|
||||||
|
|||||||
@@ -37,9 +37,7 @@ use crate::documents::{
|
|||||||
tags::assign_tags as assign_tags_to_document,
|
tags::assign_tags as assign_tags_to_document,
|
||||||
};
|
};
|
||||||
use crate::error::{AppError, AppResult};
|
use crate::error::{AppError, AppResult};
|
||||||
use crate::jobs::{
|
use crate::jobs::{enqueue_job, JobQueueError, JOB_ANALYZE_DOCUMENT, JOB_PURGE_DOCUMENT};
|
||||||
enqueue_job, JobQueueError, JOB_ANALYZE_DOCUMENT, JOB_INDEX_DOCUMENT_TEXT, JOB_PURGE_DOCUMENT,
|
|
||||||
};
|
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
Document, DocumentAsset, DocumentAssetObject, DocumentVersion, NewDocument, NewDocumentVersion,
|
Document, DocumentAsset, DocumentAssetObject, DocumentVersion, NewDocument, NewDocumentVersion,
|
||||||
Tag,
|
Tag,
|
||||||
@@ -1163,10 +1161,11 @@ impl<'a> DocumentsService<'a> {
|
|||||||
if let Err(err) = enqueue_job(
|
if let Err(err) = enqueue_job(
|
||||||
conn,
|
conn,
|
||||||
tenant_id,
|
tenant_id,
|
||||||
JOB_INDEX_DOCUMENT_TEXT,
|
JOB_ANALYZE_DOCUMENT,
|
||||||
json!({
|
json!({
|
||||||
"document_id": document.id,
|
"document_id": document.id,
|
||||||
"document_version_id": current_version.id,
|
"document_version_id": current_version.id,
|
||||||
|
"force": false,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
) {
|
) {
|
||||||
@@ -1174,7 +1173,7 @@ impl<'a> DocumentsService<'a> {
|
|||||||
document_id = %document.id,
|
document_id = %document.id,
|
||||||
version_id = %current_version.id,
|
version_id = %current_version.id,
|
||||||
error = %err,
|
error = %err,
|
||||||
"failed to enqueue reindex job after title change"
|
"failed to enqueue analyze job after title change"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ use serde::Deserialize;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
jobs::JOB_ANALYZE_DOCUMENT, models::Document, state::AppState, storage::TenantStorage,
|
auth::ensure_active_tenant, jobs::JOB_ANALYZE_DOCUMENT, models::Document, state::AppState,
|
||||||
|
storage::TenantStorage,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -49,6 +50,12 @@ impl JobHandler for AnalyzeDocumentJob {
|
|||||||
job: crate::models::Job,
|
job: crate::models::Job,
|
||||||
storage: TenantStorage,
|
storage: TenantStorage,
|
||||||
) -> JobExecution {
|
) -> JobExecution {
|
||||||
|
if let Err(err) = ensure_active_tenant(&state, job.tenant_id) {
|
||||||
|
return JobExecution::Failed {
|
||||||
|
error: err.to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let payload: AnalyzePayload = match serde_json::from_value(job.payload.clone()) {
|
let payload: AnalyzePayload = match serde_json::from_value(job.payload.clone()) {
|
||||||
Ok(payload) => payload,
|
Ok(payload) => payload,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -1,101 +1,15 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::documents::search::{build_quickwit_ingest_record, quickwit_ingest};
|
||||||
documents::search::{build_quickwit_ingest_record, quickwit_ingest},
|
|
||||||
jobs::JOB_INDEX_DOCUMENT_TEXT,
|
|
||||||
state::AppState,
|
|
||||||
storage::TenantStorage,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
job_execution_from_task_error,
|
|
||||||
ocr::OCR_TEXT_ASSET_TYPE,
|
ocr::OCR_TEXT_ASSET_TYPE,
|
||||||
taskflow::{
|
taskflow::{document::DocumentVersionTaskContext, Task, TaskError, TaskResult},
|
||||||
document::DocumentVersionTaskContext, BoxedTask, Task, TaskError, TaskExecutor,
|
|
||||||
TaskPlanner, TaskResult,
|
|
||||||
},
|
|
||||||
JobExecution, JobHandler,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct IndexPayload {
|
|
||||||
document_id: Uuid,
|
|
||||||
document_version_id: Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct IndexDocumentTextJob;
|
|
||||||
|
|
||||||
impl IndexDocumentTextJob {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl JobHandler for IndexDocumentTextJob {
|
|
||||||
fn job_type(&self) -> &'static str {
|
|
||||||
JOB_INDEX_DOCUMENT_TEXT
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle(
|
|
||||||
&self,
|
|
||||||
state: Arc<AppState>,
|
|
||||||
job: crate::models::Job,
|
|
||||||
storage: TenantStorage,
|
|
||||||
) -> JobExecution {
|
|
||||||
let payload: IndexPayload = match serde_json::from_value(job.payload.clone()) {
|
|
||||||
Ok(payload) => payload,
|
|
||||||
Err(err) => {
|
|
||||||
return JobExecution::Failed {
|
|
||||||
error: format!("invalid index payload: {err}"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut context = DocumentVersionTaskContext::new(
|
|
||||||
job.id,
|
|
||||||
JOB_INDEX_DOCUMENT_TEXT,
|
|
||||||
job.tenant_id,
|
|
||||||
payload.document_id,
|
|
||||||
payload.document_version_id,
|
|
||||||
false,
|
|
||||||
state.config.worker_max_document_bytes,
|
|
||||||
state.clone(),
|
|
||||||
storage,
|
|
||||||
);
|
|
||||||
|
|
||||||
let planner = IndexPlanner::new();
|
|
||||||
match TaskExecutor::run(&planner, &mut context).await {
|
|
||||||
Ok(()) => JobExecution::Success,
|
|
||||||
Err(err) => job_execution_from_task_error(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct IndexPlanner;
|
|
||||||
|
|
||||||
impl IndexPlanner {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl TaskPlanner<DocumentVersionTaskContext> for IndexPlanner {
|
|
||||||
async fn plan(
|
|
||||||
&self,
|
|
||||||
_ctx: &mut DocumentVersionTaskContext,
|
|
||||||
) -> TaskResult<Vec<BoxedTask<DocumentVersionTaskContext>>> {
|
|
||||||
Ok(vec![Box::new(IndexDocumentTask::new())])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct IndexDocumentTask;
|
pub struct IndexDocumentTask;
|
||||||
|
|
||||||
impl IndexDocumentTask {
|
impl IndexDocumentTask {
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ pub fn default_handlers() -> Vec<Arc<dyn JobHandler>> {
|
|||||||
vec![
|
vec![
|
||||||
Arc::new(analyze::AnalyzeDocumentJob::new()),
|
Arc::new(analyze::AnalyzeDocumentJob::new()),
|
||||||
Arc::new(purge::PurgeDocumentJob::new()),
|
Arc::new(purge::PurgeDocumentJob::new()),
|
||||||
Arc::new(index::IndexDocumentTextJob::new()),
|
|
||||||
Arc::new(ProvisionTenantJob::new()),
|
Arc::new(ProvisionTenantJob::new()),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use diesel::result::Error as DieselError;
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::auth::ensure_active_tenant;
|
||||||
use crate::jobs::JOB_PURGE_DOCUMENT;
|
use crate::jobs::JOB_PURGE_DOCUMENT;
|
||||||
use crate::models::{Document, DocumentVersion};
|
use crate::models::{Document, DocumentVersion};
|
||||||
use crate::schema::{document_asset_objects, document_assets, document_versions};
|
use crate::schema::{document_asset_objects, document_assets, document_versions};
|
||||||
@@ -52,6 +53,12 @@ impl JobHandler for PurgeDocumentJob {
|
|||||||
job: crate::models::Job,
|
job: crate::models::Job,
|
||||||
storage: TenantStorage,
|
storage: TenantStorage,
|
||||||
) -> JobExecution {
|
) -> JobExecution {
|
||||||
|
if let Err(err) = ensure_active_tenant(&state, job.tenant_id) {
|
||||||
|
return JobExecution::Failed {
|
||||||
|
error: err.to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let payload: PurgeDocumentPayload = match serde_json::from_value(job.payload.clone()) {
|
let payload: PurgeDocumentPayload = match serde_json::from_value(job.payload.clone()) {
|
||||||
Ok(payload) => payload,
|
Ok(payload) => payload,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -8,9 +8,19 @@ use papercrate::auth::passkeys::{
|
|||||||
PasskeyLoginFinishPayload, PasskeyLoginStartPayload, PasskeyRegistrationFinishPayload,
|
PasskeyLoginFinishPayload, PasskeyLoginStartPayload, PasskeyRegistrationFinishPayload,
|
||||||
RegistrationChallengeResponse,
|
RegistrationChallengeResponse,
|
||||||
};
|
};
|
||||||
use papercrate::models::{NewUserMembership, NewUserSession, TenantStatus, UserPasskey};
|
use papercrate::models::{
|
||||||
|
MagicToken, MagicTokenKind, NewUserMembership, NewUserSession, TenantStatus, UserPasskey,
|
||||||
|
};
|
||||||
use papercrate::openapi::schemas::PasskeySummary;
|
use papercrate::openapi::schemas::PasskeySummary;
|
||||||
use papercrate::schema::{capability_sets, tenants, user_memberships, user_sessions, users};
|
use papercrate::schema::{
|
||||||
|
capability_sets,
|
||||||
|
magic_tokens::dsl as magic_dsl,
|
||||||
|
tenants,
|
||||||
|
tenants::dsl as tenant_dsl,
|
||||||
|
user_memberships,
|
||||||
|
user_sessions,
|
||||||
|
users,
|
||||||
|
};
|
||||||
use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole};
|
use papercrate::test_support::{acquire_db_lock, body_to_vec, TestApp, TestUserRole};
|
||||||
use rand::{rngs::OsRng, TryRngCore};
|
use rand::{rngs::OsRng, TryRngCore};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
@@ -678,3 +688,71 @@ fn hash_session_token(value: &str) -> String {
|
|||||||
hasher.update(value.as_bytes());
|
hasher.update(value.as_bytes());
|
||||||
hex::encode(hasher.finalize())
|
hex::encode(hasher.finalize())
|
||||||
}
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tenant_selection_excludes_inactive_tenants() -> Result<()> {
|
||||||
|
let _lock = acquire_db_lock().await;
|
||||||
|
let app = TestApp::new().await?;
|
||||||
|
|
||||||
|
let user_id = app.insert_user("tenant-user", TestUserRole::Owner).await?;
|
||||||
|
|
||||||
|
let tenant_id = app
|
||||||
|
.with_conn(|conn| {
|
||||||
|
let tenant: papercrate::models::Tenant = tenant_dsl::tenants
|
||||||
|
.filter(tenant_dsl::name.eq("test_tenant"))
|
||||||
|
.first(conn)?;
|
||||||
|
Ok::<_, anyhow::Error>(tenant.id)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
app.with_conn(move |conn| {
|
||||||
|
diesel::update(tenant_dsl::tenants.find(tenant_id))
|
||||||
|
.set(tenant_dsl::status.eq(TenantStatus::Suspended))
|
||||||
|
.execute(conn)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let magic_value = "tenant-status-token";
|
||||||
|
let token_hash = {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(magic_value.as_bytes());
|
||||||
|
hex::encode(hasher.finalize())
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id_for_token = user_id;
|
||||||
|
app.with_conn(move |conn| {
|
||||||
|
let token = MagicToken {
|
||||||
|
id: Uuid::new_v4(),
|
||||||
|
user_id: user_id_for_token,
|
||||||
|
kind: MagicTokenKind::EmailLogin,
|
||||||
|
token_hash,
|
||||||
|
metadata: json!({}),
|
||||||
|
expires_at: (Utc::now() + ChronoDuration::hours(1)).naive_utc(),
|
||||||
|
max_uses: None,
|
||||||
|
used_count: 0,
|
||||||
|
created_at: Utc::now().naive_utc(),
|
||||||
|
created_by: None,
|
||||||
|
last_used_at: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::insert_into(magic_dsl::magic_tokens)
|
||||||
|
.values(&token)
|
||||||
|
.execute(conn)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload = json!({
|
||||||
|
"username": "tenant-user",
|
||||||
|
"magic_token": magic_value,
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = app.post_json("/api/auth/login", &payload, None).await?;
|
||||||
|
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||||
|
let body = body_to_vec(response.into_body()).await?;
|
||||||
|
let err: ApiErrorResponse = serde_json::from_slice(&body)?;
|
||||||
|
assert_eq!(err.error, "no active tenants available");
|
||||||
|
|
||||||
|
app.cleanup().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user