diff --git a/src/org/labkey/test/components/core/ApiKeyDialog.java b/src/org/labkey/test/components/core/ApiKeyDialog.java index bed036e0ff..3841551a0d 100644 --- a/src/org/labkey/test/components/core/ApiKeyDialog.java +++ b/src/org/labkey/test/components/core/ApiKeyDialog.java @@ -18,6 +18,7 @@ import org.labkey.test.Locator; import org.labkey.test.components.bootstrap.ModalDialog; import org.labkey.test.components.html.Input; +import org.labkey.test.components.html.OptionSelect; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; @@ -151,6 +152,12 @@ public String getDescription() return elementCache().descriptionDisplay.getText(); } + public ApiKeyDialog setRestrictionRole(String restrictionRole) + { + elementCache().restrictionRoleSelect.selectByVisibleText(restrictionRole); + return this; + } + public String getInputFieldValue() { return elementCache().inputField.getValue(); @@ -172,6 +179,8 @@ protected class ElementCache extends ModalDialog.ElementCache { Input descriptionInput = Input.Input(Locator.tagWithId("input", "keyDescription"), getDriver()).findWhenNeeded(this); WebElement descriptionDisplay = Locator.tagWithClassContaining("div", "api-key__description").findWhenNeeded(this); + OptionSelect restrictionRoleSelect = new OptionSelect<>(Locator.tagWithId("select", "keyRole") + .findWhenNeeded(this).withTimeout(2000)); WebElement generateApiKeyButton = Locator.tagWithText("button", "Generate API Key").findWhenNeeded(this); Input inputField = Input.Input(Locator.tagWithClass("input", "api-key__input"), getDriver()).findWhenNeeded(this); WebElement copyKeyButton = Locator.tagWithName("button", "copy_apikey_token").findWhenNeeded(this); diff --git a/src/org/labkey/test/components/core/ApiKeyPanel.java b/src/org/labkey/test/components/core/ApiKeyPanel.java index be2cdbc1c6..6142dc3c01 100644 --- a/src/org/labkey/test/components/core/ApiKeyPanel.java +++ b/src/org/labkey/test/components/core/ApiKeyPanel.java @@ -36,15 +36,19 @@ public static SimpleWebDriverComponentFinder panelFinder(WebDriver return new Panel.PanelFinder(driver).withTitle("API Keys").wrap(ApiKeyPanel::new); } - public String generateApiKey(@Nullable String description) + public String generateApiKey(@Nullable String description, @Nullable String restrictionRole) { ApiKeyDialog apiKeyDialog = clickGenerateApiKey(); if (description != null) { apiKeyDialog.setDescription(description); } + if (restrictionRole != null) + { + apiKeyDialog.setRestrictionRole(restrictionRole); + } apiKeyDialog.generateApiKey(); - Assert.assertEquals("API Key discription", description == null ? "" : description, apiKeyDialog.getDescription()); + Assert.assertEquals("API Key description", description == null ? "" : description, apiKeyDialog.getDescription()); String inputFieldValue = apiKeyDialog.getInputFieldValue(); apiKeyDialog.clickDone(); return inputFieldValue; @@ -52,8 +56,9 @@ public String generateApiKey(@Nullable String description) public String generateApiKey() { - return generateApiKey(null); + return generateApiKey(null, null); } + public ApiKeyDialog clickGenerateApiKey() { elementCache().generateApiKeyButton.click(); diff --git a/src/org/labkey/test/tests/ApiKeyTest.java b/src/org/labkey/test/tests/ApiKeyTest.java index 369abff4cc..cbcc813e9f 100644 --- a/src/org/labkey/test/tests/ApiKeyTest.java +++ b/src/org/labkey/test/tests/ApiKeyTest.java @@ -50,6 +50,7 @@ import org.labkey.test.pages.core.admin.CustomizeSitePage; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.list.IntListDefinition; +import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.Maps; import org.labkey.test.util.PasswordUtil; import org.labkey.test.util.TestUser; @@ -362,6 +363,35 @@ public void testSessionKeyDisabled() throws IOException } } + @Test + public void testRestrictedApiKey() throws IOException + { + List> _generatedApiKeys = new ArrayList<>(); + + goToAdminConsole() + .clickSiteSettings() + .setAllowApiKeys(true) + .setApiKeyExpiration(CustomizeSitePage.KeyExpirationOptions.ONE_WEEK) + .save(); + + String apiKey = generateAPIKey(null, "Reader"); + _generatedApiKeys.add(getLastAPIKeyRecord()); + + // Test connection that does not keep a session + log("Verify active API key via basic authentication"); + Connection cn = createBasicAuthConnection(apiKey); + verifyReadOnlyAPIKey(cn); + + // Test connection that does keep a session + cn = createApiKeyConnection(apiKey); + log("Verify active API key via api authentication"); + verifyReadOnlyAPIKey(cn); + + log("Verify revoked/deleted api key"); + deleteAPIKeys(_generatedApiKeys); + verifyInvalidAPIKey(createApiKeyConnection(apiKey), false); + } + private void verifyValidAPIKey(Connection connection) throws IOException { verifyValidAPIKey(connection, PasswordUtil.getUsername()); @@ -397,6 +427,37 @@ private void verifyValidAPIKey(Connection connection, String userEmail) throws I } } + private void verifyReadOnlyAPIKey(Connection connection) throws IOException + { + try + { + WhoAmIResponse whoAmI = new WhoAmICommand().execute(connection, null); + assertEquals("Connection user", PasswordUtil.getUsername(), whoAmI.getEmail()); + + // Likely multiple permissions (depending on what modules are running), but all should end with "ReadPermission" + List badPerms = new ApiPermissionsHelper(this, () -> connection).getUserPermissions(getProjectName(), null).stream() + .filter(perm -> !perm.endsWith("ReadPermission")) + .toList(); + + assertFalse("Unexpected permissions: " + badPerms, badPerms.isEmpty()); + + // Should be able to select rows + QueryApiHelper queryApiHelper = new QueryApiHelper(connection, getProjectName(), "lists", LIST_NAME); + SelectRowsResponse selectResponse = queryApiHelper.selectRows(); + assertEquals("Total rows", valueCount.get(), selectResponse.getRowCount()); + + // Should NOT be able to import, insert, update, or delete + Assert.assertThrows("User does not have permission to insert rows", CommandException.class, () -> queryApiHelper.importData(LIST_VALUE + "\nvalue" + valueCount.get())); + Assert.assertThrows("User does not have permission to perform this operation.", CommandException.class, () -> queryApiHelper.insertRows(List.of())); + Assert.assertThrows("User does not have permission to perform this operation.", CommandException.class, () -> queryApiHelper.updateRows(List.of())); + Assert.assertThrows("User does not have permission to perform this operation.", CommandException.class, () -> queryApiHelper.deleteRows(List.of())); + } + catch (CommandException e) + { + throw new RuntimeException("Response: " + e.getStatusCode(), e); + } + } + private void verifySessionKeyCsrf(Connection connection) throws IOException { try @@ -484,9 +545,14 @@ private String generateSessionKey() } private String generateAPIKey(@Nullable String description) + { + return generateAPIKey(description, null); + } + + private String generateAPIKey(@Nullable String description, @Nullable String restrictionRole) { goToExternalToolPage(); - return ApiKeyPanel.panelFinder(getDriver()).find().generateApiKey(description); + return ApiKeyPanel.panelFinder(getDriver()).find().generateApiKey(description, restrictionRole); } private String generateAPIKeyAndRecord(List> _generatedApiKeys) throws IOException diff --git a/src/org/labkey/test/util/ApiPermissionsHelper.java b/src/org/labkey/test/util/ApiPermissionsHelper.java index deda75e5d0..ff94975a67 100644 --- a/src/org/labkey/test/util/ApiPermissionsHelper.java +++ b/src/org/labkey/test/util/ApiPermissionsHelper.java @@ -16,6 +16,7 @@ package org.labkey.test.util; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; import org.junit.Assert; import org.labkey.remoteapi.CommandException; import org.labkey.remoteapi.CommandResponse; @@ -248,7 +249,7 @@ public Integer getGroupId(String groupName) return id; } - public Integer getUserId(String user) + public Integer getUserId(@Nullable String user) { try { @@ -291,12 +292,12 @@ private List getGroupMembers(String container, String groupName) return (List)response.getParsedData().get("groupMembers"); } - private List> getUserGroups(String container, String user) throws CommandException + private List> getUserGroups(String container, @Nullable String user) throws CommandException { return getUserPerms(container, user).getProperty("container.groups"); } - public List getUserRoles(String container, String user) + public List getUserRoles(String container, @Nullable String user) { try { @@ -308,7 +309,20 @@ public List getUserRoles(String container, String user) } } - private CommandResponse getUserPerms(String container, String user) throws CommandException + public List getUserPermissions(String container, @Nullable String user) + { + try + { + return getUserPerms(container, user).getProperty("container.effectivePermissions"); + } + catch (CommandException e) + { + throw new RuntimeException(e); + } + } + + // Pass null for current user + private CommandResponse getUserPerms(String container, @Nullable String user) throws CommandException { Connection connection = getConnection(); SimpleGetCommand command = new SimpleGetCommand("security", "getUserPerms");