2222import org .jetbrains .annotations .Nullable ;
2323import org .junit .Assert ;
2424import org .junit .Test ;
25+ import org .labkey .api .data .ContainerManager ;
2526import org .labkey .api .data .CoreSchema ;
2627import org .labkey .api .data .DbScope ;
2728import org .labkey .api .data .DbScope .Transaction ;
3940import org .labkey .api .query .FieldKey ;
4041import org .labkey .api .security .UserManager .SessionHandler ;
4142import org .labkey .api .security .ValidEmail .InvalidEmailException ;
43+ import org .labkey .api .security .permissions .AdminPermission ;
44+ import org .labkey .api .security .permissions .DeletePermission ;
45+ import org .labkey .api .security .permissions .InsertPermission ;
46+ import org .labkey .api .security .permissions .ReadPermission ;
47+ import org .labkey .api .security .permissions .UpdatePermission ;
48+ import org .labkey .api .security .roles .EditorRole ;
49+ import org .labkey .api .security .roles .ReaderRole ;
50+ import org .labkey .api .security .roles .Role ;
51+ import org .labkey .api .security .roles .RoleManager ;
4252import org .labkey .api .settings .AppProps ;
4353import org .labkey .api .settings .LenientStartupPropertyHandler ;
4454import org .labkey .api .settings .StartupProperty ;
6070import java .util .Map ;
6171import java .util .Objects ;
6272import java .util .Set ;
73+ import java .util .stream .Stream ;
6374
6475import static org .labkey .api .util .IntegerUtils .asInteger ;
6576
@@ -85,9 +96,9 @@ private ApiKeyManager()
8596 * @param user User to be associated with the new API key.
8697 * @return An API key that expires after the admin-configured duration
8798 */
88- public @ NotNull String createKey (@ NotNull User user , @ Nullable String description )
99+ public @ NotNull String createKey (@ NotNull User user , @ Nullable String description , @ Nullable Class <? extends Role > restrictionRole )
89100 {
90- return createKey (user , AppProps .getInstance ().getApiKeyExpirationSeconds (), description );
101+ return createKey (user , AppProps .getInstance ().getApiKeyExpirationSeconds (), description , restrictionRole );
91102 }
92103
93104 /**
@@ -97,6 +108,18 @@ private ApiKeyManager()
97108 * @return An API key that expires after the specified number of seconds
98109 */
99110 public @ NotNull String createKey (@ NotNull User user , int expirationSeconds , @ Nullable String description )
111+ {
112+ return createKey (user , expirationSeconds , description , null );
113+ }
114+
115+ /**
116+ * Create an API key associated with a user and persist it in the database.
117+ * @param user User to be associated with the new API key.
118+ * @param expirationSeconds Number of seconds until expiration. -1 means no expiration.
119+ * @param restrictionRole Role class that limits this API key's permissions. null means no restrictions.
120+ * @return An API key that expires after the specified number of seconds
121+ */
122+ public @ NotNull String createKey (@ NotNull User user , int expirationSeconds , @ Nullable String description , @ Nullable Class <? extends Role > restrictionRole )
100123 {
101124 if (user .isGuest ())
102125 throw new IllegalStateException ("Can't create an API key for a guest" );
@@ -120,6 +143,9 @@ private ApiKeyManager()
120143 if (description != null )
121144 map .put ("Description" , StringUtils .abbreviate (description .trim (), 256 ));
122145
146+ if (restrictionRole != null )
147+ map .put ("RestrictionRole" , restrictionRole .getName ());
148+
123149 try (Transaction t = CoreSchema .getInstance ().getScope ().beginTransaction (TRANSACTION_KIND ))
124150 {
125151 Table .insert (user , CoreSchema .getInstance ().getTableAPIKeys (), map );
@@ -183,17 +209,35 @@ public void updateLastUsed(String apikey)
183209
184210 try (Transaction t = scope .beginTransaction (TRANSACTION_KIND ))
185211 {
186- SQLFragment sql = new SQLFragment ("UPDATE " + CoreSchema .getInstance ().getTableAPIKeys () + " SET LastUsed = ? WHERE Crypt = ?" , new Date (), crypt (apikey ));
212+ SQLFragment sql = new SQLFragment ("UPDATE " )
213+ .append (CoreSchema .getInstance ().getTableAPIKeys ())
214+ .append (" SET LastUsed = ? WHERE Crypt = ?" )
215+ .add (new Date ())
216+ .add (crypt (apikey ));
187217 new SqlExecutor (scope ).execute (sql );
188218 t .commit ();
189219 }
190220 }
191221
192- public record ApiKeyAuthentication (int createdBy , int rowId )
222+ public record ApiKeyAuthentication (int createdBy , int rowId , @ Nullable Class <? extends Role > restrictionRole )
193223 {
194- public User getUser ()
224+ public @ Nullable User getUser ()
195225 {
196- return UserManager .getUser (createdBy ());
226+ User user = UserManager .getUser (createdBy ());
227+ if (restrictionRole != null )
228+ {
229+ Role role = RoleManager .getRole (restrictionRole );
230+ if (role == null )
231+ {
232+ LOG .error ("API key for {} specifies a restriction role {} that was not found" , user , restrictionRole .getName ());
233+ user = null ;
234+ }
235+ else
236+ {
237+ user = new PermissionsRestrictedUser (user , role .getPermissions ());
238+ }
239+ }
240+ return user ;
197241 }
198242 }
199243
@@ -212,7 +256,7 @@ public User getUser()
212256
213257 try (Transaction t = CoreSchema .getInstance ().getScope ().beginTransaction (TRANSACTION_KIND ))
214258 {
215- ret = new TableSelector (CoreSchema .getInstance ().getTableAPIKeys (), Set .of ("CreatedBy" , "RowId" ), filter , null ).getObject (ApiKeyAuthentication .class );
259+ ret = new TableSelector (CoreSchema .getInstance ().getTableAPIKeys (), Set .of ("CreatedBy" , "RowId" , "RestrictionRole" ), filter , null ).getObject (ApiKeyAuthentication .class );
216260 t .commit ();
217261 }
218262
@@ -327,6 +371,53 @@ public void testTransaction()
327371 ApiKeyManager .get ().deleteKey (apikey );
328372 assertNull (ApiKeyManager .get ().authenticateFromApiKey (apikey ));
329373 }
374+
375+ private record UserAndKey (User user , String apiKey ){}
376+
377+ @ Test
378+ public void testRoleRestrictions ()
379+ {
380+ User admin = TestContext .get ().getUser ();
381+ UserAndKey readerUAK = createApiKeyAndRetrieveUser (admin , ReaderRole .class );
382+ User reader = readerUAK .user ();
383+ UserAndKey editorUAK = createApiKeyAndRetrieveUser (admin , EditorRole .class );
384+ User editor = editorUAK .user ();
385+
386+ ContainerManager .getAllChildren (ContainerManager .getRoot (), admin , AdminPermission .class ).stream ()
387+ .limit (5 )
388+ .forEach (child -> {
389+ assertTrue (child .hasPermission (admin , AdminPermission .class ));
390+ assertFalse (child .hasPermission (editor , AdminPermission .class ));
391+ assertFalse (child .hasPermission (reader , AdminPermission .class ));
392+ Stream .of (DeletePermission .class , UpdatePermission .class , InsertPermission .class )
393+ .forEach (perm -> {
394+ assertTrue (child .hasPermission (admin , perm ));
395+ assertTrue (child .hasPermission (editor , perm ));
396+ assertFalse (child .hasPermission (reader , perm ));
397+ });
398+ assertTrue (child .hasPermission (admin , ReadPermission .class ));
399+ assertTrue (child .hasPermission (editor , ReadPermission .class ));
400+ assertTrue (child .hasPermission (reader , ReadPermission .class ));
401+ });
402+
403+ ApiKeyManager .get ().deleteKey (readerUAK .apiKey ());
404+ ApiKeyManager .get ().deleteKey (editorUAK .apiKey ());
405+ assertNull (ApiKeyManager .get ().authenticateFromApiKey (readerUAK .apiKey ()));
406+ assertNull (ApiKeyManager .get ().authenticateFromApiKey (editorUAK .apiKey ()));
407+ }
408+
409+ private UserAndKey createApiKeyAndRetrieveUser (User user , Class <? extends Role > restrictionRole )
410+ {
411+ String apiKey = ApiKeyManager .get ().createKey (user , 10 , "Created by ApiKeyManager.TestCase" , restrictionRole );
412+ ApiKeyAuthentication auth = ApiKeyManager .get ().authenticateFromApiKey (apiKey );
413+ assertNotNull (auth );
414+ User restrictedUser = auth .getUser ();
415+ assertNotNull (restrictedUser );
416+ assertEquals (user .getUserId (), restrictedUser .getUserId ());
417+ assertTrue (restrictedUser instanceof PermissionsRestrictedUser );
418+
419+ return new UserAndKey (restrictedUser , apiKey );
420+ }
330421 }
331422
332423 public static class ApiKeyMaintenanceTask implements MaintenanceTask
0 commit comments