@@ -14,9 +14,7 @@ public sealed class SandboxUAuthAccessPolicy(IDbContextFactory<LabDbContext> dbF
1414 public bool AppliesTo ( AccessContext context )
1515 => IsProtectedAction ( context . Action ) ;
1616
17- public async ValueTask < AccessDecision > DecideAsync (
18- AccessContext context ,
19- CancellationToken cancellationToken = default )
17+ public async ValueTask < AccessDecision > DecideAsync ( AccessContext context , CancellationToken cancellationToken = default )
2018 {
2119 cancellationToken . ThrowIfCancellationRequested ( ) ;
2220
@@ -45,8 +43,7 @@ public async ValueTask<AccessDecision> DecideAsync(
4543 if ( rootResolver . IsLadmin ( context ) )
4644 return AccessDecision . Allow ( ) ;
4745
48- await using var db = await dbFactory . CreateDbContextAsync (
49- cancellationToken ) ;
46+ await using var db = await dbFactory . CreateDbContextAsync ( cancellationToken ) ;
5047
5148 var actor = await db . SandboxMembers
5249 . AsNoTracking ( )
@@ -70,18 +67,35 @@ public async ValueTask<AccessDecision> DecideAsync(
7067 if ( operation == SandboxOperation . Denied )
7168 return AccessDecision . Deny ( "sandbox_operation_restricted" ) ;
7269
73- if ( actor . Status == SandboxStatus . Passive &&
74- operation != SandboxOperation . Read )
70+ if ( actor . Status == SandboxStatus . Passive && operation != SandboxOperation . Read )
7571 {
7672 return AccessDecision . Deny ( "sandbox_read_only" ) ;
7773 }
7874
79- if ( operation == SandboxOperation . Manage &&
80- actor . Role != SandboxMemberRole . Owner )
75+ if ( operation == SandboxOperation . Manage && actor . Role != SandboxMemberRole . Owner )
8176 {
8277 return AccessDecision . Deny ( "sandbox_owner_required" ) ;
8378 }
8479
80+ if ( action == UAuthActions . Users . GetByKeysAdmin )
81+ {
82+ var requestedKeys = context . TargetUserKeys ;
83+
84+ if ( requestedKeys . Count == 0 )
85+ return AccessDecision . Deny ( "sandbox_targets_required" ) ;
86+
87+ var keys = requestedKeys . Distinct ( ) . ToArray ( ) ;
88+
89+ var allowedCount = await db . SandboxMembers
90+ . AsNoTracking ( )
91+ . CountAsync ( x => x . SandboxId == actor . SandboxId && keys . Contains ( x . UserKey ) , cancellationToken ) ;
92+
93+ if ( allowedCount != keys . Length )
94+ {
95+ return AccessDecision . Deny ( "sandbox_target_out_of_scope" ) ;
96+ }
97+ }
98+
8599 // All target-based admin operations must remain
86100 // inside the actor's sandbox.
87101 if ( RequiresTarget ( action ) )
@@ -91,10 +105,7 @@ public async ValueTask<AccessDecision> DecideAsync(
91105
92106 var belongsToSandbox = await db . SandboxMembers
93107 . AsNoTracking ( )
94- . AnyAsync (
95- x => x . UserKey == targetKey &&
96- x . SandboxId == actor . SandboxId ,
97- cancellationToken ) ;
108+ . AnyAsync ( x => x . UserKey == targetKey && x . SandboxId == actor . SandboxId , cancellationToken ) ;
98109
99110 if ( ! belongsToSandbox )
100111 return AccessDecision . Deny ( "sandbox_target_out_of_scope" ) ;
@@ -138,18 +149,14 @@ private static SandboxOperation ClassifyAction(string action)
138149 {
139150 // User creation requires a controlled sandbox
140151 // creation workflow, including membership and capacity.
141- if ( action is
142- UAuthActions . Users . CreateAdmin or
143- UAuthActions . Users . QueryAdmin )
152+ if ( action is UAuthActions . Users . CreateAdmin or UAuthActions . Users . QueryAdmin )
144153 {
145154 return SandboxOperation . Denied ;
146155 }
147156
148157 // Global role definitions and assignments are
149158 // handled separately by LabApplicationPolicy.
150- if ( action . StartsWith (
151- "authorization.roles." ,
152- StringComparison . Ordinal ) )
159+ if ( action . StartsWith ( "authorization.roles." , StringComparison . Ordinal ) )
153160 {
154161 return action is
155162 UAuthActions . Authorization . Roles . GetSelf or
@@ -173,6 +180,7 @@ UAuthActions.Authorization.Roles.GetAdmin or
173180
174181 private static bool IsReadAction ( string action )
175182 => action is
183+ UAuthActions . Users . GetByKeysAdmin or
176184 UAuthActions . UserProfiles . GetSelf or
177185 UAuthActions . UserProfiles . GetAdmin or
178186 UAuthActions . UserIdentifiers . GetSelf or
0 commit comments