Skip to content

Commit

Permalink
Automation test for invalidate sessions. (#2234)
Browse files Browse the repository at this point in the history
  • Loading branch information
labkey-sweta authored Jan 27, 2025
1 parent 76fde62 commit 2d7e93d
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ public enum XFrameOption
public enum KeyExpirationOptions implements OptionSelect.SelectOption
{
UNLIMITED(-1),
TEN_SECONDS(10),
ONE_WEEK(7*SECONDS_PER_DAY),
ONE_MONTH(30*SECONDS_PER_DAY),
THREE_MONTHS(90*SECONDS_PER_DAY),
Expand Down
12 changes: 12 additions & 0 deletions src/org/labkey/test/pages/user/UserDetailsPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import org.labkey.test.Locator;
import org.labkey.test.WebDriverWrapper;
import org.labkey.test.WebTestHelper;
import org.labkey.test.components.core.login.SetPasswordForm;
import org.labkey.test.pages.LabKeyPage;
import org.labkey.test.util.PasswordUtil;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

Expand Down Expand Up @@ -34,6 +36,15 @@ public ClonePermissionsPage clickClonePermission()
return new ClonePermissionsPage(getDriver());
}

public SetPasswordForm clickChangePassword()
{
if (PasswordUtil.getUsername().equals(getCurrentUser()))
throw new IllegalArgumentException("Don't change the primary site admin user's password");

clickAndWait(elementCache().changePwdButton);
return new SetPasswordForm(getDriver());
}

@Override
protected ElementCache newElementCache()
{
Expand All @@ -44,5 +55,6 @@ protected class ElementCache extends LabKeyPage<?>.ElementCache
{
WebElement editButton = Locator.lkButton("Edit").findWhenNeeded(this);
WebElement cloneButton = Locator.lkButton("Clone Permissions").findWhenNeeded(this);
WebElement changePwdButton = Locator.lkButton("Change Password").findWhenNeeded(this);
}
}
179 changes: 114 additions & 65 deletions src/org/labkey/test/tests/ApiKeyTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,21 @@ public class ApiKeyTest extends BaseWebDriverTest
private static final String API_USERNAME = "apikey";
private static final TestUser EDITOR_USER = new TestUser("editor@apikey.test");

@Override
protected void doCleanup(boolean afterTest) throws TestTimeoutException
{
super.doCleanup(afterTest);
_userHelper.deleteUsers(false, EDITOR_USER);
}

@BeforeClass
public static void setupProject()
{
ApiKeyTest init = (ApiKeyTest) getCurrentTest();
ApiKeyTest init = getCurrentTest();

init.doSetup();
}

@Override
protected void doCleanup(boolean afterTest) throws TestTimeoutException
{
super.doCleanup(afterTest);
_userHelper.deleteUsers(false, EDITOR_USER);
}

private void doSetup()
{
_containerHelper.createProject(getProjectName(), null);
Expand All @@ -107,64 +107,21 @@ public void testSessionKey() throws IOException

String apiKey = generateSessionKey();

verifyValidAPIKey(apiKey);
verifyValidAPIKey(createApiKeyConnection(apiKey, false));

log("Verify session key remains valid if key generation is turned off");
goToAdminConsole()
.clickSiteSettings()
.setAllowSessionKeys(false)
.save();
verifyValidAPIKey(apiKey);
verifyValidAPIKey(createApiKeyConnection(apiKey, false));

signOut();
log("Verify that logging out invalidates session keys");
verifyInvalidAPIKey(apiKey);
verifyInvalidAPIKey(createApiKeyConnection(apiKey, false), false);
simpleSignIn();
log("Verify that session keys remain invalid after logging back in");
verifyInvalidAPIKey(apiKey);
}

private void verifyValidAPIKey(String apiKey) throws IOException
{
verifyValidAPIKey(apiKey, false);
}

private void verifyValidAPIKey(String apiKey, boolean basicAuth) throws IOException
{
Connection cn = new Connection(WebTestHelper.getBaseURL(), basicAuth ? new BasicAuthCredentialsProvider(API_USERNAME, apiKey) : new ApiKeyCredentialsProvider(apiKey));
try
{
GetSchemasCommand cmd = new GetSchemasCommand();
GetSchemasResponse resp = cmd.execute(cn, getProjectName());
List<String> schemaNames = resp.getSchemaNames().stream().map(String::toLowerCase).collect(Collectors.toList());
Set<String> missingSchemas = new HashSet<>(Arrays.asList("pipeline", "lists", "core"));
missingSchemas.removeAll(schemaNames);
assertTrue("Some expected schemas missing. Schemas missing: " + missingSchemas, missingSchemas.isEmpty());
}
catch (CommandException e)
{
throw new RuntimeException("Response: " + e.getStatusCode(), e);
}
}

private void verifyInvalidAPIKey(String apiKey) throws IOException
{
boolean isSessionKey = !apiKey.startsWith(API_USERNAME);
Connection cn = new Connection(WebTestHelper.getBaseURL(), new ApiKeyCredentialsProvider(apiKey));
try
{
GetSchemasCommand cmd = new GetSchemasCommand();
cmd.execute(cn, getProjectName());
if (isSessionKey)
fail("Session key didn't invalidate after logout");
else
fail("API key should no longer be valid");
}
catch(CommandException e)
{
assertEquals("Wrong response for invalid " + (isSessionKey ? "session" : "API") + " key", HttpStatus.SC_UNAUTHORIZED, e.getStatusCode());
log("Success: command failed as expected.");
}
verifyInvalidAPIKey(createApiKeyConnection(apiKey, false), false);
}

@Test
Expand All @@ -182,14 +139,14 @@ public void testNonAdminUser() throws IOException
signIn(EDITOR_USER.getEmail(), EDITOR_USER.getPassword());
String keyDescription = "Key for editing";
String apiKey = generateAPIKey(keyDescription);
verifyValidAPIKey(apiKey);
verifyValidAPIKey(createApiKeyConnection(apiKey, false));

QueryGrid grid = new QueryGrid.QueryGridFinder(getDriver()).waitFor();
int beforeDeleteCount = grid.getRecordCount();
assertFalse("Row with description not found", grid.getRowMap("Description", keyDescription).isEmpty());
grid = deleteAPIKeyViaUI();
assertEquals("Number of keys after UI deletion not as expected", beforeDeleteCount-1, grid.getRecordCount());
verifyInvalidAPIKey(apiKey);
assertEquals("Number of keys after UI deletion not as expected", beforeDeleteCount - 1, grid.getRecordCount());
verifyInvalidAPIKey(createApiKeyConnection(apiKey, false), false);
}

@Test
Expand All @@ -205,34 +162,85 @@ public void testStandardApiKey() throws IOException

String apiKey = generateAPIKeyAndRecord(_generatedApiKeys);
log("Verify active API key via api authentication");
verifyValidAPIKey(apiKey);
verifyValidAPIKey(createApiKeyConnection(apiKey, false));
log("Verify active API key via basic authentication");
verifyValidAPIKey(apiKey, true);
verifyValidAPIKey(createApiKeyConnection(apiKey, true));

log("Generate two other keys for use in testing deletion.");
generateAPIKey(null);
generateAPIKey(null);
QueryGrid grid = new QueryGrid.QueryGridFinder(getDriver()).waitFor();
int beforeDeleteCount = grid.getRecordCount();
grid = deleteAPIKeyViaUI();
assertEquals("Number of keys after UI deletion not as expected", beforeDeleteCount-1, grid.getRecordCount());
assertEquals("Number of keys after UI deletion not as expected", beforeDeleteCount - 1, grid.getRecordCount());

log("Verify existing active API key with disabled api key setting");
goToAdminConsole()
.clickSiteSettings()
.setAllowApiKeys(false)
.save();
verifyValidAPIKey(apiKey);
verifyValidAPIKey(createApiKeyConnection(apiKey, false));

log("Verify key deletion via UI with disabled api key generation works.");
grid = deleteAPIKeyViaUI();
assertEquals("Number of keys after UI deletion not as expected", beforeDeleteCount-2, grid.getRecordCount());
assertEquals("Number of keys after UI deletion not as expected", beforeDeleteCount - 2, grid.getRecordCount());

// skip testing api key expiration since it's already covered in unit test and 10 seconds expiration option is dev mode only

log("Verify revoked/deleted api key");
deleteAPIKeys(_generatedApiKeys);
verifyInvalidAPIKey(apiKey);
verifyInvalidAPIKey(createApiKeyConnection(apiKey, false), false);
}

/*
Regression coverage for Secure Issue 51637: Invalidate sessions when their API key becomes invalid
*/
@Test
public void testSessionInvalidatesAfterAPIKeyChange() throws IOException
{
List<Map<String, Object>> _generatedApiKeys = new ArrayList<>();

log("Generating an apikey which expire in one week");
goToAdminConsole()
.clickSiteSettings()
.setAllowApiKeys(true)
.setApiKeyExpiration(CustomizeSitePage.KeyExpirationOptions.ONE_WEEK)
.save();

String apiKey1 = generateAPIKeyAndRecord(_generatedApiKeys);
Connection cn = createApiKeyConnection(apiKey1, false);
verifyValidAPIKey(cn);

log("Deleting the apikey");
deleteAPIKeys(_generatedApiKeys);

/*
Regression coverage for Issue 52004: Session associated with APIKey can used even after APIKey is deleted.
*/
log("Verifying the session associated with deleted apikey is invalid");
verifyInvalidAPIKey(cn, false);

log("Verifying that new connection cannot be created after apikey is deleted");
verifyInvalidAPIKey(createApiKeyConnection(apiKey1, false), false);

log("Generating the apikey which expires in ten seconds");
goToAdminConsole()
.clickSiteSettings()
.setAllowApiKeys(true)
.setApiKeyExpiration(CustomizeSitePage.KeyExpirationOptions.TEN_SECONDS)
.save();

log("Verify apikey expiration");
goToExternalToolPage();
String apikey2 = ApiKeyPanel.panelFinder(getDriver()).find().generateApiKey();

log("Verify apikey can be used before expiring");
verifyValidAPIKey(createApiKeyConnection(apikey2, false));

sleep(10000); // Wait for apikey to expire

log("Verify apikey cannot be used after it has expired");
verifyInvalidAPIKey(createApiKeyConnection(apikey2, false), false);
}

@Test
Expand Down Expand Up @@ -335,6 +343,47 @@ public void testSessionKeyDisabled() throws IOException
}
}

private void verifyValidAPIKey(Connection connection) throws IOException
{
try
{
GetSchemasCommand cmd = new GetSchemasCommand();
GetSchemasResponse resp = cmd.execute(connection, getProjectName());
List<String> schemaNames = resp.getSchemaNames().stream().map(String::toLowerCase).collect(Collectors.toList());
Set<String> missingSchemas = new HashSet<>(Arrays.asList("pipeline", "lists", "core"));
missingSchemas.removeAll(schemaNames);
assertTrue("Some expected schemas missing. Schemas missing: " + missingSchemas, missingSchemas.isEmpty());
}
catch (CommandException e)
{
throw new RuntimeException("Response: " + e.getStatusCode(), e);
}
}

private Connection createApiKeyConnection(String apiKey, boolean basicAuth)
{
return new Connection(WebTestHelper.getBaseURL(), basicAuth ? new BasicAuthCredentialsProvider(API_USERNAME, apiKey)
: new ApiKeyCredentialsProvider(apiKey));
}

private void verifyInvalidAPIKey(Connection connection, boolean isSessionKey) throws IOException
{
try
{
GetSchemasCommand cmd = new GetSchemasCommand();
cmd.execute(connection, getProjectName());
if (isSessionKey)
fail("Session key didn't invalidate after logout");
else
fail("API key should no longer be valid");
}
catch (CommandException e)
{
assertEquals("Wrong response for invalid " + (isSessionKey ? "session" : "API") + " key", HttpStatus.SC_UNAUTHORIZED, e.getStatusCode());
log("Success: command failed as expected.");
}
}

private void verifyAPIKeysTablePresence(boolean isAdmin)
{
beginAt(new URLBuilder("query", "begin", getProjectName()).setFragment("sbh-ssp-core").buildURL());
Expand Down Expand Up @@ -413,7 +462,7 @@ protected Map<String, Object> getLastAPIKeyRecord() throws IOException
String keyField = "RowId";
Map<String, Object> record = response.getRows().get(0);
Map<String, Object> newRow = new HashMap<>();
Integer rowId = (Integer)((Map<String, Object>)record.get(keyField)).get("value");
Integer rowId = (Integer) ((Map<String, Object>) record.get(keyField)).get("value");
newRow.put(keyField, rowId);

return newRow;
Expand Down
Loading

0 comments on commit 2d7e93d

Please sign in to comment.