Skip to content

Commit 6497365

Browse files
committed
test(files): cover selected tenant authorization
1 parent 77c27c8 commit 6497365

1 file changed

Lines changed: 99 additions & 11 deletions

File tree

crates/schema-forge-acton/tests/file_authorization.rs

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use schema_forge_acton::{
1919
storage::StorageRegistry,
2020
DynSchemaBackend, ForgeActor,
2121
};
22-
use schema_forge_backend::entity::Entity;
22+
use schema_forge_backend::{entity::Entity, tenant::TenantConfig};
2323
use schema_forge_core::types::{
2424
Annotation, DynamicValue, EntityId, FieldAnnotation, FieldDefinition, FieldName, FieldType,
2525
FileAccess, FileConstraints, MimePattern, SchemaDefinition, SchemaId, SchemaName, TenantKind,
@@ -134,6 +134,22 @@ async fn fixture_with_owner(
134134
DynEntityStore::create(backend.as_ref(), &entity)
135135
.await
136136
.unwrap();
137+
let organization = SchemaDefinition::new(
138+
SchemaId::new(),
139+
SchemaName::new("Organization").unwrap(),
140+
vec![FieldDefinition::new(
141+
FieldName::new("name").unwrap(),
142+
FieldType::Text(TextConstraints::unconstrained()),
143+
)],
144+
vec![Annotation::Tenant(TenantKind::Root)],
145+
)
146+
.unwrap();
147+
let tenant_config =
148+
TenantConfig::from_schemas(&[organization.clone(), schema.clone()]).unwrap();
149+
let tenant_scope_state = schema_forge_acton::middleware::tenant_scope::TenantScopeState {
150+
entity_store: backend.clone(),
151+
tenant_config: Arc::new(Some(tenant_config.clone())),
152+
};
137153
let service = ServiceBuilder::new()
138154
.with_config(Config::<SchemaForgeConfig>::default())
139155
.with_actor::<ForgeActor>()
@@ -144,9 +160,12 @@ async fn fixture_with_owner(
144160
.actor::<ForgeActor>()
145161
.unwrap()
146162
.send(InitForge {
147-
registry: HashMap::from([("Document".into(), schema)]),
163+
registry: HashMap::from([
164+
("Document".into(), schema),
165+
("Organization".into(), organization),
166+
]),
148167
backend,
149-
tenant_config: None,
168+
tenant_config: Some(tenant_config),
150169
record_access_policy: None,
151170
hook_dispatcher: None,
152171
storage_registry: StorageRegistry::default(),
@@ -160,6 +179,10 @@ async fn fixture_with_owner(
160179
.unwrap()
161180
.unwrap();
162181
let app = forge_routes()
182+
.layer(axum::middleware::from_fn_with_state(
183+
tenant_scope_state,
184+
schema_forge_acton::middleware::tenant_scope::middleware,
185+
))
163186
.layer(axum::middleware::from_fn(
164187
move |mut req: axum::extract::Request, next: axum::middleware::Next| {
165188
let caller = caller.clone();
@@ -179,6 +202,15 @@ async fn fixture_with_owner(
179202
}
180203

181204
async fn status(app: &Router, path: &str, operation: &str) -> StatusCode {
205+
status_with_tenant(app, path, operation, None).await
206+
}
207+
208+
async fn status_with_tenant(
209+
app: &Router,
210+
path: &str,
211+
operation: &str,
212+
tenant: Option<&str>,
213+
) -> StatusCode {
182214
let (method, suffix, body) = match operation {
183215
"download" => ("GET", "?redirect=false", ""),
184216
"mint" => (
@@ -193,16 +225,16 @@ async fn status(app: &Router, path: &str, operation: &str) -> StatusCode {
193225
),
194226
_ => ("POST", "/scan-complete", r#"{"status":"available"}"#),
195227
};
228+
let mut request = Request::builder()
229+
.method(method)
230+
.uri(format!("{path}{suffix}"))
231+
.header("content-type", "application/json");
232+
if let Some(tenant) = tenant {
233+
request = request.header("x-active-tenant", tenant);
234+
}
196235
let response = app
197236
.clone()
198-
.oneshot(
199-
Request::builder()
200-
.method(method)
201-
.uri(format!("{path}{suffix}"))
202-
.header("content-type", "application/json")
203-
.body(Body::from(body))
204-
.unwrap(),
205-
)
237+
.oneshot(request.body(Body::from(body)).unwrap())
206238
.await
207239
.unwrap();
208240
let status = response.status();
@@ -353,3 +385,59 @@ async fn same_tenant_cannot_modify_another_owners_file() {
353385
StatusCode::INTERNAL_SERVER_ERROR
354386
);
355387
}
388+
389+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
390+
async fn selected_tenant_scopes_files_for_multi_membership_callers() {
391+
let mut caller = claims(None, &["editor"]);
392+
caller.custom.insert(
393+
"tenant_chain".into(),
394+
serde_json::json!([
395+
{"schema":"Organization","entity_id":"organization_alpha"},
396+
{"schema":"Organization","entity_id":"organization_beta"}
397+
]),
398+
);
399+
let (app, path) = fixture(Some(caller), false).await;
400+
for (tenant, expected_entity) in [
401+
(Some("Organization:organization_alpha"), StatusCode::OK),
402+
(
403+
Some("Organization:organization_beta"),
404+
StatusCode::FORBIDDEN,
405+
),
406+
(None, StatusCode::BAD_REQUEST),
407+
(
408+
Some("Organization:organization_gamma"),
409+
StatusCode::FORBIDDEN,
410+
),
411+
] {
412+
let mut request = Request::builder().uri(path.trim_end_matches("/fields/attachment"));
413+
if let Some(tenant) = tenant {
414+
request = request.header("x-active-tenant", tenant);
415+
}
416+
let entity_response = app
417+
.clone()
418+
.oneshot(request.body(Body::empty()).unwrap())
419+
.await
420+
.unwrap();
421+
assert_eq!(
422+
entity_response.status(),
423+
expected_entity,
424+
"entity {tenant:?}"
425+
);
426+
for operation in ["download", "mint", "confirm"] {
427+
let expected = if expected_entity == StatusCode::OK {
428+
if operation == "confirm" {
429+
StatusCode::UNPROCESSABLE_ENTITY
430+
} else {
431+
StatusCode::INTERNAL_SERVER_ERROR
432+
}
433+
} else {
434+
expected_entity
435+
};
436+
assert_eq!(
437+
status_with_tenant(&app, &path, operation, tenant).await,
438+
expected,
439+
"{operation} {tenant:?}"
440+
);
441+
}
442+
}
443+
}

0 commit comments

Comments
 (0)