This is an automated email from the ASF dual-hosted git repository. smolnar82 pushed a commit to branch knox_idf in repository https://gitbox.apache.org/repos/asf/knox.git
commit 331f3bbd257e5f8b6d2014ba773ac78a21d6c206 Author: Sandor Molnar <[email protected]> AuthorDate: Wed Aug 12 18:22:31 2026 +0200 KNOX-3414: fix embedded-Derby table-exists check and add DerbyDB TrustedOidcIssuerService default JDBCUtils.tableExists passed the table name verbatim for uppercase-storing engines (Derby), so a lowercase constant like federated_identity never matched Derby's uppercased FEDERATED_IDENTITY metadata. On restart tableExists returned false, createTableIfNotExists re-ran the CREATE, and Derby failed with "Table/View 'FEDERATED_IDENTITY' already exists". Uppercase-constant callers (KNOX_TOKENS, KNOX_PROVIDERS, TRUSTED_OIDC_ISSUERS) accidentally worked. Extract a normalizeIdentifier() helper that upper-cases for uppercase-storing engines and lower-cases for lowercase-storing ones. Also add DerbyDBTrustedOidcIssuerService, a self-provisioning embedded-Derby default mirroring DerbyDBFederatedIdentityService, and wire TrustedOidcIssuerServiceFactory to auto-select Derby vs JDBC based on external DB config instead of always using JDBC. Co-Authored-By: Claude Opus 4.8 <[email protected]> --- .../apache/knox/gateway/database/JDBCUtils.java | 20 +- .../factory/TrustedOidcIssuerServiceFactory.java | 72 ++++-- .../DerbyDBTrustedOidcIssuerService.java | 102 ++++++++ .../TrustedOidcIssuerServiceFactoryTest.java | 188 +++++++++++++++ .../DerbyDBFederatedIdentityServiceTest.java | 42 ++++ .../DerbyDBTrustedOidcIssuerServiceTest.java | 131 ++++++++++ .../TrustedOidcIssuerServiceFactoryTest.java | 265 --------------------- 7 files changed, 540 insertions(+), 280 deletions(-) diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/JDBCUtils.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/JDBCUtils.java index 014f8144f..30e2ab59f 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/database/JDBCUtils.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/JDBCUtils.java @@ -36,7 +36,7 @@ public class JDBCUtils { boolean exists; try (Connection connection = dataSource.getConnection()) { final DatabaseMetaData dbMetadata = connection.getMetaData(); - final String tableNameToCheck = dbMetadata.storesUpperCaseIdentifiers() ? tableName : tableName.toLowerCase(Locale.ROOT); + final String tableNameToCheck = normalizeIdentifier(tableName, dbMetadata); try (ResultSet tables = dbMetadata.getTables(connection.getCatalog(), null, tableNameToCheck, null)) { exists = tables.next(); } @@ -44,6 +44,24 @@ public class JDBCUtils { return exists; } + /** + * Normalises an unquoted identifier to the case the driver actually stores it in, so it can be + * matched against {@link DatabaseMetaData#getTables}. Derby (and other uppercase-storing + * engines) store an unquoted {@code federated_identity} as {@code FEDERATED_IDENTITY}; passing + * the name verbatim would match nothing and cause {@code createTableIfNotExists} to re-run the + * CREATE and fail with "table already exists". Callers that use already-uppercase constants + * (KNOX_TOKENS, KNOX_PROVIDERS, TRUSTED_OIDC_ISSUERS) are unaffected since upper-casing them is + * a no-op. + */ + private static String normalizeIdentifier(String identifier, DatabaseMetaData dbMetadata) throws SQLException { + if (dbMetadata.storesUpperCaseIdentifiers()) { + return identifier.toUpperCase(Locale.ROOT); + } else if (dbMetadata.storesLowerCaseIdentifiers()) { + return identifier.toLowerCase(Locale.ROOT); + } + return identifier; + } + public static void createTableFromSQL(String createSqlFileName, DataSource dataSource, ClassLoader classLoader) throws Exception { try (InputStream is = classLoader.getResourceAsStream(createSqlFileName); Connection connection = dataSource.getConnection();Statement createTableStatement = connection.createStatement()) { diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java index 3f520f094..e8219c8e3 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java @@ -18,11 +18,13 @@ package org.apache.knox.gateway.services.factory; import org.apache.knox.gateway.GatewayMessages; import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.database.DatabaseType; import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.services.GatewayServices; import org.apache.knox.gateway.services.Service; import org.apache.knox.gateway.services.ServiceLifecycleException; import org.apache.knox.gateway.services.ServiceType; +import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.DerbyDBTrustedOidcIssuerService; import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.EmptyTrustedOidcIssuerService; import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.JdbcTrustedOidcIssuerService; import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.TrustedOidcIssuerService; @@ -42,27 +44,20 @@ public class TrustedOidcIssuerServiceFactory extends AbstractServiceFactory { throws ServiceLifecycleException { String implementationToUse = implementation; + // No explicit impl configured: auto-select a persistence backend when KnoxIDF is deployed. + // Otherwise honor the configured impl (very likely a prod JDBC store). if (isEmptyDefaultImplementation(implementationToUse) && isKnoxIdfEnabledInAnyTopology(gatewayServices, gatewayConfig)) { - implementationToUse = JdbcTrustedOidcIssuerService.class.getName(); + implementationToUse = chooseAutoImplementation(gatewayConfig); } TrustedOidcIssuerService service = null; if (shouldCreateService(implementationToUse)) { if (matchesImplementation(implementationToUse, EmptyTrustedOidcIssuerService.class, true)) { service = new EmptyTrustedOidcIssuerService(); + } else if (matchesImplementation(implementationToUse, DerbyDBTrustedOidcIssuerService.class)) { + service = createDerbyService(gatewayServices, gatewayConfig, options); } else if (matchesImplementation(implementationToUse, JdbcTrustedOidcIssuerService.class)) { - try { - final JdbcTrustedOidcIssuerService jdbcService = new JdbcTrustedOidcIssuerService(); - jdbcService.setAliasService(getAliasService(gatewayServices)); - jdbcService.init(gatewayConfig, options); - service = jdbcService; - } catch (ServiceLifecycleException e) { - LOG.errorInitializingService(implementationToUse, e.getMessage(), e); - service = new EmptyTrustedOidcIssuerService(); - } catch (Exception e) { - throw new ServiceLifecycleException( - "Error while creating TrustedOidcIssuerService: " + e, e); - } + service = createJdbcService(gatewayServices, gatewayConfig, options); } if (service != null) { logServiceUsage(service.getClass().getName(), serviceType); @@ -71,6 +66,55 @@ public class TrustedOidcIssuerServiceFactory extends AbstractServiceFactory { return service; } + /** + * Chooses the auto-enabled implementation when KnoxIDF is deployed with no explicit impl: an + * operator-configured external database wins (very likely a prod JDBC store), otherwise a + * self-provisioning embedded Derby store (the {@code none}/{@code derbydb} default) so the + * trusted OIDC issuer registry works out of the box without any extra infrastructure. + */ + String chooseAutoImplementation(GatewayConfig gatewayConfig) { + return isExternalDatabaseConfigured(gatewayConfig) + ? JdbcTrustedOidcIssuerService.class.getName() + : DerbyDBTrustedOidcIssuerService.class.getName(); + } + + private boolean isExternalDatabaseConfigured(GatewayConfig gatewayConfig) { + final String databaseType = gatewayConfig.getDatabaseType(); + try { + return DatabaseType.fromString(databaseType) != DatabaseType.DERBY; + } catch (IllegalArgumentException e) { + // "none" (the default) or any unrecognized value: no real external DB -> use Derby. + return false; + } + } + + private TrustedOidcIssuerService createDerbyService(GatewayServices gatewayServices, GatewayConfig gatewayConfig, Map<String, String> options) + throws ServiceLifecycleException { + try { + final DerbyDBTrustedOidcIssuerService derbyService = new DerbyDBTrustedOidcIssuerService(); + derbyService.setAliasService(getAliasService(gatewayServices)); + derbyService.setMasterService(getMasterService(gatewayServices)); + derbyService.init(gatewayConfig, options); + return derbyService; + } catch (ServiceLifecycleException e) { + LOG.errorInitializingService(DerbyDBTrustedOidcIssuerService.class.getName(), e.getMessage(), e); + return new EmptyTrustedOidcIssuerService(); + } + } + + private TrustedOidcIssuerService createJdbcService(GatewayServices gatewayServices, GatewayConfig gatewayConfig, Map<String, String> options) + throws ServiceLifecycleException { + try { + final JdbcTrustedOidcIssuerService jdbcService = new JdbcTrustedOidcIssuerService(); + jdbcService.setAliasService(getAliasService(gatewayServices)); + jdbcService.init(gatewayConfig, options); + return jdbcService; + } catch (ServiceLifecycleException e) { + LOG.errorInitializingService(JdbcTrustedOidcIssuerService.class.getName(), e.getMessage(), e); + return new EmptyTrustedOidcIssuerService(); + } + } + @Override protected ServiceType getServiceType() { return ServiceType.TRUSTED_OIDC_ISSUER_SERVICE; @@ -78,6 +122,6 @@ public class TrustedOidcIssuerServiceFactory extends AbstractServiceFactory { @Override protected Collection<String> getKnownImplementations() { - return List.of(DEFAULT_IMPLEMENTATION, JdbcTrustedOidcIssuerService.class.getName()); + return List.of(DEFAULT_IMPLEMENTATION, JdbcTrustedOidcIssuerService.class.getName(), DerbyDBTrustedOidcIssuerService.class.getName()); } } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/DerbyDBTrustedOidcIssuerService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/DerbyDBTrustedOidcIssuerService.java new file mode 100644 index 000000000..f1c73e9b3 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/DerbyDBTrustedOidcIssuerService.java @@ -0,0 +1,102 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.trustedoidcissuer; + +import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_NAME; +import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_TYPE; +import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME; +import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME; +import static org.apache.knox.gateway.database.DatabaseType.DERBY; +import static org.apache.knox.gateway.services.security.AliasService.NO_CLUSTER_NAME; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.hadoop.conf.Configuration; +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.security.MasterService; +import org.apache.knox.gateway.services.token.impl.DerbyDBTokenStateService; +import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase; + +/** + * A self-provisioning, embedded-Derby backed {@link TrustedOidcIssuerService}. This is the + * auto-enabled default when KnoxIDF is deployed without an operator-configured external database, + * mirroring how {@link DerbyDBTokenStateService} is the default token-state service and + * {@code DerbyDBFederatedIdentityService} is the default federated-identity service. + * <p> + * It reuses the single embedded Derby database that the token-state service already provisions + * under {@code ${securityDir}/tokens} (the {@code ;create=true} JDBC URL is idempotent, so + * connecting to an already-booted database simply connects), sets the shared {@link GatewayConfig} + * to point at it, ensures the connection user/password aliases exist, and then delegates all + * persistence to {@link JdbcTrustedOidcIssuerService} (which builds the + * {@link TrustedOidcIssuerDatabase} and self-creates its table). + */ +public class DerbyDBTrustedOidcIssuerService extends JdbcTrustedOidcIssuerService { + + private DerbyDatabase derbyDatabase; + private Path derbyDatabaseFolder; + private MasterService masterService; + + public void setMasterService(MasterService masterService) { + this.masterService = masterService; + } + + @Override + public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException { + try { + derbyDatabaseFolder = Paths.get(config.getGatewaySecurityDir(), DerbyDBTokenStateService.DB_NAME); + startDerby(); + ((Configuration) config).set(GATEWAY_DATABASE_TYPE, DERBY.type()); + ((Configuration) config).set(GATEWAY_DATABASE_NAME, derbyDatabaseFolder.toString()); + getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_USER_ALIAS_NAME, getDatabaseUserName()); + getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_PASSWORD_ALIAS_NAME, getDatabasePassword()); + super.init(config, options); + } catch (Exception e) { + throw new ServiceLifecycleException("Error while initiating DerbyDBTrustedOidcIssuerService: " + e, e); + } + } + + private void startDerby() throws Exception { + derbyDatabase = new DerbyDatabase(derbyDatabaseFolder.toString()); + derbyDatabase.create(); + TimeUnit.SECONDS.sleep(1); // give a bit of time for the server to start + } + + private String getDatabasePassword() throws Exception { + final char[] dbPasswordAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_PASSWORD_ALIAS_NAME); + return dbPasswordAliasValue != null ? new String(dbPasswordAliasValue) : new String(masterService.getMasterSecret()); + } + + private String getDatabaseUserName() throws Exception { + final char[] dbUserAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_USER_ALIAS_NAME); + return dbUserAliasValue != null ? new String(dbUserAliasValue) : DerbyDBTokenStateService.DEFAULT_TOKEN_DB_USER_NAME; + } + + @Override + public void stop() throws ServiceLifecycleException { + try { + if (derbyDatabase != null) { + derbyDatabase.shutdown(); + } + } catch (Exception e) { + throw new ServiceLifecycleException("Error while shutting down Derby Database", e); + } + } +} diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactoryTest.java new file mode 100644 index 000000000..ff06f7825 --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactoryTest.java @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.factory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.apache.knox.gateway.config.impl.GatewayConfigImpl; +import org.apache.knox.gateway.database.DatabaseType; +import org.apache.knox.gateway.services.GatewayServices; +import org.apache.knox.gateway.services.Service; +import org.apache.knox.gateway.services.ServiceType; +import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.DerbyDBTrustedOidcIssuerService; +import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.EmptyTrustedOidcIssuerService; +import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.JdbcTrustedOidcIssuerService; +import org.apache.knox.gateway.services.security.AliasService; +import org.apache.knox.gateway.services.security.MasterService; +import org.apache.knox.gateway.services.topology.TopologyService; +import org.apache.knox.gateway.topology.Topology; +import org.apache.knox.test.TestUtils; +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Test; + +public class TrustedOidcIssuerServiceFactoryTest { + + private final TrustedOidcIssuerServiceFactory serviceFactory = new TrustedOidcIssuerServiceFactory(); + private final Map<String, String> options = new HashMap<>(); + private File tempDir; + private Service createdService; + + @After + public void tearDown() throws Exception { + if (createdService != null) { + createdService.stop(); + } + if (tempDir != null) { + FileUtils.forceDelete(tempDir); + } + } + + // ------------------------------------------------------------------ + // Auto-implementation selection + // ------------------------------------------------------------------ + + @Test + public void shouldChooseDerbyWhenNoDatabaseConfigured() { + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getDatabaseType()).andReturn("none").anyTimes(); + EasyMock.replay(config); + assertEquals(DerbyDBTrustedOidcIssuerService.class.getName(), serviceFactory.chooseAutoImplementation(config)); + } + + @Test + public void shouldChooseDerbyWhenDatabaseTypeIsDerby() { + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes(); + EasyMock.replay(config); + assertEquals(DerbyDBTrustedOidcIssuerService.class.getName(), serviceFactory.chooseAutoImplementation(config)); + } + + @Test + public void shouldChooseJdbcWhenExternalDatabaseConfigured() { + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.POSTGRESQL.type()).anyTimes(); + EasyMock.replay(config); + assertEquals(JdbcTrustedOidcIssuerService.class.getName(), serviceFactory.chooseAutoImplementation(config)); + } + + // ------------------------------------------------------------------ + // Empty (no KNOXIDF) cases + // ------------------------------------------------------------------ + + /** Zero topologies → KnoxIDF not deployed → Empty. */ + @Test + public void shouldSelectEmptyWhenNoTopologies() throws Exception { + final GatewayServices gws = servicesWithTopology(/* no topologies */); + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.replay(config); + createdService = serviceFactory.create(gws, ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, config, options, ""); + assertTrue(createdService instanceof EmptyTrustedOidcIssuerService); + } + + /** Topologies exist but none contain KNOXIDF or KNOXIDF_ADMIN → Empty. */ + @Test + public void shouldSelectEmptyWhenNoKnoxIdfRole() throws Exception { + final GatewayServices gws = servicesWithTopology(topologyWithRole("KNOXSSO")); + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.replay(config); + createdService = serviceFactory.create(gws, ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, config, options, ""); + assertTrue(createdService instanceof EmptyTrustedOidcIssuerService); + } + + /** An explicit Empty implementation is honored even when KnoxIDF is deployed. */ + @Test + public void shouldHonorExplicitEmptyImplEvenWhenKnoxIdfIsDeployed() throws Exception { + final GatewayServices gws = servicesWithTopology(topologyWithRole("KNOXIDF")); + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.replay(config); + createdService = serviceFactory.create(gws, ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, config, options, + EmptyTrustedOidcIssuerService.class.getName()); + assertTrue(createdService instanceof EmptyTrustedOidcIssuerService); + } + + // ------------------------------------------------------------------ + // Derby auto-provisioning + // ------------------------------------------------------------------ + + @Test + public void shouldAutoSelectDerbyServiceWhenKnoxIdfDeployedWithoutExternalDatabase() throws Exception { + tempDir = TestUtils.createTempDir(this.getClass().getName()); + final MasterService masterService = EasyMock.createNiceMock(MasterService.class); + EasyMock.expect(masterService.getMasterSecret()).andReturn("M4st3RSecret!".toCharArray()).anyTimes(); + EasyMock.replay(masterService); + final AliasService aliasService = EasyMock.createNiceMock(AliasService.class); + EasyMock.replay(aliasService); + + final TopologyService topologyService = EasyMock.createNiceMock(TopologyService.class); + EasyMock.expect(topologyService.getTopologies()).andReturn(Collections.singletonList(topologyWithRole("KNOXIDF"))).anyTimes(); + EasyMock.replay(topologyService); + final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class); + EasyMock.expect(gws.getService(ServiceType.TOPOLOGY_SERVICE)).andReturn(topologyService).anyTimes(); + EasyMock.expect(gws.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes(); + EasyMock.expect(gws.getService(ServiceType.MASTER_SERVICE)).andReturn(masterService).anyTimes(); + EasyMock.replay(gws); + + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes(); + EasyMock.expect(config.getGatewaySecurityDir()).andReturn(tempDir.getAbsolutePath()).anyTimes(); + EasyMock.expect(config.getDatabaseName()).andReturn(Paths.get(tempDir.getAbsolutePath(), "tokens").toString()).anyTimes(); + EasyMock.expect(config.getTrustedOidcIssuerMaxTrustedIssuers()).andReturn(10).anyTimes(); + EasyMock.expect(config.getTrustedOidcIssuerDiscoveryCacheTtlSecs()).andReturn(300).anyTimes(); + EasyMock.expect(config.getTrustedOidcIssuerDiscoveryConnectTimeoutMs()).andReturn(2000).anyTimes(); + EasyMock.expect(config.getTrustedOidcIssuerDiscoveryReadTimeoutMs()).andReturn(2000).anyTimes(); + EasyMock.replay(config); + + createdService = serviceFactory.create(gws, ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, config, options, ""); + assertNotNull(createdService); + assertTrue("Expected a self-provisioning Derby-backed trusted OIDC issuer service, got " + + createdService.getClass().getName(), createdService instanceof DerbyDBTrustedOidcIssuerService); + } + + // ------------------------------------------------------------------ + // Helpers + // ------------------------------------------------------------------ + + private Topology topologyWithRole(String role) { + final Topology topology = new Topology(); + topology.setName("topology-" + role); + final org.apache.knox.gateway.topology.Service service = new org.apache.knox.gateway.topology.Service(); + service.setRole(role); + topology.addService(service); + return topology; + } + + private GatewayServices servicesWithTopology(Topology... topologies) { + final TopologyService topologyService = EasyMock.createNiceMock(TopologyService.class); + EasyMock.expect(topologyService.getTopologies()).andReturn(Arrays.asList(topologies)).anyTimes(); + EasyMock.replay(topologyService); + final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class); + EasyMock.expect(gws.getService(ServiceType.TOPOLOGY_SERVICE)).andReturn(topologyService).anyTimes(); + EasyMock.replay(gws); + return gws; + } +} diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityServiceTest.java index 267dd28a1..2aed54885 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityServiceTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityServiceTest.java @@ -105,4 +105,46 @@ public class DerbyDBFederatedIdentityServiceTest { "KEYCLOAK", "https://issuer.example.com/realms/knox", "no-such-subject"); assertFalse("Did not expect an identity for an unknown subject", missing.isPresent()); } + + /** + * Regression guard for the "Table/View 'FEDERATED_IDENTITY' already exists" failure on restart: + * re-initialising against the same on-disk Derby database (as happens on a Knox restart) must not + * try to re-create the already-present tables. Before the {@code JDBCUtils.tableExists} casing + * fix, the lowercase {@code federated_identity} table name never matched Derby's uppercased + * metadata, so init re-ran the CREATE and blew up on the second boot. + */ + @Test + public void shouldReinitializeWithoutErrorWhenTablesAlreadyExist() throws Exception { + service = newDerbyService(); + final FederatedIdentity identity = new FederatedIdentity("knox-user-1", "KEYCLOAK", "external-subject-1", + "https://issuer.example.com/realms/knox", Instant.now(), new HashMap<>()); + service.addFederatedIdentity(identity); + service.stop(); + + // Simulate a restart: a brand-new service instance pointing at the same Derby folder. + service = newDerbyService(); + final Optional<FederatedIdentity> byId = service.findById(identity.getId()); + assertTrue("Expected the previously-persisted identity to survive a restart", byId.isPresent()); + } + + private DerbyDBFederatedIdentityService newDerbyService() throws Exception { + final MasterService masterService = EasyMock.createNiceMock(MasterService.class); + EasyMock.expect(masterService.getMasterSecret()).andReturn("M4st3RSecret!".toCharArray()).anyTimes(); + EasyMock.replay(masterService); + + final AliasService aliasService = EasyMock.createNiceMock(AliasService.class); + EasyMock.replay(aliasService); + + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getGatewaySecurityDir()).andReturn(securityDir.getAbsolutePath()).anyTimes(); + EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes(); + EasyMock.expect(config.getDatabaseName()).andReturn(Paths.get(securityDir.getAbsolutePath(), "tokens").toString()).anyTimes(); + EasyMock.replay(config); + + final DerbyDBFederatedIdentityService svc = new DerbyDBFederatedIdentityService(); + svc.setAliasService(aliasService); + svc.setMasterService(masterService); + svc.init(config, Collections.emptyMap()); + return svc; + } } diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/DerbyDBTrustedOidcIssuerServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/DerbyDBTrustedOidcIssuerServiceTest.java new file mode 100644 index 000000000..f2bc4f081 --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/DerbyDBTrustedOidcIssuerServiceTest.java @@ -0,0 +1,131 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.trustedoidcissuer; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Collections; +import java.util.List; + +import org.apache.commons.io.FileUtils; +import org.apache.knox.gateway.config.impl.GatewayConfigImpl; +import org.apache.knox.gateway.database.DatabaseType; +import org.apache.knox.gateway.services.security.AliasService; +import org.apache.knox.gateway.services.security.MasterService; +import org.apache.knox.test.TestUtils; +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Verifies that {@link DerbyDBTrustedOidcIssuerService} self-provisions an embedded Derby database + * and round-trips a trusted OIDC issuer through it. + */ +public class DerbyDBTrustedOidcIssuerServiceTest { + + private File securityDir; + private DerbyDBTrustedOidcIssuerService service; + + @Before + public void setUp() throws IOException { + securityDir = TestUtils.createTempDir(this.getClass().getName()); + } + + @After + public void tearDown() throws Exception { + if (service != null) { + service.stop(); + } + if (securityDir != null) { + FileUtils.forceDelete(securityDir); + } + } + + @Test + public void shouldRoundTripATrustedIssuerOnEmbeddedDerby() throws Exception { + service = newService(newConfig()); + + final TrustedOidcIssuer issuer = new TrustedOidcIssuer( + "https://issuer.example.com/realms/knox", true, "clusterA", Instant.now(), "admin"); + service.register(issuer); + + assertTrue("Expected the registered issuer to be trusted", service.isTrusted(issuer.getIssuerUrl())); + assertTrue("Expected the registered issuer to be dynamic-jwks", service.isDynamicJwks(issuer.getIssuerUrl())); + assertFalse("Did not expect an unknown issuer to be trusted", service.isTrusted("https://unknown.example.com")); + + final List<TrustedOidcIssuer> all = service.list(); + assertEquals(1, all.size()); + assertEquals(issuer.getIssuerUrl(), all.get(0).getIssuerUrl()); + + service.deregister(issuer.getIssuerUrl()); + assertFalse("Expected the issuer to be gone after deregister", service.isTrusted(issuer.getIssuerUrl())); + } + + /** + * Regression guard for the "Table/View 'TRUSTED_OIDC_ISSUERS' already exists" failure on restart: + * re-initialising against the same on-disk Derby database (as happens on a Knox restart) must not + * try to re-create the already-present table. + */ + @Test + public void shouldReinitializeWithoutErrorWhenTableAlreadyExists() throws Exception { + service = newService(newConfig()); + final TrustedOidcIssuer issuer = new TrustedOidcIssuer( + "https://issuer.example.com/realms/knox", false, null, Instant.now(), "admin"); + service.register(issuer); + service.stop(); + + // Simulate a restart: a brand-new service instance pointing at the same Derby folder. + service = newService(newConfig()); + assertTrue("Expected the previously-registered issuer to survive a restart", + service.isTrusted(issuer.getIssuerUrl())); + } + + private DerbyDBTrustedOidcIssuerService newService(GatewayConfigImpl config) throws Exception { + final MasterService masterService = EasyMock.createNiceMock(MasterService.class); + EasyMock.expect(masterService.getMasterSecret()).andReturn("M4st3RSecret!".toCharArray()).anyTimes(); + EasyMock.replay(masterService); + + final AliasService aliasService = EasyMock.createNiceMock(AliasService.class); + EasyMock.replay(aliasService); + + final DerbyDBTrustedOidcIssuerService svc = new DerbyDBTrustedOidcIssuerService(); + svc.setAliasService(aliasService); + svc.setMasterService(masterService); + svc.init(config, Collections.emptyMap()); + return svc; + } + + private GatewayConfigImpl newConfig() { + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getGatewaySecurityDir()).andReturn(securityDir.getAbsolutePath()).anyTimes(); + EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes(); + EasyMock.expect(config.getDatabaseName()).andReturn(Paths.get(securityDir.getAbsolutePath(), "tokens").toString()).anyTimes(); + EasyMock.expect(config.getTrustedOidcIssuerMaxTrustedIssuers()).andReturn(10).anyTimes(); + EasyMock.expect(config.getTrustedOidcIssuerDiscoveryCacheTtlSecs()).andReturn(300).anyTimes(); + EasyMock.expect(config.getTrustedOidcIssuerDiscoveryConnectTimeoutMs()).andReturn(2000).anyTimes(); + EasyMock.expect(config.getTrustedOidcIssuerDiscoveryReadTimeoutMs()).andReturn(2000).anyTimes(); + EasyMock.replay(config); + return config; + } +} diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerServiceFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerServiceFactoryTest.java deleted file mode 100644 index be885e1ef..000000000 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/TrustedOidcIssuerServiceFactoryTest.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with this - * work for additional information regarding copyright ownership. The ASF - * licenses this file to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * <p> - * http://www.apache.org/licenses/LICENSE-2.0 - * <p> - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package org.apache.knox.gateway.services.knoxidf.trustedoidcissuer; - -import org.apache.knox.gateway.config.GatewayConfig; -import org.apache.knox.gateway.config.impl.GatewayConfigImpl; -import org.apache.knox.gateway.database.AbstractDataSourceFactory; -import org.apache.knox.gateway.database.DatabaseType; -import org.apache.knox.gateway.services.GatewayServices; -import org.apache.knox.gateway.services.ServiceType; -import org.apache.knox.gateway.services.factory.TrustedOidcIssuerServiceFactory; -import org.apache.knox.gateway.services.security.AliasService; -import org.apache.knox.gateway.services.topology.TopologyService; -import org.apache.knox.gateway.topology.Topology; -import org.easymock.EasyMock; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Test; - -import java.sql.DriverManager; -import java.sql.SQLException; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - -public class TrustedOidcIssuerServiceFactoryTest { - - private static final String DB_NAME = "trustedissuers_factory_test"; - private static final String DERBY_CREATE_URL = "jdbc:derby:memory:" + DB_NAME + ";create=true"; - private static final String DERBY_SHUTDOWN_URL = "jdbc:derby:memory:" + DB_NAME + ";shutdown=true"; - - @BeforeClass - public static void setUpClass() throws SQLException { - // Derby 10.14 does not recognize locales like en_001; force a standard locale. - java.util.Locale.setDefault(java.util.Locale.US); - DriverManager.getConnection(DERBY_CREATE_URL).close(); - } - - @AfterClass - public static void tearDownClass() { - try { - DriverManager.getConnection(DERBY_SHUTDOWN_URL); - } catch (SQLException e) { - if (!(e.getErrorCode() == 45000 && "08006".equals(e.getSQLState()))) { - throw new RuntimeException("Unexpected Derby shutdown error", e); - } - } - } - - // ------------------------------------------------------------------ - // Empty (no KNOXIDF) cases - // ------------------------------------------------------------------ - - /** Zero topologies → no topology service returns anything → Empty. */ - @Test - public void testNoTopologiesReturnsEmpty() throws Exception { - assertIsEmpty(createFactory(), buildEmptyGatewayServices(), emptyConfig()); - } - - /** Topologies exist but none contain KNOXIDF or KNOXIDF_ADMIN → Empty. */ - @Test - public void testTopologiesWithNonKnoxIdfRolesReturnsEmpty() throws Exception { - final GatewayServices gws = buildGatewayServicesWithTopology(withRoles("HDFS", "WEBHDFS"), null); - assertIsEmpty(createFactory(), gws, emptyConfig()); - } - - /** TopologyService is null (not yet registered) → Empty, no NPE. */ - @Test - public void testNullTopologyServiceReturnsEmpty() throws Exception { - final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class); - EasyMock.expect(gws.getService(ServiceType.TOPOLOGY_SERVICE)).andReturn(null).anyTimes(); - EasyMock.replay(gws); - assertIsEmpty(createFactory(), gws, emptyConfig()); - } - - // ------------------------------------------------------------------ - // JDBC cases - // ------------------------------------------------------------------ - - /** A single KNOXIDF topology → JDBC. */ - @Test - public void testKnoxIdfTopologyReturnsJdbc() throws Exception { - final GatewayServices gws = buildGatewayServicesWithTopology(withRoles("KNOXIDF"), derbyAlias()); - assertIsJdbc(createFactory(), gws, derbyConfig()); - } - - /** A single KNOXIDF_ADMIN-only topology (no KNOXIDF) → JDBC. */ - @Test - public void testKnoxIdfAdminOnlyTopologyReturnsJdbc() throws Exception { - final GatewayServices gws = buildGatewayServicesWithTopology(withRoles("KNOXIDF_ADMIN"), derbyAlias()); - assertIsJdbc(createFactory(), gws, derbyConfig()); - } - - /** Both KNOXIDF and KNOXIDF_ADMIN in the same topology → JDBC. */ - @Test - public void testBothRolesInSameTopologyReturnsJdbc() throws Exception { - final GatewayServices gws = buildGatewayServicesWithTopology( - withRoles("KNOXIDF", "KNOXIDF_ADMIN"), derbyAlias()); - assertIsJdbc(createFactory(), gws, derbyConfig()); - } - - /** Multiple topologies; only the second has KNOXIDF → JDBC (verifies the loop continues). */ - @Test - public void testMultipleTopologiesOneHasKnoxIdfReturnsJdbc() throws Exception { - final AliasService alias = derbyAlias(); - final GatewayServices gws = buildGatewayServicesWithMultipleTopologies( - withRoles("HDFS", "WEBHDFS"), withRoles("KNOXIDF"), alias); - assertIsJdbc(createFactory(), gws, derbyConfig()); - } - - // ------------------------------------------------------------------ - // Error handling - // ------------------------------------------------------------------ - - /** - * When JDBC service initialization fails (e.g. bad DB type), the factory must fall back - * to EmptyTrustedOidcIssuerService rather than propagating the exception. - */ - @Test - public void testJdbcInitFailureFallsBackToEmpty() throws Exception { - final GatewayServices gws = buildGatewayServicesWithTopology(withRoles("KNOXIDF"), derbyAlias()); - - final GatewayConfig brokenConfig = EasyMock.createNiceMock(GatewayConfig.class); - EasyMock.expect(brokenConfig.getDatabaseType()).andReturn("invalid_db_type").anyTimes(); - EasyMock.expect(brokenConfig.getServiceParameter(EasyMock.anyString(), EasyMock.anyString())) - .andReturn("").anyTimes(); - EasyMock.replay(brokenConfig); - - assertIsEmpty(createFactory(), gws, brokenConfig); - } - - // ------------------------------------------------------------------ - // Helpers - // ------------------------------------------------------------------ - - private static TrustedOidcIssuerServiceFactory createFactory() { - return new TrustedOidcIssuerServiceFactory(); - } - - private static void assertIsEmpty(TrustedOidcIssuerServiceFactory factory, - GatewayServices gws, GatewayConfig config) throws Exception { - final org.apache.knox.gateway.services.Service result = - factory.create(gws, ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, config, Map.of()); - assertNotNull(result); - assertTrue("Expected EmptyTrustedOidcIssuerService but got " + result.getClass().getSimpleName(), - result instanceof EmptyTrustedOidcIssuerService); - } - - private static void assertIsJdbc(TrustedOidcIssuerServiceFactory factory, - GatewayServices gws, GatewayConfig config) throws Exception { - final org.apache.knox.gateway.services.Service result = - factory.create(gws, ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, config, Map.of()); - assertNotNull(result); - assertTrue("Expected JdbcTrustedOidcIssuerService but got " + result.getClass().getSimpleName(), - result instanceof JdbcTrustedOidcIssuerService); - } - - /** GatewayServices with a TopologyService returning no topologies; no AliasService needed. */ - private static GatewayServices buildEmptyGatewayServices() { - final TopologyService topologyService = EasyMock.createNiceMock(TopologyService.class); - EasyMock.expect(topologyService.getTopologies()).andReturn(Collections.emptyList()).anyTimes(); - EasyMock.replay(topologyService); - - final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class); - EasyMock.expect(gws.getService(ServiceType.TOPOLOGY_SERVICE)) - .andReturn(topologyService).anyTimes(); - EasyMock.replay(gws); - return gws; - } - - /** - * Builds a {@link GatewayServices} mock with one topology that has the given service roles. - * {@code alias} may be null when no JDBC init will be attempted. - */ - private static GatewayServices buildGatewayServicesWithTopology( - String[] roles, AliasService alias) throws Exception { - final Topology topology = topologyWithRoles(roles); - return buildGatewayServices(Collections.singletonList(topology), alias); - } - - private static GatewayServices buildGatewayServicesWithMultipleTopologies( - String[] roles1, String[] roles2, AliasService alias) throws Exception { - return buildGatewayServices( - Arrays.asList(topologyWithRoles(roles1), topologyWithRoles(roles2)), alias); - } - - private static GatewayServices buildGatewayServices( - java.util.List<Topology> topologies, AliasService alias) throws Exception { - final TopologyService topologyService = EasyMock.createNiceMock(TopologyService.class); - EasyMock.expect(topologyService.getTopologies()).andReturn(topologies).anyTimes(); - EasyMock.replay(topologyService); - - final GatewayServices gws = EasyMock.createNiceMock(GatewayServices.class); - EasyMock.expect(gws.getService(ServiceType.TOPOLOGY_SERVICE)) - .andReturn(topologyService).anyTimes(); - if (alias != null) { - EasyMock.expect(gws.getService(ServiceType.ALIAS_SERVICE)) - .andReturn(alias).anyTimes(); - } - EasyMock.replay(gws); - return gws; - } - - private static Topology topologyWithRoles(String... roles) { - final Topology topology = EasyMock.createNiceMock(Topology.class); - final java.util.List<org.apache.knox.gateway.topology.Service> services = new java.util.ArrayList<>(); - for (String role : roles) { - final org.apache.knox.gateway.topology.Service svc = - EasyMock.createNiceMock(org.apache.knox.gateway.topology.Service.class); - EasyMock.expect(svc.getRole()).andReturn(role).anyTimes(); - EasyMock.replay(svc); - services.add(svc); - } - EasyMock.expect(topology.getServices()).andReturn(services).anyTimes(); - EasyMock.replay(topology); - return topology; - } - - private static String[] withRoles(String... roles) { - return roles; - } - - private static AliasService derbyAlias() throws Exception { - final AliasService alias = EasyMock.createNiceMock(AliasService.class); - EasyMock.expect(alias.getPasswordFromAliasForGateway( - AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME)).andReturn(null).anyTimes(); - EasyMock.expect(alias.getPasswordFromAliasForGateway( - AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME)).andReturn(null).anyTimes(); - EasyMock.replay(alias); - return alias; - } - - private static GatewayConfig derbyConfig() { - final GatewayConfigImpl config = new GatewayConfigImpl(); - config.set(GatewayConfigImpl.GATEWAY_DATABASE_TYPE, DatabaseType.DERBY.type()); - config.set(GatewayConfigImpl.GATEWAY_DATABASE_NAME, "memory:" + DB_NAME); - return config; - } - - /** Config mock that returns empty string for getServiceParameter (required for impl detection). */ - private static GatewayConfig emptyConfig() { - final GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class); - EasyMock.expect(config.getServiceParameter(EasyMock.anyString(), EasyMock.anyString())) - .andReturn("").anyTimes(); - EasyMock.replay(config); - return config; - } -}
