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/data/EncryptedPropertyStore.java b/api/src/org/labkey/api/data/EncryptedPropertyStore.java index 8ca23b6323f..6db435b1c9f 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"), getEncryptedSetFilter(), null).forEachMap(map -> { int set = (int)map.get("Set"); String encryption = (String)map.get("Encryption"); String propertySetName = "\"" + map.get("Category") + "\" (Set = " + set + ")"; @@ -165,4 +171,34 @@ 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"), + 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 ef728b1bf27..2827a6d1f2c 100644 --- a/api/src/org/labkey/api/data/PropertyManager.java +++ b/api/src/org/labkey/api/data/PropertyManager.java @@ -628,6 +628,33 @@ 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 + + try (Transaction t = 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); + t.commit(); + } + } + public static class PropertyEntry { private int _userId; 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 e07254c6ff4..4d498671541 100644 --- a/api/src/org/labkey/api/security/AuthenticationManager.java +++ b/api/src/org/labkey/api/security/AuthenticationManager.java @@ -327,41 +327,68 @@ 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 String getDescription() + { + return "Encrypted Authentication Properties"; + } + + @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/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 484db473869..e7dd5bb8d6c 100644 --- a/api/src/org/labkey/api/security/Encryption.java +++ b/api/src/org/labkey/api/security/Encryption.java @@ -26,9 +26,14 @@ import org.junit.Assert; 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; +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; @@ -38,10 +43,14 @@ 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; +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; @@ -63,7 +72,10 @@ 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; import java.util.concurrent.atomic.AtomicInteger; @@ -109,9 +121,28 @@ public void addDynamicWarnings(@NotNull Warnings warnings, @Nullable ViewContext int count = DECRYPTION_EXCEPTIONS.get(); 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(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(link) + .append(". ") + .append(getEncryptionKeyHelpLink())); + } } } @@ -155,7 +186,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 +275,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() @@ -536,7 +567,11 @@ static void registerHandler(EncryptionMigrationHandler handler) HANDLERS.add(handler); } + String getDescription(); + void migrateEncryptedContent(String oldPassPhrase, String keySource, AESConfig oldConfig); + + void deleteEncryptedContent(); } public static void checkMigration() @@ -567,7 +602,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 +622,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 +635,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 +647,52 @@ 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)); + 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"); + } - private static final EncryptionMigrationHandler TEST_HANDLER = (oldPassPhrase, keySource, oldConfig) -> {}; + private static final EncryptionMigrationHandler TEST_HANDLER = new EncryptionMigrationHandler() + { + @Override + public String getDescription() + { + return "Test"; + } + + @Override + public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESConfig oldConfig) + { + } + + @Override + public void deleteEncryptedContent() + { + } + }; public static class TestCase extends Assert { @@ -673,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()); + } + } } } diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index dc5accedfed..7ee5917faa8 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; @@ -901,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()); @@ -12142,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) @@ -12358,6 +12366,39 @@ 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 + { + 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. Restart the server " + + "after clearing encrypted content."); + } + + @Override + public boolean handlePost(Object o, BindException errors) throws Exception + { + Encryption.deleteEncryptedContent(getUser()); + return true; + } + + @Override + public @NonNull URLHelper getSuccessURL(Object o) + { + return getShowAdminURL(); + } + } + public static class TestCase extends AbstractActionPermissionTest { @Override @@ -12369,6 +12410,11 @@ public void testActionPermissions() AdminController controller = new AdminController(); + assertForNoPermission(user, + new ContentSecurityPolicyReportAction(), + new ContentSecurityPolicyReportToAction() + ); + // @RequiresPermission(ReadPermission.class) assertForReadPermission(user, false, new GetModulesAction(), @@ -12424,7 +12470,8 @@ controller.new ShowNetworkDriveTestAction(), controller.new ValidateDomainsAction(), new OptionalFeatureAction(), new GetSchemaXmlDocAction(), - new RecreateViewsAction() + new RecreateViewsAction(), + new DeleteEncryptedContentAction() ); // @AdminConsoleAction @@ -12466,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 863641b4b0d..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; @@ -31,8 +32,10 @@ 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.labkey.api.util.logging.LogHelper; import org.springframework.beans.MutablePropertyValues; import java.io.IOException; @@ -44,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()) @@ -210,9 +215,18 @@ 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) + { + LOG.error("Failed to decrypt password for {}", getName(), 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 0f6309ef8a6..fe719508720 100644 --- a/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java +++ b/core/src/org/labkey/core/reports/ScriptEngineManagerImpl.java @@ -122,52 +122,83 @@ 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 String getDescription() { - LOG.warn(" Cannot migrate encrypted content: EncryptionKey not specified"); - return; + return "Script Engine Passwords"; } - 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) + @Override + public void migrateEncryptedContent(String oldPassPhrase, String keySource, AESConfig oldConfig) + { + String currentPassPhrase = Encryption.getEncryptionPassPhrase(); + if (currentPassPhrase == null) { - LOG.info(" Migrating script engine configuration " + rowId); - try + 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); + + 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) + { + savePassword(tinfo, rowId, json, newEncryptedPassword); + } + } + 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"); + } + + 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("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) { - LOG.error("Exception while attempting to migrate configuration " + rowId, e); + savePassword(tinfo, rowId, json, null); } - } - }); - LOG.info(" Migration of encrypted content in scripting engine configurations is complete"); + }); + } }; public static void registerEncryptionMigrationHandler()