Bug 573950 - Secure storage provider compatibility break

- change the org.eclipse.equinox.security extension schema
  to allow an optional "obsoletes" attribute to the provider
  element to allow specifying an id that is being replaced by this
  provider
- modify org.eclipse.equinox.security.linux fragment.xml to
  note that linuxkeystoreintegrationjna obsoletes
  org.eclipse.equinox.security.linuxkeystoreintegration provider
- modify PasswordProviderSelector.findAvailableModules() method
  to look for "obsoletes" attribute if the id doesn't match the
  specified non-null id
- add new passwordProvidersFind() method to InternalExchangeUtils to
  allow testing of the "obsoletes" attribute
- add new ObsoletesTest test to AllSecurityTests
- avoid master password regenerate if secure store created
  with obsolete module
- bump up versions appropriately

Change-Id: I89ca3252c96d1069617661d6081f04711cf3eabc
Signed-off-by: Jeff Johnston <jjohnstn@redhat.com>
Signed-off-by: Julien Dehaudt <julien.dehaudt@st.com>
Reviewed-on: https://git.eclipse.org/r/c/equinox/rt.equinox.bundles/+/181421
Tested-by: Equinox Bot <equinox-bot@eclipse.org>
Reviewed-by: Alexander Kurtakov <akurtako@redhat.com>
diff --git a/bundles/org.eclipse.equinox.security.linux/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.security.linux/META-INF/MANIFEST.MF
index b1cc95b..987f446 100644
--- a/bundles/org.eclipse.equinox.security.linux/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.security.linux/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %fragmentName
 Bundle-SymbolicName: org.eclipse.equinox.security.linux;singleton:=true
-Bundle-Version: 1.0.100.qualifier
+Bundle-Version: 1.0.200.qualifier
 Bundle-Vendor: %providerName
 Fragment-Host: org.eclipse.equinox.security;bundle-version="[1.0.0,2.0.0)"
 Bundle-RequiredExecutionEnvironment: JavaSE-11
diff --git a/bundles/org.eclipse.equinox.security.linux/fragment.properties b/bundles/org.eclipse.equinox.security.linux/fragment.properties
index b0c9c1f..3215da8 100644
--- a/bundles/org.eclipse.equinox.security.linux/fragment.properties
+++ b/bundles/org.eclipse.equinox.security.linux/fragment.properties
@@ -1,5 +1,5 @@
 ###############################################################################
-# Copyright (c) 2017 IBM Corporation and others.
+# Copyright (c) 2017, 2021 IBM Corporation and others.
 #
 # This program and the accompanying materials
 # are made available under the terms of the Eclipse Public License 2.0
diff --git a/bundles/org.eclipse.equinox.security.linux/fragment.xml b/bundles/org.eclipse.equinox.security.linux/fragment.xml
index 971743f..d91ed1d 100644
--- a/bundles/org.eclipse.equinox.security.linux/fragment.xml
+++ b/bundles/org.eclipse.equinox.security.linux/fragment.xml
@@ -8,11 +8,11 @@
       <provider
             class="org.eclipse.equinox.internal.security.linux.LinuxPasswordProvider"
             description="%providerDescription"
+            obsoletes="org.eclipse.equinox.security.linuxkeystoreintegration"
             priority="5">
          <hint
                value="AutomaticPasswordGeneration">
          </hint>
       </provider>
    </extension>
-
-</fragment>
+ </fragment>
diff --git a/bundles/org.eclipse.equinox.security.linux/pom.xml b/bundles/org.eclipse.equinox.security.linux/pom.xml
index 0b3c85c..e42cfb0 100644
--- a/bundles/org.eclipse.equinox.security.linux/pom.xml
+++ b/bundles/org.eclipse.equinox.security.linux/pom.xml
@@ -20,7 +20,7 @@
   </parent>
   <groupId>org.eclipse.equinox</groupId>
   <artifactId>org.eclipse.equinox.security.linux</artifactId>
-  <version>1.0.100-SNAPSHOT</version>
+  <version>1.0.200-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 
   <build>
diff --git a/bundles/org.eclipse.equinox.security.tests/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.security.tests/META-INF/MANIFEST.MF
index 4e53ef1..fd52e82 100644
--- a/bundles/org.eclipse.equinox.security.tests/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.security.tests/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: Equinox security tests
 Bundle-SymbolicName: org.eclipse.equinox.security.tests;singleton:=true
-Bundle-Version: 1.2.0.qualifier
+Bundle-Version: 1.2.100.qualifier
 Bundle-Activator: org.eclipse.equinox.internal.security.tests.SecurityTestsActivator
 Bundle-RequiredExecutionEnvironment: JavaSE-11
 Bundle-Vendor: Eclipse.org
diff --git a/bundles/org.eclipse.equinox.security.tests/pom.xml b/bundles/org.eclipse.equinox.security.tests/pom.xml
index c015e8f..04e68ee 100644
--- a/bundles/org.eclipse.equinox.security.tests/pom.xml
+++ b/bundles/org.eclipse.equinox.security.tests/pom.xml
@@ -19,7 +19,7 @@
   </parent>
   <groupId>org.eclipse.equinox</groupId>
   <artifactId>org.eclipse.equinox.security.tests</artifactId>
-  <version>1.2.0-SNAPSHOT</version>
+  <version>1.2.100-SNAPSHOT</version>
   <packaging>eclipse-test-plugin</packaging>
   <properties>
   	<testClass>org.eclipse.equinox.security.tests.AllSecurityTests</testClass>
diff --git a/bundles/org.eclipse.equinox.security.tests/src/org/eclipse/equinox/internal/security/tests/storage/ObsoletesTest.java b/bundles/org.eclipse.equinox.security.tests/src/org/eclipse/equinox/internal/security/tests/storage/ObsoletesTest.java
new file mode 100644
index 0000000..86610b1
--- /dev/null
+++ b/bundles/org.eclipse.equinox.security.tests/src/org/eclipse/equinox/internal/security/tests/storage/ObsoletesTest.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Red Hat Inc. and others.
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ * 
+ * Contributors:
+ *     Red Hat Inc. - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.equinox.internal.security.tests.storage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.util.List;
+import org.eclipse.equinox.internal.security.storage.friends.InternalExchangeUtils;
+import org.eclipse.equinox.internal.security.storage.friends.PasswordProviderDescription;
+import org.eclipse.equinox.internal.security.tests.SecurityTestsActivator;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+public class ObsoletesTest {
+
+	final private static String LINUX_BUNDLE = "org.eclipse.equinox.security.linux";
+
+	@Before
+	public void setUp() {
+		org.junit.Assume.assumeTrue(hasBundle(LINUX_BUNDLE));
+	}
+
+	@Test
+	public void testObsoletes() {
+		List<PasswordProviderDescription> descs = InternalExchangeUtils
+				.passwordProvidersFind("org.eclipse.equinox.security.linuxkeystoreintegration");
+		assertNotNull(descs);
+		assertEquals(1, descs.size());
+		PasswordProviderDescription desc = descs.get(0);
+		assertEquals("org.eclipse.equinox.security.linuxkeystoreintegrationjna", desc.getId());
+	}
+
+	static private boolean hasBundle(String symbolicID) {
+		BundleContext context = SecurityTestsActivator.getDefault().getBundleContext();
+		Bundle[] bundles = context.getBundles();
+		for (Bundle bundle : bundles) {
+			String bundleName = bundle.getSymbolicName();
+			if (!symbolicID.equals(bundleName))
+				continue;
+			int bundleState = bundle.getState();
+			return (bundleState != Bundle.INSTALLED) && (bundleState != Bundle.UNINSTALLED);
+		}
+		return false;
+	}
+}
diff --git a/bundles/org.eclipse.equinox.security.tests/src/org/eclipse/equinox/security/tests/AllSecurityTests.java b/bundles/org.eclipse.equinox.security.tests/src/org/eclipse/equinox/security/tests/AllSecurityTests.java
index 8e03b09..7e84a7f 100644
--- a/bundles/org.eclipse.equinox.security.tests/src/org/eclipse/equinox/security/tests/AllSecurityTests.java
+++ b/bundles/org.eclipse.equinox.security.tests/src/org/eclipse/equinox/security/tests/AllSecurityTests.java
@@ -26,7 +26,7 @@
  */
 @RunWith(Suite.class)
 @SuiteClasses({ Base64Test.class, DetectPBECiphersTest.class, SlashEncodeTest.class, DefaultPreferencesTest.class,
-		DynamicPreferencesTest.class, WinPreferencesTest.class })
+		DynamicPreferencesTest.class, ObsoletesTest.class, WinPreferencesTest.class })
 public class AllSecurityTests {
 	// see @SuiteClasses
 }
diff --git a/bundles/org.eclipse.equinox.security/META-INF/MANIFEST.MF b/bundles/org.eclipse.equinox.security/META-INF/MANIFEST.MF
index 9fa41ba..d347f75 100644
--- a/bundles/org.eclipse.equinox.security/META-INF/MANIFEST.MF
+++ b/bundles/org.eclipse.equinox.security/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.equinox.security;singleton:=true
-Bundle-Version: 1.3.600.qualifier
+Bundle-Version: 1.3.700.qualifier
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
 Bundle-Activator: org.eclipse.equinox.internal.security.auth.AuthPlugin
diff --git a/bundles/org.eclipse.equinox.security/pom.xml b/bundles/org.eclipse.equinox.security/pom.xml
index 9162149..ab6dc3b 100644
--- a/bundles/org.eclipse.equinox.security/pom.xml
+++ b/bundles/org.eclipse.equinox.security/pom.xml
@@ -19,7 +19,7 @@
   </parent>
   <groupId>org.eclipse.equinox</groupId>
   <artifactId>org.eclipse.equinox.security</artifactId>
-  <version>1.3.600-SNAPSHOT</version>
+  <version>1.3.700-SNAPSHOT</version>
   <packaging>eclipse-plugin</packaging>
 
   <build>
diff --git a/bundles/org.eclipse.equinox.security/schema/secureStorage.exsd b/bundles/org.eclipse.equinox.security/schema/secureStorage.exsd
index df73e0e..d23f7f6 100644
--- a/bundles/org.eclipse.equinox.security/schema/secureStorage.exsd
+++ b/bundles/org.eclipse.equinox.security/schema/secureStorage.exsd
@@ -44,7 +44,7 @@
                </appInfo>
             </annotation>
          </attribute>
-      </complexType>
+       </complexType>
    </element>
 
    <element name="provider">
@@ -79,6 +79,13 @@
                </appInfo>
             </annotation>
          </attribute>
+         <attribute name="obsoletes" type="string">
+            <annotation>
+               <documentation>
+                  Full id of any old secure storage provider this extension obsoletes.
+               </documentation>
+            </annotation>
+         </attribute>
       </complexType>
    </element>
 
diff --git a/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/PasswordProviderModuleExt.java b/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/PasswordProviderModuleExt.java
index 947657d..12ef11c 100644
--- a/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/PasswordProviderModuleExt.java
+++ b/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/PasswordProviderModuleExt.java
@@ -24,16 +24,22 @@
 
 	final private PasswordProvider providerModule;
 	final private String moduleID;
+	final private String obsoleteID;
 
-	public PasswordProviderModuleExt(PasswordProvider module, String moduleID) {
+	public PasswordProviderModuleExt(PasswordProvider module, String moduleID, String obsoleteID) {
 		this.providerModule = module;
 		this.moduleID = moduleID;
+		this.obsoleteID = obsoleteID;
 	}
 
 	public String getID() {
 		return moduleID;
 	}
 
+	public String getObsoleteID() {
+		return obsoleteID;
+	}
+
 	public PBEKeySpec getPassword(IPreferencesContainer container, int passwordType) {
 		return providerModule.getPassword(container, passwordType);
 	}
diff --git a/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/PasswordProviderSelector.java b/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/PasswordProviderSelector.java
index 329e1b1..1971bc8 100644
--- a/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/PasswordProviderSelector.java
+++ b/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/PasswordProviderSelector.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2008, 2018 IBM Corporation and others.
+ * Copyright (c) 2008, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -38,6 +38,7 @@
 	final private static String STORAGE_MODULE = "provider";//$NON-NLS-1$
 	final private static String MODULE_PRIORITY = "priority";//$NON-NLS-1$
 	final private static String MODULE_DESCRIPTION = "description";//$NON-NLS-1$
+	final private static String OBSOLETES_ID = "obsoletes"; //$NON-NLS-1$
 	final private static String CLASS_NAME = "class";//$NON-NLS-1$
 	final private static String HINTS_NAME = "hint";//$NON-NLS-1$
 	final private static String HINT_VALUE = "value";//$NON-NLS-1$
@@ -46,16 +47,18 @@
 
 	public class ExtStorageModule {
 		public String moduleID;
+		public String obsoleteID;
 		public IConfigurationElement element;
 		public int priority;
 		public String name;
 		public String description;
 		public List<String> hints;
 
-		public ExtStorageModule(String id, IConfigurationElement element, int priority, String name, String description, List<String> hints) {
+		public ExtStorageModule(String id, String obsoleteID, IConfigurationElement element, int priority, String name, String description, List<String> hints) {
 			super();
 			this.element = element;
 			this.moduleID = id;
+			this.obsoleteID = obsoleteID;
 			this.priority = priority;
 			this.name = name;
 			this.description = description;
@@ -99,16 +102,25 @@
 			if (moduleID == null) // IDs on those extensions are mandatory; if not specified, ignore the extension
 				continue;
 			moduleID = moduleID.toLowerCase();
+			boolean isFound = true;
 			if (expectedID != null && !expectedID.equals(moduleID))
-				continue;
+				isFound = false;
 			IConfigurationElement[] elements = extension.getConfigurationElements();
 			if (elements.length == 0)
 				continue;
 			IConfigurationElement element = elements[0]; // only one module is allowed per extension
 			if (!STORAGE_MODULE.equals(element.getName())) {
+				if (!isFound) // don't bother issue error message if id doesn't match
+					continue;
 				reportError(SecAuthMessages.unexpectedConfigElement, element.getName(), element, null);
 				continue;
 			}
+			String obsoletes = element.getAttribute(OBSOLETES_ID);
+			if (!isFound) {
+				// check if old id has been replaced by newer one (Bug 573950)
+				if (obsoletes == null || !expectedID.equals(obsoletes))
+					continue;
+			}
 			String attribute = element.getAttribute(MODULE_PRIORITY);
 			int priority = -1;
 			if (attribute != null) {
@@ -140,7 +152,7 @@
 			} catch (CoreException e) {
 				continue;
 			}
-			allAvailableModules.add(new ExtStorageModule(moduleID, element, priority, name, description, suppliedHints));
+			allAvailableModules.add(new ExtStorageModule(moduleID, obsoletes, element, priority, name, description, suppliedHints));
 		}
 
 		Collections.sort(allAvailableModules, (o1, o2) -> {
@@ -178,7 +190,7 @@
 			if (!(clazz instanceof PasswordProvider))
 				continue;
 
-			PasswordProviderModuleExt result = new PasswordProviderModuleExt((PasswordProvider) clazz, module.moduleID);
+			PasswordProviderModuleExt result = new PasswordProviderModuleExt((PasswordProvider) clazz, module.moduleID, module.obsoleteID);
 
 			// cache the result
 			synchronized (modules) {
diff --git a/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/SecurePreferencesRoot.java b/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/SecurePreferencesRoot.java
index 102f7c0..9d07500 100644
--- a/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/SecurePreferencesRoot.java
+++ b/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/SecurePreferencesRoot.java
@@ -236,6 +236,7 @@
 
 		PasswordProviderModuleExt moduleExt = PasswordProviderSelector.getInstance().findStorageModule(moduleID);
 		String key = moduleExt.getID();
+		String obsoleteKey = moduleExt.getObsoleteID();
 		PasswordExt passwordExt = null;
 		boolean validPassword = false;
 		boolean setupPasswordRecovery = false;
@@ -258,6 +259,9 @@
 
 			// is there password verification string already?
 			SecurePreferences node = node(PASSWORD_VERIFICATION_NODE);
+			if (obsoleteKey != null && node.hasKey(obsoleteKey)) {
+				key = obsoleteKey;
+			}
 			boolean newPassword = !node.hasKey(key);
 			int passwordType = newPassword ? PasswordProvider.CREATE_NEW_PASSWORD : 0;
 
diff --git a/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/friends/InternalExchangeUtils.java b/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/friends/InternalExchangeUtils.java
index 087f704..4129d95 100644
--- a/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/friends/InternalExchangeUtils.java
+++ b/bundles/org.eclipse.equinox.security/src/org/eclipse/equinox/internal/security/storage/friends/InternalExchangeUtils.java
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2008, 2018 IBM Corporation and others.
+ * Copyright (c) 2008, 2021 IBM Corporation and others.
  *
  * This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License 2.0
@@ -60,6 +60,21 @@
 	}
 
 	/**
+	 * Gathers list of available password providers with specified id. Note: this method does not try
+	 * to instantiate providers, hence, providers listed as available by this method
+	 * might fail on instantiation and not be available for the actual use.
+	 * @return available password providers as described in extensions
+	 */
+	static public List<PasswordProviderDescription> passwordProvidersFind(String id) {
+		List<ExtStorageModule> availableModules = PasswordProviderSelector.getInstance().findAvailableModules(id);
+		List<PasswordProviderDescription> result = new ArrayList<>(availableModules.size());
+		for (ExtStorageModule module : availableModules) {
+			result.add(new PasswordProviderDescription(module.name, module.moduleID, module.priority, module.description, module.hints));
+		}
+		return result;
+	}
+
+	/**
 	 * Clears cached information from password providers.
 	 */
 	static public void passwordProvidersReset() {