From 8b85b12b69287bca383002be030c017e0be0892c Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Sun, 22 Mar 2026 09:05:24 -0700 Subject: [PATCH 1/4] Delete encrypted content --- .../api/data/EncryptedPropertyStore.java | 16 ++++ .../org/labkey/api/data/PropertyManager.java | 23 ++++++ .../api/security/AuthenticationManager.java | 71 ++++++++++------ .../org/labkey/api/security/Encryption.java | 31 +++++-- .../labkey/core/admin/AdminController.java | 32 ++++++++ .../core/reports/ScriptEngineManagerImpl.java | 80 ++++++++++++------- 6 files changed, 192 insertions(+), 61 deletions(-) diff --git a/api/src/org/labkey/api/data/EncryptedPropertyStore.java b/api/src/org/labkey/api/data/EncryptedPropertyStore.java index 8ca23b6323f..ac5bba61f97 100644 --- a/api/src/org/labkey/api/data/EncryptedPropertyStore.java +++ b/api/src/org/labkey/api/data/EncryptedPropertyStore.java @@ -165,4 +165,20 @@ public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESC clearCache(); LOG.info(" Migration of encrypted property store values is complete"); } + + @Override + public void deleteEncryptedContent() + { + LOG.info("Deleting all encrypted property sets"); + TableInfo sets = PropertySchema.getInstance().getTableInfoPropertySets(); + new TableSelector( + sets, + Set.of("Set", "Category", "Encryption"), + new SimpleFilter(FieldKey.fromParts("Encryption"), "None", CompareType.NEQ), + null + ).forEachMap(map -> { + int set = (int)map.get("Set"); + PropertyManager.deleteSetDirectly(set); + }); + } } diff --git a/api/src/org/labkey/api/data/PropertyManager.java b/api/src/org/labkey/api/data/PropertyManager.java index ef728b1bf27..affe2d5406f 100644 --- a/api/src/org/labkey/api/data/PropertyManager.java +++ b/api/src/org/labkey/api/data/PropertyManager.java @@ -628,6 +628,29 @@ public static void deleteSetDirectly(User user, String objectId, String category new SqlExecutor(SCHEMA.getSchema()).execute(deleteSets); } + public static void deleteSetDirectly(int propertySet) + { + SqlExecutor executor = new SqlExecutor(SCHEMA.getSchema()); + + var setSelectName = SCHEMA.getTableInfoProperties().getColumn("Set").getSelectIdentifier(); // Keyword in some dialects + + SQLFragment deleteProps = new SQLFragment("DELETE FROM ") + .append(SCHEMA.getTableInfoProperties()) + .append(" WHERE ") + .appendIdentifier(setSelectName) + .append(" = ?") + .add(propertySet); + executor.execute(deleteProps); + + SQLFragment deleteSet = new SQLFragment("DELETE FROM ") + .append(SCHEMA.getTableInfoPropertySets()) + .append(" WHERE ") + .appendIdentifier(setSelectName) + .append(" = ?") + .add(propertySet); + executor.execute(deleteSet); + } + public static class PropertyEntry { private int _userId; diff --git a/api/src/org/labkey/api/security/AuthenticationManager.java b/api/src/org/labkey/api/security/AuthenticationManager.java index e07254c6ff4..a54f39ffa5a 100644 --- a/api/src/org/labkey/api/security/AuthenticationManager.java +++ b/api/src/org/labkey/api/security/AuthenticationManager.java @@ -327,41 +327,62 @@ public static void reorderConfigurations(User user, String name, int[] rowIds) } } - static final EncryptionMigrationHandler ENCRYPTION_MIGRATION_HANDLER = (oldPassPhrase, keySource, oldConfig) -> { - Algorithm decryptAes = Encryption.getAES128(oldPassPhrase, keySource, oldConfig); - _log.info(" Attempting to migrate encrypted properties in authentication configurations"); - TableInfo tinfo = CoreSchema.getInstance().getTableInfoAuthenticationConfigurations(); - Map map = new TableSelector(tinfo, PageFlowUtil.set("RowId", "EncryptedProperties"), + static final EncryptionMigrationHandler ENCRYPTION_MIGRATION_HANDLER = new EncryptionMigrationHandler() + { + @Override + public void migrateEncryptedContent(String oldPassPhrase, String keySource, Encryption.AESConfig oldConfig) + { + Algorithm decryptAes = Encryption.getAES128(oldPassPhrase, keySource, oldConfig); + _log.info(" Attempting to migrate encrypted properties in authentication configurations"); + TableInfo tinfo = CoreSchema.getInstance().getTableInfoAuthenticationConfigurations(); + Map map = new TableSelector(tinfo, PageFlowUtil.set("RowId", "EncryptedProperties"), new SimpleFilter(FieldKey.fromParts("EncryptedProperties"), null, CompareType.NONBLANK), null).getValueMap(Integer.class); - Map saveMap = new HashMap<>(); + Map saveMap = new HashMap<>(); - map.forEach((key, value) -> { - try - { - _log.info(" Migrating encrypted properties for configuration " + key); + map.forEach((key, value) -> { try { - String decryptedValue = decryptAes.decrypt(Base64.decodeBase64(value)); - String newEncryptedValue = Base64.encodeBase64String(AES.get().encrypt(decryptedValue)); - assert decryptedValue.equals(AES.get().decrypt(Base64.decodeBase64(newEncryptedValue))); + _log.info(" Migrating encrypted properties for configuration {}", key); + try + { + String decryptedValue = decryptAes.decrypt(Base64.decodeBase64(value)); + String newEncryptedValue = Base64.encodeBase64String(AES.get().encrypt(decryptedValue)); + assert decryptedValue.equals(AES.get().decrypt(Base64.decodeBase64(newEncryptedValue))); - if (newEncryptedValue != null) + if (newEncryptedValue != null) + { + saveMap.put("EncryptedProperties", newEncryptedValue); + Table.update(null, tinfo, saveMap, key); + } + } + catch (DecryptionException e) { - saveMap.put("EncryptedProperties", newEncryptedValue); - Table.update(null, tinfo, saveMap, key); + _log.info(" Failed to decrypt encrypted properties for configuration {}. It will be skipped.", key); } } - catch (DecryptionException e) + catch (Exception e) { - _log.info(" Failed to decrypt encrypted properties for configuration " + key + ". It will be skipped."); + _log.error("Exception while migrating configuration {}", key, e); } - } - catch (Exception e) - { - _log.error("Exception while migrating configuration " + key, e); - } - }); - _log.info(" Migration of encrypted properties in authentication configurations is complete"); + }); + _log.info(" Migration of encrypted properties in authentication configurations is complete"); + } + + @Override + public void deleteEncryptedContent() + { + _log.info("Clearing the core.AuthenticationConfigurations.EncryptedProperties column"); + TableInfo tinfo = CoreSchema.getInstance().getTableInfoAuthenticationConfigurations(); + new TableSelector( + tinfo, + PageFlowUtil.set("RowId"), + new SimpleFilter(FieldKey.fromParts("EncryptedProperties"), null, CompareType.NONBLANK), + null + ).forEach( + Integer.class, + rowId -> Table.update(null, tinfo, PageFlowUtil.map("EncryptedProperties", null), rowId) + ); + } }; // Register a handler so encrypted properties are migrated whenever the encryption key changes diff --git a/api/src/org/labkey/api/security/Encryption.java b/api/src/org/labkey/api/security/Encryption.java index 484db473869..6e9896f4de1 100644 --- a/api/src/org/labkey/api/security/Encryption.java +++ b/api/src/org/labkey/api/security/Encryption.java @@ -155,7 +155,7 @@ private static void testEncryptionKey(String passPhrase, AESConfig config, Strin { if (passPhrase != null) { - LOG.info("Attempting to test the integrity of the " + keyDescription); + LOG.info("Attempting to test the integrity of the {}", keyDescription); try { @@ -244,7 +244,7 @@ protected boolean isValidPropertyMap(PropertyManager.PropertyMap props) private static void logFailureGuidance() { - LOG.error(KEY_CHANGE_GUIDANCE + " For more information, see " + new HelpTopic("labkeyxml", "encrypt").getHelpTopicHref() + "."); + LOG.error("{} For more information, see {}.", KEY_CHANGE_GUIDANCE, new HelpTopic("labkeyxml", "encrypt").getHelpTopicHref()); } private Encryption() @@ -537,6 +537,8 @@ static void registerHandler(EncryptionMigrationHandler handler) } void migrateEncryptedContent(String oldPassPhrase, String keySource, AESConfig oldConfig); + + void deleteEncryptedContent(); } public static void checkMigration() @@ -567,7 +569,7 @@ public static void checkMigration() } else if (!cipher.equals(AESConfig.current.getCipherName())) { - LOG.error("Unexpected cipher configuration: " + cipher); + LOG.error("Unexpected cipher configuration: {}", cipher); } if (migrationNeeded) @@ -587,7 +589,7 @@ else if (!cipher.equals(AESConfig.current.getCipherName())) if (DECRYPTION_EXCEPTIONS.get() == 0) { Encryption.EncryptionMigrationHandler.HANDLERS - .forEach(handler -> handler.migrateEncryptedContent(passPhrase, message, migrationConfig)); + .forEach(handler -> handler.migrateEncryptedContent(passPhrase, message, migrationConfig)); CacheManager.clearAllKnownCaches(); } @@ -600,7 +602,7 @@ else if (!cipher.equals(AESConfig.current.getCipherName())) if (oldPassPhrase != null) { LOG.info("Migration of all existing encrypted content from OldEncryptionKey to EncryptionKey is complete"); - LOG.info("IMPORTANT: Since migration is complete you should now remove the " + keySource); + LOG.info("IMPORTANT: Since migration is complete you should now remove the {}", keySource); } if (cipher == null) { @@ -612,8 +614,25 @@ else if (!cipher.equals(AESConfig.current.getCipherName())) } } + public static void deleteEncryptedContent() + { + EncryptionMigrationHandler.HANDLERS + .forEach(EncryptionMigrationHandler::deleteEncryptedContent); + CacheManager.clearAllKnownCaches(); + } - private static final EncryptionMigrationHandler TEST_HANDLER = (oldPassPhrase, keySource, oldConfig) -> {}; + private static final EncryptionMigrationHandler TEST_HANDLER = new EncryptionMigrationHandler() + { + @Override + public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESConfig oldConfig) + { + } + + @Override + public void deleteEncryptedContent() + { + } + }; public static class TestCase extends Assert { diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index dc5accedfed..b02a390c3a6 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -48,6 +48,7 @@ import org.jfree.data.category.DefaultCategoryDataset; import org.json.JSONArray; import org.json.JSONObject; +import org.jspecify.annotations.NonNull; import org.junit.Assert; import org.junit.Test; import org.labkey.api.Constants; @@ -194,6 +195,7 @@ import org.labkey.api.security.CSRF; import org.labkey.api.security.Directive; import org.labkey.api.security.ElevatedUser; +import org.labkey.api.security.Encryption; import org.labkey.api.security.Group; import org.labkey.api.security.GroupManager; import org.labkey.api.security.IgnoresTermsOfUse; @@ -12358,6 +12360,36 @@ protected void forwardReports(URI destination, HttpServletRequest request, Strin } } + @RequiresPermission(AdminOperationsPermission.class) // Must be site administrator + public static class DeleteEncryptedContentAction extends ConfirmAction + { + @Override + public void validateCommand(Object o, Errors errors) + { + } + + @Override + public ModelAndView getConfirmView(Object o, BindException errors) throws Exception + { + return HtmlView.of("Are you sure you want to delete all encrypted content in the server? This " + + "content can include passwords to external systems, authentication configurations, users' TOTP " + + "settings, etc. The encrypted content will be cleared and can't be recovered."); + } + + @Override + public boolean handlePost(Object o, BindException errors) throws Exception + { + Encryption.deleteEncryptedContent(); + return true; + } + + @Override + public @NonNull URLHelper getSuccessURL(Object o) + { + return getShowAdminURL(); + } + } + public static class TestCase extends AbstractActionPermissionTest { @Override diff --git a/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java b/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java index 0f6309ef8a6..6043ff45e70 100644 --- a/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java +++ b/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java @@ -122,52 +122,72 @@ public class ScriptEngineManagerImpl extends ScriptEngineManager implements LabK private static final String PASSWORD_FIELD = "password"; - static final EncryptionMigrationHandler ENCRYPTION_MIGRATION_HANDLER = (oldPassPhrase, keySource, oldConfig) -> { - String currentPassPhrase = Encryption.getEncryptionPassPhrase(); - if (currentPassPhrase == null) + static final EncryptionMigrationHandler ENCRYPTION_MIGRATION_HANDLER = new EncryptionMigrationHandler() + { + @Override + public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESConfig oldConfig) { - LOG.warn(" Cannot migrate encrypted content: EncryptionKey not specified"); - return; - } + String currentPassPhrase = Encryption.getEncryptionPassPhrase(); + if (currentPassPhrase == null) + { + LOG.warn(" Cannot migrate encrypted content: EncryptionKey not specified"); + return; + } - Algorithm oldAes = Encryption.getAES128(oldPassPhrase, keySource, oldConfig); - Algorithm newAes = Encryption.getAES128(currentPassPhrase, keySource, AESConfig.current); + Algorithm oldAes = Encryption.getAES128(oldPassPhrase, keySource, oldConfig); + Algorithm newAes = Encryption.getAES128(currentPassPhrase, keySource, AESConfig.current); - TableInfo tinfo = CoreSchema.getInstance().getTableInfoReportEngines(); - new TableSelector(tinfo, PageFlowUtil.set("RowId", "Configuration")).getValueMap(Integer.class).forEach((rowId, configuration) -> { - JSONObject json = new JSONObject(configuration); - String oldEncryptedPassword = json.optString(PASSWORD_FIELD, null); - if (null != oldEncryptedPassword) - { - LOG.info(" Migrating script engine configuration " + rowId); - try + TableInfo tinfo = CoreSchema.getInstance().getTableInfoReportEngines(); + new TableSelector(tinfo, PageFlowUtil.set("RowId", "Configuration")).getValueMap(Integer.class).forEach((rowId, configuration) -> { + JSONObject json = new JSONObject(configuration); + String oldEncryptedPassword = json.optString(PASSWORD_FIELD, null); + if (null != oldEncryptedPassword) { - String decryptedPassword; + LOG.info(" Migrating script engine configuration {}", rowId); try { - decryptedPassword = oldAes.decrypt(Base64.decodeBase64(oldEncryptedPassword)); + String decryptedPassword; + try + { + decryptedPassword = oldAes.decrypt(Base64.decodeBase64(oldEncryptedPassword)); - String newEncryptedPassword = Base64.encodeBase64String(newAes.encrypt(decryptedPassword)); - assert decryptedPassword.equals(newAes.decrypt(Base64.decodeBase64(newEncryptedPassword))); + String newEncryptedPassword = Base64.encodeBase64String(newAes.encrypt(decryptedPassword)); + assert decryptedPassword.equals(newAes.decrypt(Base64.decodeBase64(newEncryptedPassword))); - if (newEncryptedPassword != null) + if (newEncryptedPassword != null) + { + json.put(PASSWORD_FIELD, newEncryptedPassword); + Table.update(null, tinfo, PageFlowUtil.map("Configuration", json.toString()), rowId); + } + } + catch (DecryptionException e) { - json.put(PASSWORD_FIELD, newEncryptedPassword); - Table.update(null, tinfo, PageFlowUtil.map("Configuration", json.toString()), rowId); + LOG.info(" Failed to decrypt password for configuration {}. This configuration will be skipped.", rowId); } } - catch (DecryptionException e) + catch (Exception e) { - LOG.info(" Failed to decrypt password for configuration " + rowId + ". This configuration will be skipped."); + LOG.error("Exception while attempting to migrate configuration {}", rowId, e); } } - catch (Exception e) + }); + LOG.info(" Migration of encrypted content in scripting engine configurations is complete"); + } + + @Override + public void deleteEncryptedContent() + { + LOG.info("Deleting all script engine configurations that have a password"); + TableInfo tinfo = CoreSchema.getInstance().getTableInfoReportEngines(); + new TableSelector(tinfo, PageFlowUtil.set("RowId", "Configuration")).getValueMap(Integer.class).forEach((rowId, configuration) -> { + JSONObject json = new JSONObject(configuration); + String encryptedPassword = json.optString(PASSWORD_FIELD, null); + if (null != encryptedPassword) { - LOG.error("Exception while attempting to migrate configuration " + rowId, e); + Table.delete(tinfo, rowId); } - } - }); - LOG.info(" Migration of encrypted content in scripting engine configurations is complete"); + }); + } }; public static void registerEncryptionMigrationHandler() From 38461530fa43d2b1deba5bbba195a63776cde070 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Sun, 22 Mar 2026 11:22:33 -0700 Subject: [PATCH 2/4] Add text and link to admin banner. Make script engine configurations behave better if their password can't be decrypted. Clear just the script engine password intead of deleting the entire configuration. --- api/src/org/labkey/api/admin/AdminUrls.java | 1 + .../api/security/ConfigurationSettings.java | 4 ++-- api/src/org/labkey/api/security/Encryption.java | 15 +++++++++++++-- .../org/labkey/core/admin/AdminController.java | 11 ++++++++++- .../ExternalScriptEngineDefinitionImpl.java | 15 ++++++++++++--- .../core/reports/ScriptEngineManagerImpl.java | 13 +++++++++---- 6 files changed, 47 insertions(+), 12 deletions(-) diff --git a/api/src/org/labkey/api/admin/AdminUrls.java b/api/src/org/labkey/api/admin/AdminUrls.java index b22cefd042f..69078f7b703 100644 --- a/api/src/org/labkey/api/admin/AdminUrls.java +++ b/api/src/org/labkey/api/admin/AdminUrls.java @@ -68,6 +68,7 @@ public interface AdminUrls extends UrlProvider ActionURL getCspReportToURL(); ActionURL getAllowedExternalRedirectHostsURL(); + ActionURL getDeleteEncryptedContentURL(); /** * Simply adds an "Admin Console" link to nav trail if invoked in the root container. Otherwise, root is unchanged. diff --git a/api/src/org/labkey/api/security/ConfigurationSettings.java b/api/src/org/labkey/api/security/ConfigurationSettings.java index b62fee9594b..c5584491f99 100644 --- a/api/src/org/labkey/api/security/ConfigurationSettings.java +++ b/api/src/org/labkey/api/security/ConfigurationSettings.java @@ -36,12 +36,12 @@ public ConfigurationSettings(Map settings) } catch (Encryption.DecryptionException e) { - LOG.warn("Encrypted properties can't be read", e); + LOG.warn("Encrypted properties can't be decrypted", e); } } else { - LOG.warn("Encrypted properties can't be read: encryption key has not been set in " + AppProps.getInstance().getWebappConfigurationFilename() + "!"); + LOG.warn("Encrypted properties can't be read: encryption key has not been set in {}!", AppProps.getInstance().getWebappConfigurationFilename()); } } diff --git a/api/src/org/labkey/api/security/Encryption.java b/api/src/org/labkey/api/security/Encryption.java index 6e9896f4de1..98277bf77f9 100644 --- a/api/src/org/labkey/api/security/Encryption.java +++ b/api/src/org/labkey/api/security/Encryption.java @@ -26,6 +26,7 @@ import org.junit.Assert; import org.junit.Test; import org.labkey.api.action.SpringActionController; +import org.labkey.api.admin.AdminUrls; import org.labkey.api.cache.CacheManager; import org.labkey.api.collections.ConcurrentHashSet; import org.labkey.api.data.ContainerManager; @@ -41,6 +42,8 @@ import org.labkey.api.util.HelpTopic; import org.labkey.api.util.HtmlStringBuilder; import org.labkey.api.util.JobRunner; +import org.labkey.api.util.LinkBuilder; +import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.StringUtilsLabKey; import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.ViewContext; @@ -64,6 +67,7 @@ import java.security.spec.KeySpec; import java.util.Arrays; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -107,11 +111,16 @@ public void addDynamicWarnings(@NotNull Warnings warnings, @Nullable ViewContext ". An encryption key is required to save credentials used in various integrations.").append(getEncryptionKeyHelpLink())); int count = DECRYPTION_EXCEPTIONS.get(); + String who = context == null || context.getUser().hasSiteAdminPermission() ? "you" : "a site administrator"; if (count > 0 || showAllWarnings) warnings.add(HtmlStringBuilder.of("On " + StringUtilsLabKey.pluralize(count, "attempt") + - " the server failed to decrypt encrypted content using the " + - ENCRYPTION_KEY_CHANGED + " " + KEY_CHANGE_GUIDANCE).append(getEncryptionKeyHelpLink())); + ", the server failed to decrypt encrypted content using the " + + ENCRYPTION_KEY_CHANGED + " " + KEY_CHANGE_GUIDANCE) + .append(" If the previous encryption key has been lost, " + who + " can clear all encrypted content via ") + .append(LinkBuilder.simpleLink("this link", Objects.requireNonNull(PageFlowUtil.urlProvider(AdminUrls.class)).getDeleteEncryptedContentURL())) + .append(". ") + .append(getEncryptionKeyHelpLink())); } } @@ -616,9 +625,11 @@ else if (!cipher.equals(AESConfig.current.getCipherName())) public static void deleteEncryptedContent() { + LOG.info("Deleting all encrypted content at the request of a site administrator"); EncryptionMigrationHandler.HANDLERS .forEach(EncryptionMigrationHandler::deleteEncryptedContent); CacheManager.clearAllKnownCaches(); + LOG.info("Finished deleting all encrypted content"); } private static final EncryptionMigrationHandler TEST_HANDLER = new EncryptionMigrationHandler() diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index b02a390c3a6..1057c0c8a21 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -903,6 +903,12 @@ public ActionURL getAllowedExternalRedirectHostsURL() .addParameter("type", AllowListType.Redirect.name()); } + @Override + public ActionURL getDeleteEncryptedContentURL() + { + return new ActionURL(DeleteEncryptedContentAction.class, ContainerManager.getRoot()); + } + public static ActionURL getDeprecatedFeaturesURL() { return new ActionURL(OptionalFeaturesAction.class, ContainerManager.getRoot()).addParameter("type", FeatureType.Deprecated.name()); @@ -12371,9 +12377,12 @@ public void validateCommand(Object o, Errors errors) @Override public ModelAndView getConfirmView(Object o, BindException errors) throws Exception { + getPageConfig().setShowHeader(false); + getPageConfig().setTitle("Delete Encrypted Content?"); return HtmlView.of("Are you sure you want to delete all encrypted content in the server? This " + "content can include passwords to external systems, authentication configurations, users' TOTP " + - "settings, etc. The encrypted content will be cleared and can't be recovered."); + "settings, etc. The encrypted content will be cleared and can't be recovered. Restart the server " + + "after clearing encrypted content."); } @Override diff --git a/core/src/org/labkey/core/reports/ExternalScriptEngineDefinitionImpl.java b/core/src/org/labkey/core/reports/ExternalScriptEngineDefinitionImpl.java index 863641b4b0d..ec9a43abb52 100644 --- a/core/src/org/labkey/core/reports/ExternalScriptEngineDefinitionImpl.java +++ b/core/src/org/labkey/core/reports/ExternalScriptEngineDefinitionImpl.java @@ -31,6 +31,7 @@ import org.labkey.api.reports.report.r.RserveScriptEngine; import org.labkey.api.security.Encryption; import org.labkey.api.security.Encryption.Algorithm; +import org.labkey.api.security.Encryption.DecryptionException; import org.labkey.api.security.User; import org.labkey.api.settings.AppProps; import org.springframework.beans.MutablePropertyValues; @@ -210,9 +211,17 @@ public void setConfiguration(String configuration, boolean decrypt) throws IOExc { String password = json.getString("password"); if (decrypt) - setPassword(AES.get().decrypt(Base64.decodeBase64(password))); - else - setPassword(password); + { + try + { + password = AES.get().decrypt(Base64.decodeBase64(password)); + } + catch (DecryptionException e) + { + password = null; + } + } + setPassword(password); } if (json.has("external")) setExternal(json.getBoolean("external")); diff --git a/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java b/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java index 6043ff45e70..1533ea62cb8 100644 --- a/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java +++ b/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java @@ -156,8 +156,7 @@ public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESC if (newEncryptedPassword != null) { - json.put(PASSWORD_FIELD, newEncryptedPassword); - Table.update(null, tinfo, PageFlowUtil.map("Configuration", json.toString()), rowId); + savePassword(tinfo, rowId, json, newEncryptedPassword); } } catch (DecryptionException e) @@ -174,17 +173,23 @@ public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESC LOG.info(" Migration of encrypted content in scripting engine configurations is complete"); } + private void savePassword(TableInfo tinfo, int rowId, JSONObject json, @Nullable String newPassword) + { + json.put(PASSWORD_FIELD, newPassword); + Table.update(null, tinfo, PageFlowUtil.map("Configuration", json.toString()), rowId); + } + @Override public void deleteEncryptedContent() { - LOG.info("Deleting all script engine configurations that have a password"); + LOG.info("Clearing the password from all script engine configurations that have one"); TableInfo tinfo = CoreSchema.getInstance().getTableInfoReportEngines(); new TableSelector(tinfo, PageFlowUtil.set("RowId", "Configuration")).getValueMap(Integer.class).forEach((rowId, configuration) -> { JSONObject json = new JSONObject(configuration); String encryptedPassword = json.optString(PASSWORD_FIELD, null); if (null != encryptedPassword) { - Table.delete(tinfo, rowId); + savePassword(tinfo, rowId, json, null); } }); } From 6b00d4400cd25790f4a46b7897eef2bc493b8cb4 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 24 Mar 2026 11:47:15 -0700 Subject: [PATCH 3/4] Code review feedback: logging, audit events, transactions, action permission checks, handle null password --- .../api/data/EncryptedPropertyStore.java | 12 +++-- .../org/labkey/api/data/PropertyManager.java | 35 ++++++------ .../reports/report/r/RserveScriptEngine.java | 8 ++- .../api/security/AuthenticationManager.java | 6 +++ .../org/labkey/api/security/Encryption.java | 53 +++++++++++++++++-- .../labkey/core/admin/AdminController.java | 15 ++++-- .../org/labkey/core/admin/customizeSite.jsp | 2 +- .../ExternalScriptEngineDefinitionImpl.java | 5 ++ .../core/reports/ScriptEngineManagerImpl.java | 6 +++ 9 files changed, 112 insertions(+), 30 deletions(-) diff --git a/api/src/org/labkey/api/data/EncryptedPropertyStore.java b/api/src/org/labkey/api/data/EncryptedPropertyStore.java index ac5bba61f97..c94784212de 100644 --- a/api/src/org/labkey/api/data/EncryptedPropertyStore.java +++ b/api/src/org/labkey/api/data/EncryptedPropertyStore.java @@ -91,7 +91,13 @@ protected PropertyEncryption getPreferredPropertyEncryption() protected void appendWhereFilter(SQLFragment sql) { sql.append("NOT Encryption = ?"); - sql.add("None"); + sql.add(PropertyEncryption.None.toString()); + } + + @Override + public String getDescription() + { + return "Encrypted Property Sets"; } @Override @@ -103,7 +109,7 @@ public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESC TableInfo sets = PropertySchema.getInstance().getTableInfoPropertySets(); TableInfo props = PropertySchema.getInstance().getTableInfoProperties(); - new TableSelector(sets, Set.of("Set", "Category", "Encryption"), new SimpleFilter(FieldKey.fromParts("Encryption"), "None", CompareType.NEQ), null).forEachMap(map -> { + new TableSelector(sets, Set.of("Set", "Category", "Encryption"), new SimpleFilter(FieldKey.fromParts("Encryption"), PropertyEncryption.None.toString(), CompareType.NEQ), null).forEachMap(map -> { int set = (int)map.get("Set"); String encryption = (String)map.get("Encryption"); String propertySetName = "\"" + map.get("Category") + "\" (Set = " + set + ")"; @@ -174,7 +180,7 @@ public void deleteEncryptedContent() new TableSelector( sets, Set.of("Set", "Category", "Encryption"), - new SimpleFilter(FieldKey.fromParts("Encryption"), "None", CompareType.NEQ), + new SimpleFilter(FieldKey.fromParts("Encryption"), PropertyEncryption.None.toString(), CompareType.NEQ), null ).forEachMap(map -> { int set = (int)map.get("Set"); diff --git a/api/src/org/labkey/api/data/PropertyManager.java b/api/src/org/labkey/api/data/PropertyManager.java index affe2d5406f..4a627ec746f 100644 --- a/api/src/org/labkey/api/data/PropertyManager.java +++ b/api/src/org/labkey/api/data/PropertyManager.java @@ -628,27 +628,30 @@ public static void deleteSetDirectly(User user, String objectId, String category new SqlExecutor(SCHEMA.getSchema()).execute(deleteSets); } + // Note: caller is responsible for clearing caches public static void deleteSetDirectly(int propertySet) { SqlExecutor executor = new SqlExecutor(SCHEMA.getSchema()); - var setSelectName = SCHEMA.getTableInfoProperties().getColumn("Set").getSelectIdentifier(); // Keyword in some dialects - SQLFragment deleteProps = new SQLFragment("DELETE FROM ") - .append(SCHEMA.getTableInfoProperties()) - .append(" WHERE ") - .appendIdentifier(setSelectName) - .append(" = ?") - .add(propertySet); - executor.execute(deleteProps); - - SQLFragment deleteSet = new SQLFragment("DELETE FROM ") - .append(SCHEMA.getTableInfoPropertySets()) - .append(" WHERE ") - .appendIdentifier(setSelectName) - .append(" = ?") - .add(propertySet); - executor.execute(deleteSet); + try (Transaction _ = SCHEMA.getSchema().getScope().ensureTransaction()) + { + SQLFragment deleteProps = new SQLFragment("DELETE FROM ") + .append(SCHEMA.getTableInfoProperties()) + .append(" WHERE ") + .appendIdentifier(setSelectName) + .append(" = ?") + .add(propertySet); + executor.execute(deleteProps); + + SQLFragment deleteSet = new SQLFragment("DELETE FROM ") + .append(SCHEMA.getTableInfoPropertySets()) + .append(" WHERE ") + .appendIdentifier(setSelectName) + .append(" = ?") + .add(propertySet); + executor.execute(deleteSet); + } } public static class PropertyEntry diff --git a/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java b/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java index 2ca7b759924..9a28711071d 100644 --- a/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java +++ b/api/src/org/labkey/api/reports/report/r/RserveScriptEngine.java @@ -569,7 +569,13 @@ private RConnection getConnection(RConnectionHolder rh, ScriptContext context) if (rconn.needLogin()) { LOG.debug("Logging in to RServe as '" + _def.getUser() + "'"); - rconn.login(_def.getUser(), _def.getPassword()); + String password = _def.getPassword(); + if (password == null) + { + LOG.warn("RServe password is null! Login will likely fail."); + password = ""; + } + rconn.login(_def.getUser(), password); } initEnv(rconn, context); diff --git a/api/src/org/labkey/api/security/AuthenticationManager.java b/api/src/org/labkey/api/security/AuthenticationManager.java index a54f39ffa5a..4d498671541 100644 --- a/api/src/org/labkey/api/security/AuthenticationManager.java +++ b/api/src/org/labkey/api/security/AuthenticationManager.java @@ -329,6 +329,12 @@ public static void reorderConfigurations(User user, String name, int[] rowIds) static final EncryptionMigrationHandler ENCRYPTION_MIGRATION_HANDLER = new EncryptionMigrationHandler() { + @Override + public String getDescription() + { + return "Encrypted Authentication Properties"; + } + @Override public void migrateEncryptedContent(String oldPassPhrase, String keySource, Encryption.AESConfig oldConfig) { diff --git a/api/src/org/labkey/api/security/Encryption.java b/api/src/org/labkey/api/security/Encryption.java index 98277bf77f9..80163ab5289 100644 --- a/api/src/org/labkey/api/security/Encryption.java +++ b/api/src/org/labkey/api/security/Encryption.java @@ -27,6 +27,8 @@ import org.junit.Test; import org.labkey.api.action.SpringActionController; import org.labkey.api.admin.AdminUrls; +import org.labkey.api.audit.AuditLogService; +import org.labkey.api.audit.provider.SiteSettingsAuditProvider.SiteSettingsAuditEvent; import org.labkey.api.cache.CacheManager; import org.labkey.api.collections.ConcurrentHashSet; import org.labkey.api.data.ContainerManager; @@ -39,6 +41,7 @@ import org.labkey.api.security.permissions.TroubleshooterPermission; import org.labkey.api.settings.AppProps; import org.labkey.api.util.ConfigurationException; +import org.labkey.api.util.HasHtmlString; import org.labkey.api.util.HelpTopic; import org.labkey.api.util.HtmlStringBuilder; import org.labkey.api.util.JobRunner; @@ -111,16 +114,30 @@ public void addDynamicWarnings(@NotNull Warnings warnings, @Nullable ViewContext ". An encryption key is required to save credentials used in various integrations.").append(getEncryptionKeyHelpLink())); int count = DECRYPTION_EXCEPTIONS.get(); - String who = context == null || context.getUser().hasSiteAdminPermission() ? "you" : "a site administrator"; if (count > 0 || showAllWarnings) + { + final String who; + final HasHtmlString link; + if (context != null && context.getUser().hasSiteAdminPermission()) + { + who = "you"; + link = LinkBuilder.simpleLink("this link", Objects.requireNonNull(PageFlowUtil.urlProvider(AdminUrls.class)).getDeleteEncryptedContentURL()); + } + else + { + who = "a site administrator"; + link = HtmlStringBuilder.of("the \"delete encrypted content\" action"); + } + warnings.add(HtmlStringBuilder.of("On " + StringUtilsLabKey.pluralize(count, "attempt") + ", the server failed to decrypt encrypted content using the " + ENCRYPTION_KEY_CHANGED + " " + KEY_CHANGE_GUIDANCE) .append(" If the previous encryption key has been lost, " + who + " can clear all encrypted content via ") - .append(LinkBuilder.simpleLink("this link", Objects.requireNonNull(PageFlowUtil.urlProvider(AdminUrls.class)).getDeleteEncryptedContentURL())) + .append(link) .append(". ") .append(getEncryptionKeyHelpLink())); + } } } @@ -545,6 +562,8 @@ static void registerHandler(EncryptionMigrationHandler handler) HANDLERS.add(handler); } + String getDescription(); + void migrateEncryptedContent(String oldPassPhrase, String keySource, AESConfig oldConfig); void deleteEncryptedContent(); @@ -623,17 +642,41 @@ else if (!cipher.equals(AESConfig.current.getCipherName())) } } - public static void deleteEncryptedContent() + public static void deleteEncryptedContent(User user) { - LOG.info("Deleting all encrypted content at the request of a site administrator"); + LOG.info("Deleting all encrypted content at the request of {}", user.getDisplayName(user)); + SiteSettingsAuditEvent event = new SiteSettingsAuditEvent(ContainerManager.getRoot(), "All encrypted content was deleted"); + String changes = StringUtilsLabKey.joinWithConjunction( + EncryptionMigrationHandler.HANDLERS.stream() + .map(handler -> handler.getDescription().toLowerCase()) + .toList(), + "and" + ); + event.setChanges("Deleted content: " + changes); + AuditLogService.get().addEvent(user, event); EncryptionMigrationHandler.HANDLERS - .forEach(EncryptionMigrationHandler::deleteEncryptedContent); + .forEach(encryptionMigrationHandler -> { + try + { + encryptionMigrationHandler.deleteEncryptedContent(); + } + catch (Exception e) + { + LOG.warn("Error while deleting encrypted content from {}", encryptionMigrationHandler.getDescription(), e); + } + }); CacheManager.clearAllKnownCaches(); LOG.info("Finished deleting all encrypted content"); } private static final EncryptionMigrationHandler TEST_HANDLER = new EncryptionMigrationHandler() { + @Override + public String getDescription() + { + return "Test"; + } + @Override public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESConfig oldConfig) { diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index 1057c0c8a21..7ee5917faa8 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -12150,7 +12150,7 @@ public void setHourDelta(Integer hourDelta) } @RequiresPermission(TroubleshooterPermission.class) - public class ViewUsageStatistics extends SimpleViewAction + public class ViewUsageStatisticsAction extends SimpleViewAction { @Override public ModelAndView getView(Object o, BindException errors) @@ -12388,7 +12388,7 @@ public ModelAndView getConfirmView(Object o, BindException errors) throws Except @Override public boolean handlePost(Object o, BindException errors) throws Exception { - Encryption.deleteEncryptedContent(); + Encryption.deleteEncryptedContent(getUser()); return true; } @@ -12410,6 +12410,11 @@ public void testActionPermissions() AdminController controller = new AdminController(); + assertForNoPermission(user, + new ContentSecurityPolicyReportAction(), + new ContentSecurityPolicyReportToAction() + ); + // @RequiresPermission(ReadPermission.class) assertForReadPermission(user, false, new GetModulesAction(), @@ -12465,7 +12470,8 @@ controller.new ShowNetworkDriveTestAction(), controller.new ValidateDomainsAction(), new OptionalFeatureAction(), new GetSchemaXmlDocAction(), - new RecreateViewsAction() + new RecreateViewsAction(), + new DeleteEncryptedContentAction() ); // @AdminConsoleAction @@ -12507,7 +12513,8 @@ controller.new SystemPropertiesAction(), assertForTroubleshooterPermission(ContainerManager.getRoot(), user, controller.new OptionalFeaturesAction(), controller.new ShowModuleErrorsAction(), - new ModuleStatusAction() + new ModuleStatusAction(), + controller.new ViewUsageStatisticsAction() ); } } diff --git a/core/src/org/labkey/core/admin/customizeSite.jsp b/core/src/org/labkey/core/admin/customizeSite.jsp index a04394e62db..a9d56b1f0d5 100644 --- a/core/src/org/labkey/core/admin/customizeSite.jsp +++ b/core/src/org/labkey/core/admin/customizeSite.jsp @@ -224,7 +224,7 @@ Click the Save button at any time to accept the current settings and continue. - <%=link("View", AdminController.ViewUsageStatistics.class)%> + <%=link("View", AdminController.ViewUsageStatisticsAction.class)%> <%=button("Download").id("testUsageReportDownload").onClick("testUsageReport(); return false;")%> Generate an example usage report. No data will be submitted. diff --git a/core/src/org/labkey/core/reports/ExternalScriptEngineDefinitionImpl.java b/core/src/org/labkey/core/reports/ExternalScriptEngineDefinitionImpl.java index ec9a43abb52..ecbcd93b74c 100644 --- a/core/src/org/labkey/core/reports/ExternalScriptEngineDefinitionImpl.java +++ b/core/src/org/labkey/core/reports/ExternalScriptEngineDefinitionImpl.java @@ -17,6 +17,7 @@ import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; import org.json.JSONObject; import org.labkey.api.action.ApiJsonForm; import org.labkey.api.action.BaseApiAction; @@ -34,6 +35,7 @@ import org.labkey.api.security.Encryption.DecryptionException; import org.labkey.api.security.User; import org.labkey.api.settings.AppProps; +import org.labkey.api.util.logging.LogHelper; import org.springframework.beans.MutablePropertyValues; import java.io.IOException; @@ -45,6 +47,8 @@ public class ExternalScriptEngineDefinitionImpl extends Entity implements ExternalScriptEngineDefinition, ApiJsonForm, HasAllowBindParameter { + private static final Logger LOG = LogHelper.getLogger(ExternalScriptEngineDefinitionImpl.class, "Password decryption errors"); + // Most definitions don't require encryption, so retrieve AES128 lazily static final Supplier AES = () -> { if (!Encryption.isEncryptionPassPhraseSpecified()) @@ -218,6 +222,7 @@ public void setConfiguration(String configuration, boolean decrypt) throws IOExc } catch (DecryptionException e) { + LOG.error("Failed to decrypt password for {}", getName(), e); password = null; } } diff --git a/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java b/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java index 1533ea62cb8..fe719508720 100644 --- a/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java +++ b/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java @@ -124,6 +124,12 @@ public class ScriptEngineManagerImpl extends ScriptEngineManager implements LabK static final EncryptionMigrationHandler ENCRYPTION_MIGRATION_HANDLER = new EncryptionMigrationHandler() { + @Override + public String getDescription() + { + return "Script Engine Passwords"; + } + @Override public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESConfig oldConfig) { From 56f296d00116fe6685bc039585bdad8a73a0ee7c Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 24 Mar 2026 13:08:32 -0700 Subject: [PATCH 4/4] Simple unit test --- .../api/data/EncryptedPropertyStore.java | 18 ++++++++-- .../org/labkey/api/data/PropertyManager.java | 3 +- .../org/labkey/api/security/Encryption.java | 36 ++++++++++++++----- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/api/src/org/labkey/api/data/EncryptedPropertyStore.java b/api/src/org/labkey/api/data/EncryptedPropertyStore.java index c94784212de..6db435b1c9f 100644 --- a/api/src/org/labkey/api/data/EncryptedPropertyStore.java +++ b/api/src/org/labkey/api/data/EncryptedPropertyStore.java @@ -109,7 +109,7 @@ public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESC TableInfo sets = PropertySchema.getInstance().getTableInfoPropertySets(); TableInfo props = PropertySchema.getInstance().getTableInfoProperties(); - new TableSelector(sets, Set.of("Set", "Category", "Encryption"), new SimpleFilter(FieldKey.fromParts("Encryption"), PropertyEncryption.None.toString(), CompareType.NEQ), null).forEachMap(map -> { + new TableSelector(sets, Set.of("Set", "Category", "Encryption"), getEncryptedSetFilter(), null).forEachMap(map -> { int set = (int)map.get("Set"); String encryption = (String)map.get("Encryption"); String propertySetName = "\"" + map.get("Category") + "\" (Set = " + set + ")"; @@ -180,11 +180,25 @@ public void deleteEncryptedContent() new TableSelector( sets, Set.of("Set", "Category", "Encryption"), - new SimpleFilter(FieldKey.fromParts("Encryption"), PropertyEncryption.None.toString(), CompareType.NEQ), + getEncryptedSetFilter(), null ).forEachMap(map -> { int set = (int)map.get("Set"); PropertyManager.deleteSetDirectly(set); }); } + + public long getEncryptedPropertySetCount() + { + return new TableSelector( + PropertySchema.getInstance().getTableInfoPropertySets(), + getEncryptedSetFilter(), + null + ).getRowCount(); + } + + private Filter getEncryptedSetFilter() + { + return new SimpleFilter(FieldKey.fromParts("Encryption"), PropertyEncryption.None.toString(), CompareType.NEQ); + } } diff --git a/api/src/org/labkey/api/data/PropertyManager.java b/api/src/org/labkey/api/data/PropertyManager.java index 4a627ec746f..2827a6d1f2c 100644 --- a/api/src/org/labkey/api/data/PropertyManager.java +++ b/api/src/org/labkey/api/data/PropertyManager.java @@ -634,7 +634,7 @@ public static void deleteSetDirectly(int propertySet) SqlExecutor executor = new SqlExecutor(SCHEMA.getSchema()); var setSelectName = SCHEMA.getTableInfoProperties().getColumn("Set").getSelectIdentifier(); // Keyword in some dialects - try (Transaction _ = SCHEMA.getSchema().getScope().ensureTransaction()) + try (Transaction t = SCHEMA.getSchema().getScope().ensureTransaction()) { SQLFragment deleteProps = new SQLFragment("DELETE FROM ") .append(SCHEMA.getTableInfoProperties()) @@ -651,6 +651,7 @@ public static void deleteSetDirectly(int propertySet) .append(" = ?") .add(propertySet); executor.execute(deleteSet); + t.commit(); } } diff --git a/api/src/org/labkey/api/security/Encryption.java b/api/src/org/labkey/api/security/Encryption.java index 80163ab5289..e7dd5bb8d6c 100644 --- a/api/src/org/labkey/api/security/Encryption.java +++ b/api/src/org/labkey/api/security/Encryption.java @@ -32,6 +32,8 @@ import org.labkey.api.cache.CacheManager; import org.labkey.api.collections.ConcurrentHashSet; import org.labkey.api.data.ContainerManager; +import org.labkey.api.data.DbScope; +import org.labkey.api.data.DbScope.Transaction; import org.labkey.api.data.EncryptedPropertyStore; import org.labkey.api.data.NormalPropertyStore; import org.labkey.api.data.PropertyManager; @@ -48,6 +50,7 @@ import org.labkey.api.util.LinkBuilder; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.StringUtilsLabKey; +import org.labkey.api.util.TestContext; import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.ViewContext; import org.labkey.api.view.template.WarningProvider; @@ -69,6 +72,8 @@ import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -645,26 +650,27 @@ else if (!cipher.equals(AESConfig.current.getCipherName())) public static void deleteEncryptedContent(User user) { LOG.info("Deleting all encrypted content at the request of {}", user.getDisplayName(user)); - SiteSettingsAuditEvent event = new SiteSettingsAuditEvent(ContainerManager.getRoot(), "All encrypted content was deleted"); - String changes = StringUtilsLabKey.joinWithConjunction( - EncryptionMigrationHandler.HANDLERS.stream() - .map(handler -> handler.getDescription().toLowerCase()) - .toList(), - "and" - ); - event.setChanges("Deleted content: " + changes); - AuditLogService.get().addEvent(user, event); + List descriptions = new LinkedList<>(); EncryptionMigrationHandler.HANDLERS .forEach(encryptionMigrationHandler -> { try { encryptionMigrationHandler.deleteEncryptedContent(); + descriptions.add(encryptionMigrationHandler.getDescription().toLowerCase()); } catch (Exception e) { LOG.warn("Error while deleting encrypted content from {}", encryptionMigrationHandler.getDescription(), e); } }); + SiteSettingsAuditEvent event = new SiteSettingsAuditEvent(ContainerManager.getRoot(), "All encrypted content was deleted"); + final String changes; + if (!descriptions.isEmpty()) + changes = "Deleted content: " + StringUtilsLabKey.joinWithConjunction(descriptions, "and"); + else + changes = "All deletes failed"; + event.setChanges(changes); + AuditLogService.get().addEvent(user, event); CacheManager.clearAllKnownCaches(); LOG.info("Finished deleting all encrypted content"); } @@ -746,5 +752,17 @@ private void test(Algorithm encryptAlgorithm, Algorithm decryptAlgorithm) for (String test : new String[]{"foo", "bar", "this is some text I want to encrypt"}) assertEquals(test, decryptAlgorithm.decrypt(encryptAlgorithm.encrypt(test))); } + + @Test + public void testDeleteEncryptedContent() + { + // Simple test that ensures no exceptions are thrown and checks only that encrypted property sets are gone. + // Changes are not committed, so content is not actually deleted. + try (Transaction _ = DbScope.getLabKeyScope().ensureTransaction()) + { + deleteEncryptedContent(TestContext.get().getUser()); + assertEquals(0, new EncryptedPropertyStore().getEncryptedPropertySetCount()); + } + } } }