@@ -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 } ;
2323use 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
181204async 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