org.apache.knox
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java b/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java
index 2e2482aa1d..139e51862b 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java
@@ -17,17 +17,17 @@
*/
package org.apache.knox.gateway;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
-import javax.servlet.ServletRequest;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
-
-import org.apache.knox.gateway.i18n.messages.MessagesFactory;
-import org.eclipse.jetty.util.MultiMap;
-import org.eclipse.jetty.util.UrlEncoded;
/**
* HttpServletRequest
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java
index 7a7afadd13..a9d544fe85 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java
@@ -42,6 +42,14 @@ public abstract class AbstractDataSourceFactory {
public static final String DERBY_KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME = "createKnoxProvidersTableDerby.sql";
public static final String DERBY_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME = "createKnoxDescriptorsTableDerby.sql";
+ //KNOXIDF
+ public static final String KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityTable.sql";
+ public static final String KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityAttributesTable.sql";
+ public static final String ORACLE_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityTableOracle.sql";
+ public static final String ORACLE_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityAttributesTableOracle.sql";
+ public static final String DERBY_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityTableDerby.sql";
+ public static final String DERBY_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityAttributesTableDerby.sql";
+
public static final String DATABASE_USER_ALIAS_NAME = "gateway_database_user";
public static final String DATABASE_PASSWORD_ALIAS_NAME = "gateway_database_password";
public static final String DATABASE_TRUSTSTORE_PASSWORD_ALIAS_NAME = "gateway_database_ssl_truststore_password";
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java
index 2009872782..5052d5d5a0 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java
@@ -22,37 +22,55 @@ public enum DatabaseType {
AbstractDataSourceFactory.POSTGRES_TOKENS_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.POSTGRES_TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
- AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+ AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
),
MYSQL("mysql",
AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
- AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+ AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
),
MARIADB("mariadb",
AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
- AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+ AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
),
HSQL("hsql",
AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
- AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+ AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
),
DERBY("derbydb",
AbstractDataSourceFactory.DERBY_TOKENS_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.DERBY_TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.DERBY_KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
- AbstractDataSourceFactory.DERBY_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+ AbstractDataSourceFactory.DERBY_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.DERBY_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.DERBY_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.DERBY_KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
),
ORACLE("oracle",
AbstractDataSourceFactory.ORACLE_TOKENS_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.ORACLE_TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
AbstractDataSourceFactory.ORACLE_KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
- AbstractDataSourceFactory.ORACLE_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+ AbstractDataSourceFactory.ORACLE_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.ORACLE_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.ORACLE_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME,
+ AbstractDataSourceFactory.ORACLE_KNOXIDF_TRUSTED_OIDC_ISSUERS_TABLE_SQL
);
private final String type;
@@ -60,13 +78,21 @@ public enum DatabaseType {
private final String metadataTableSql;
private final String providersTableSql;
private final String descriptorsTableSql;
+ private final String federatedIdentityTableSql;
+ private final String federatedIdentityAttrTableSql;
+ private final String trustedOidcIssuersTableSql;
- DatabaseType(String type, String tokensTableSql, String metadataTableSql, String providersTableSql, String descriptorsTableSql) {
+ DatabaseType(String type, String tokensTableSql, String metadataTableSql, String providersTableSql,
+ String descriptorsTableSql, String federatedIdentityTableSql, String federatedIdentityAttrTableSql,
+ String trustedOidcIssuersTableSql) {
this.type = type;
this.tokensTableSql = tokensTableSql;
this.metadataTableSql = metadataTableSql;
this.providersTableSql = providersTableSql;
this.descriptorsTableSql = descriptorsTableSql;
+ this.federatedIdentityTableSql = federatedIdentityTableSql;
+ this.federatedIdentityAttrTableSql = federatedIdentityAttrTableSql;
+ this.trustedOidcIssuersTableSql = trustedOidcIssuersTableSql;
}
public String type() {
@@ -89,6 +115,18 @@ public String descriptorsTableSql() {
return descriptorsTableSql;
}
+ public String federatedIdentityTableSql() {
+ return federatedIdentityTableSql;
+ }
+
+ public String federatedIdentityAttrTableSql() {
+ return federatedIdentityAttrTableSql;
+ }
+
+ public String trustedOidcIssuersTableSql() {
+ return trustedOidcIssuersTableSql;
+ }
+
public static DatabaseType fromString(String dbType) {
for (DatabaseType dt : values()) {
if (dt.type.equalsIgnoreCase(dbType)) {
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java
new file mode 100644
index 0000000000..261b368998
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java
@@ -0,0 +1,36 @@
+/*
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.database;
+
+import org.apache.knox.gateway.services.token.impl.TokenStateDatabase;
+
+import javax.sql.DataSource;
+
+public class KnoxDatabase {
+
+ protected final DataSource dataSource;
+
+ public KnoxDatabase(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ protected void createTableIfNotExists(String tableName, String createSqlFileName) throws Exception {
+ if (!JDBCUtils.tableExists(tableName, dataSource)) {
+ JDBCUtils.createTableFromSQL(createSqlFileName, dataSource, TokenStateDatabase.class.getClassLoader());
+ }
+ }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java
index dfe4a4ea90..564f793007 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java
@@ -376,6 +376,13 @@ private static void initialize(
GatewayConfig gatewayConfig) {
WebAppDescriptor wad = context.getWebAppDescriptor();
String topoName = context.getTopology().getName();
+
+ final boolean hasKnoxIdf = services!= null && services.entrySet().stream().anyMatch( e -> e.getKey().equalsIgnoreCase("KNOXIDF") );
+ if (hasKnoxIdf) {
+ wad.createServlet().servletName("auth-consent-redirect").servletClass("org.apache.knox.gateway.service.knoxidf.AuthConsentServlet");
+ wad.createServletMapping().servletName("auth-consent-redirect").urlPattern("/authConsent");
+ }
+
boolean asyncSupported = gatewayConfig.isAsyncSupported() || gatewayConfig.isTopologyAsyncSupported(topoName);
if( applications == null ) {
String servletName = topoName + SERVLET_NAME_SUFFIX;
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
index 06ce95d93a..f51f599edd 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
@@ -90,6 +90,10 @@ public void init(GatewayConfig config, Map options) throws Servic
ldapService.init(config, options);
addService(ServiceType.LDAP_SERVICE, ldapService);
}
+
+ addService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, gatewayServiceFactory.create(this, ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, config, options));
+
+ addService(ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, gatewayServiceFactory.create(this, ServiceType.TRUSTED_OIDC_ISSUER_SERVICE, config, options));
}
@Override
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java
new file mode 100644
index 0000000000..f78f96af6d
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java
@@ -0,0 +1,97 @@
+/*
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 org.apache.knox.gateway.GatewayMessages;
+import org.apache.knox.gateway.config.GatewayConfig;
+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.federation.EmptyFederatedIdentitityService;
+import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService;
+import org.apache.knox.gateway.services.knoxidf.federation.JdbcFederatedIdentityService;
+import org.apache.knox.gateway.services.topology.TopologyService;
+import org.apache.knox.gateway.topology.Topology;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class FederatedIdentityServiceFactory extends AbstractServiceFactory {
+
+ private static final GatewayMessages LOG = MessagesFactory.get(GatewayMessages.class);
+ private static final String DEFAULT_IMPLEMENTATION = EmptyFederatedIdentitityService.class.getName();
+
+ @Override
+ protected Service createService(GatewayServices gatewayServices, ServiceType serviceType, GatewayConfig gatewayConfig, Map options, String implementation)
+ throws ServiceLifecycleException {
+
+ String implementationToUse = implementation;
+ // If implementation is empty, check if we should auto-enable JdbcFederatedIdentityService
+ if (isEmptyDefaultImplementation(implementationToUse)) {
+ if (isKnoxIdfEnabledInAnyTopology(gatewayServices)) {
+ implementationToUse = JdbcFederatedIdentityService.class.getName();
+ }
+ }
+
+ FederatedIdentityService service = null;
+ if (shouldCreateService(implementationToUse)) {
+ if (matchesImplementation(implementationToUse, EmptyFederatedIdentitityService.class, true)) {
+ service = new EmptyFederatedIdentitityService();
+ } else if (matchesImplementation(implementationToUse, JdbcFederatedIdentityService.class)) {
+ try {
+ try {
+ service = new JdbcFederatedIdentityService();
+ ((JdbcFederatedIdentityService) service).setAliasService(getAliasService(gatewayServices));
+ service.init(gatewayConfig, options);
+ } catch (ServiceLifecycleException e) {
+ LOG.errorInitializingService(implementationToUse, e.getMessage(), e);
+ service = new EmptyFederatedIdentitityService();
+ }
+ } catch (Exception e) {
+ throw new ServiceLifecycleException("Error while creating Federated Identity Service: " + e, e);
+ }
+ }
+ logServiceUsage(service.getClass().getName(), serviceType);
+ }
+ return service;
+ }
+
+ private boolean isKnoxIdfEnabledInAnyTopology(GatewayServices gatewayServices) {
+ final TopologyService topologyService = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE);
+ if (topologyService != null) {
+ for (Topology topology : topologyService.getTopologies()) {
+ if (topology.getServices().stream().anyMatch(service -> "KNOXIDF".equals(service.getRole()))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected ServiceType getServiceType() {
+ return ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE;
+ }
+
+ @Override
+ protected Collection getKnownImplementations() {
+ return List.of(DEFAULT_IMPLEMENTATION, JdbcFederatedIdentityService.class.getName());
+ }
+}
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
new file mode 100644
index 0000000000..daa8d11a4f
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java
@@ -0,0 +1,105 @@
+/*
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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 org.apache.knox.gateway.GatewayMessages;
+import org.apache.knox.gateway.config.GatewayConfig;
+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.EmptyTrustedOidcIssuerService;
+import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.JdbcTrustedOidcIssuerService;
+import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.TrustedOidcIssuerService;
+import org.apache.knox.gateway.services.topology.TopologyService;
+import org.apache.knox.gateway.topology.Topology;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class TrustedOidcIssuerServiceFactory extends AbstractServiceFactory {
+
+ private static final GatewayMessages LOG = MessagesFactory.get(GatewayMessages.class);
+ private static final String DEFAULT_IMPLEMENTATION = EmptyTrustedOidcIssuerService.class.getName();
+
+ @Override
+ protected Service createService(GatewayServices gatewayServices, ServiceType serviceType,
+ GatewayConfig gatewayConfig, Map options, String implementation)
+ throws ServiceLifecycleException {
+
+ String implementationToUse = implementation;
+ if (isEmptyDefaultImplementation(implementationToUse)) {
+ if (isKnoxIdfEnabledInAnyTopology(gatewayServices)) {
+ implementationToUse = JdbcTrustedOidcIssuerService.class.getName();
+ }
+ }
+
+ TrustedOidcIssuerService service = null;
+ if (shouldCreateService(implementationToUse)) {
+ if (matchesImplementation(implementationToUse, EmptyTrustedOidcIssuerService.class, true)) {
+ service = new EmptyTrustedOidcIssuerService();
+ } 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);
+ }
+ }
+ if (service != null) {
+ logServiceUsage(service.getClass().getName(), serviceType);
+ }
+ }
+ return service;
+ }
+
+ /**
+ * Returns true if any deployed topology contains a service with role {@code KNOXIDF}
+ * or {@code KNOXIDF_ADMIN}. The trusted issuer registry is activated by either role
+ * because the admin API ({@code KNOXIDF_ADMIN}) also needs to persist registrations.
+ */
+ private boolean isKnoxIdfEnabledInAnyTopology(GatewayServices gatewayServices) {
+ final TopologyService topologyService = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE);
+ if (topologyService != null) {
+ for (Topology topology : topologyService.getTopologies()) {
+ if (topology.getServices().stream().anyMatch(
+ s -> "KNOXIDF".equals(s.getRole()) || "KNOXIDF_ADMIN".equals(s.getRole()))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected ServiceType getServiceType() {
+ return ServiceType.TRUSTED_OIDC_ISSUER_SERVICE;
+ }
+
+ @Override
+ protected Collection getKnownImplementations() {
+ return List.of(DEFAULT_IMPLEMENTATION, JdbcTrustedOidcIssuerService.class.getName());
+ }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java
new file mode 100644
index 0000000000..8ce9e550e9
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java
@@ -0,0 +1,51 @@
+/*
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.federation;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+
+import java.util.Map;
+import java.util.Optional;
+
+public class EmptyFederatedIdentitityService implements FederatedIdentityService {
+ @Override
+ public void addFederatedIdentity(FederatedIdentity identity) {
+ }
+
+ @Override
+ public Optional findById(String identityId) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional findByProviderAndSubject(String provider, String externalIssuer, String externalSubject) {
+ return Optional.empty();
+ }
+
+ @Override
+ public void init(GatewayConfig config, Map options) throws ServiceLifecycleException {
+ }
+
+ @Override
+ public void start() throws ServiceLifecycleException {
+ }
+
+ @Override
+ public void stop() throws ServiceLifecycleException {
+ }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityDatabase.java
new file mode 100644
index 0000000000..649bf30388
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityDatabase.java
@@ -0,0 +1,132 @@
+/*
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.federation;
+
+import org.apache.knox.gateway.database.DatabaseType;
+import org.apache.knox.gateway.database.KnoxDatabase;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.HashMap;
+import java.util.Optional;
+
+class FederatedIdentityDatabase extends KnoxDatabase {
+ private static final String FEDERATED_IDENTITY_TABLE_NAME = "federated_identity";
+ private static final String FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME = "federated_identity_attr";
+ private static final String ADD_FEDERATED_IDENTITY_SQL = "INSERT INTO " + FEDERATED_IDENTITY_TABLE_NAME
+ + " (id, user_id, provider, external_subject, external_issuer, created_at) VALUES (?, ?, ?, ?, ?, ?)";
+ private static final String ADD_FEDERATED_IDENTITY_ATTR_SQL = "INSERT INTO " + FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME +
+ " (identity_id, attr_key, attr_value) VALUES (?, ?, ?)";
+ private static final String FETCH_FEDERATED_IDENTITY_BY_PROV_ISS_SUB_SQL = "SELECT * FROM " + FEDERATED_IDENTITY_TABLE_NAME +
+ " WHERE provider = ? AND external_issuer = ? AND external_subject = ?";
+ private static final String FETCH_FEDERATED_IDENTITY_SQL_BY_ID = "SELECT id, user_id, provider, external_subject, external_issuer, created_at FROM "
+ + FEDERATED_IDENTITY_TABLE_NAME + " WHERE id = ?";
+ private static final String FETCH_FEDERATED_IDENTITY_ATTR_SQL = "SELECT attr_key, attr_value FROM " + FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME + " WHERE identity_id = ?";
+
+ FederatedIdentityDatabase(DataSource dataSource, String dbType) throws Exception {
+ super(dataSource);
+ DatabaseType databaseType = DatabaseType.fromString(dbType);
+ createTableIfNotExists(FEDERATED_IDENTITY_TABLE_NAME, databaseType.federatedIdentityTableSql());
+ createTableIfNotExists(FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME, databaseType.federatedIdentityAttrTableSql());
+ }
+
+ void addFederatedIdentity(FederatedIdentity identity) throws SQLException {
+ // save core metadata first
+ try (Connection connection = dataSource.getConnection(); PreparedStatement addFederatedIdentityStatement = connection.prepareStatement(ADD_FEDERATED_IDENTITY_SQL)) {
+ addFederatedIdentityStatement.setString(1, identity.getId());
+ addFederatedIdentityStatement.setString(2, identity.getUserId());
+ addFederatedIdentityStatement.setString(3, identity.getProvider());
+ addFederatedIdentityStatement.setString(4, identity.getExternalSubject());
+ addFederatedIdentityStatement.setString(5, identity.getExternalIssuer());
+ addFederatedIdentityStatement.setTimestamp(6, Timestamp.from(identity.getCreatedAt()));
+ addFederatedIdentityStatement.executeUpdate();
+ }
+
+ // save attributes
+ try (Connection connection = dataSource.getConnection(); PreparedStatement addFederatedIdentityAttrStatement = connection.prepareStatement(ADD_FEDERATED_IDENTITY_ATTR_SQL)) {
+ for (var attribute : identity.getAttributes().entrySet()) {
+ addFederatedIdentityAttrStatement.setString(1, identity.getId());
+ addFederatedIdentityAttrStatement.setString(2, attribute.getKey());
+ addFederatedIdentityAttrStatement.setString(3, attribute.getValue());
+ addFederatedIdentityAttrStatement.addBatch();
+ }
+ addFederatedIdentityAttrStatement.executeBatch();
+ }
+ }
+
+
+ Optional findByProviderAndSubject(String provider, String issuer, String subject) throws SQLException {
+ FederatedIdentity federatedIdentity = null;
+ try (Connection connection = dataSource.getConnection(); PreparedStatement getFederatedIdentityStatement = connection.prepareStatement(FETCH_FEDERATED_IDENTITY_BY_PROV_ISS_SUB_SQL)) {
+ getFederatedIdentityStatement.setString(1, provider);
+ getFederatedIdentityStatement.setString(2, issuer);
+ getFederatedIdentityStatement.setString(3, subject);
+ try (ResultSet rs = getFederatedIdentityStatement.executeQuery()) {
+ if (rs.next()) {
+ federatedIdentity = new FederatedIdentity(
+ rs.getString("id"),
+ rs.getString("user_id"),
+ provider,
+ subject,
+ issuer,
+ rs.getTimestamp("created_at").toInstant(), new HashMap<>());
+ } else {
+ return Optional.empty();
+ }
+ }
+ }
+ populateAttributes(federatedIdentity);
+ return Optional.of(federatedIdentity);
+ }
+
+ Optional findById(String id) throws SQLException {
+ FederatedIdentity federatedIdentity = null;
+ try (Connection connection = dataSource.getConnection(); PreparedStatement getFederatedIdentityStatement = connection.prepareStatement(FETCH_FEDERATED_IDENTITY_SQL_BY_ID)) {
+ getFederatedIdentityStatement.setString(1, id);
+ try (ResultSet rs = getFederatedIdentityStatement.executeQuery()) {
+ if (rs.next()) {
+ federatedIdentity = new FederatedIdentity(
+ id,
+ rs.getString("user_id"),
+ rs.getString("provider"),
+ rs.getString("external_subject"),
+ rs.getString("external_issuer"),
+ rs.getTimestamp("created_at").toInstant(), new HashMap<>());
+ } else {
+ return Optional.empty();
+ }
+ }
+ }
+ populateAttributes(federatedIdentity);
+ return Optional.of(federatedIdentity);
+ }
+
+ private void populateAttributes(FederatedIdentity federatedIdentity) throws SQLException {
+ try (Connection connection = dataSource.getConnection(); PreparedStatement getFederatedIdentityAttrStatement = connection.prepareStatement(FETCH_FEDERATED_IDENTITY_ATTR_SQL)) {
+ getFederatedIdentityAttrStatement.setString(1, federatedIdentity.getId());
+ try (ResultSet rs = getFederatedIdentityAttrStatement.executeQuery()) {
+ while (rs.next()) {
+ federatedIdentity.getAttributes().put(rs.getString(1), rs.getString(2));
+ }
+ }
+ }
+ }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceMessages.java
new file mode 100644
index 0000000000..d6f820a1d5
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceMessages.java
@@ -0,0 +1,35 @@
+/*
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.federation;
+
+import org.apache.knox.gateway.i18n.messages.Message;
+import org.apache.knox.gateway.i18n.messages.MessageLevel;
+import org.apache.knox.gateway.i18n.messages.Messages;
+import org.apache.knox.gateway.i18n.messages.StackTrace;
+
+@Messages(logger="org.apache.knox.gateway.knoxidf.federated.identity.service")
+public interface FederatedIdentityServiceMessages {
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while saving federated identity {0} in the database : {1}")
+ void errorSavingFederatedIdentityInDatabase(String federatedIdentityId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while fetching federated identity ({0} / {1} / {2}) from the database : {3}")
+ void errorFetchingFederatedIdentityFromDatabase(String provider, String issuer, String subject, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while fetching federated identity ({0}) from the database : {1}")
+ void errorFetchingFederatedIdentityFromDatabase(String id, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java
new file mode 100644
index 0000000000..8e26bdc1d5
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java
@@ -0,0 +1,108 @@
+/*
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.federation;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.database.DataSourceProvider;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+
+import java.sql.SQLException;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class JdbcFederatedIdentityService implements FederatedIdentityService {
+ private static final FederatedIdentityServiceMessages LOG = MessagesFactory.get(FederatedIdentityServiceMessages.class);
+
+ private final AtomicBoolean initialized = new AtomicBoolean(false);
+ private final Lock initLock = new ReentrantLock(true);
+ private AliasService aliasService; // connection username/pw are stored here
+ private FederatedIdentityDatabase federatedIdentityDatabase;
+
+ @Override
+ public void init(GatewayConfig config, Map options) throws ServiceLifecycleException {
+ if (!initialized.get()) {
+ initLock.lock();
+ try {
+ if (aliasService == null) {
+ throw new ServiceLifecycleException("The required AliasService reference has not been set.");
+ }
+ try {
+ this.federatedIdentityDatabase = new FederatedIdentityDatabase(DataSourceProvider.getDataSource(config, aliasService), config.getDatabaseType());
+ initialized.set(true);
+ } catch (Exception e) {
+ throw new ServiceLifecycleException("Error while initiating JDBCTokenStateService: " + e, e);
+ }
+ } finally {
+ initLock.unlock();
+ }
+ }
+ }
+
+ @Override
+ public void start() throws ServiceLifecycleException {
+ }
+
+ @Override
+ public void stop() throws ServiceLifecycleException {
+ }
+
+ public void setAliasService(AliasService aliasService) {
+ this.aliasService = aliasService;
+ }
+
+ protected AliasService getAliasService() {
+ return aliasService;
+ }
+
+ @Override
+ public void addFederatedIdentity(FederatedIdentity identity) {
+ try {
+ if (findByProviderAndSubject(identity.getProvider(), identity.getExternalIssuer(), identity.getExternalSubject()).isEmpty()) {
+ federatedIdentityDatabase.addFederatedIdentity(identity);
+ }
+ } catch (SQLException e) {
+ LOG.errorSavingFederatedIdentityInDatabase(identity.getId(), e.getMessage(), e);
+ throw new FederatedIdentityServiceException("An error occurred while saving Federated Identity " + identity.getId() + " in the database", e);
+ }
+ }
+
+ @Override
+ public Optional findByProviderAndSubject(String provider, String issuer, String subject) {
+ try {
+ return federatedIdentityDatabase.findByProviderAndSubject(provider, issuer, subject);
+ } catch (SQLException e) {
+ LOG.errorFetchingFederatedIdentityFromDatabase(provider, subject, issuer, e.getMessage(), e);
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public Optional findById(String id) {
+ try {
+ return federatedIdentityDatabase.findById(id);
+ } catch (SQLException e) {
+ LOG.errorFetchingFederatedIdentityFromDatabase(id, e.getMessage(), e);
+ }
+ return Optional.empty();
+ }
+
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/EmptyTrustedOidcIssuerService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/EmptyTrustedOidcIssuerService.java
new file mode 100644
index 0000000000..506b31d657
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/EmptyTrustedOidcIssuerService.java
@@ -0,0 +1,81 @@
+/*
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.services.ServiceLifecycleException;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * No-op stub used when the KNOXIDF or KNOXIDF_ADMIN service role is not deployed.
+ * Read methods return safe empty results; mutating methods throw
+ * {@link UnsupportedOperationException}.
+ */
+public class EmptyTrustedOidcIssuerService implements TrustedOidcIssuerService {
+
+ @Override
+ public void init(GatewayConfig config, Map options) throws ServiceLifecycleException {
+ }
+
+ @Override
+ public void start() throws ServiceLifecycleException {
+ }
+
+ @Override
+ public void stop() throws ServiceLifecycleException {
+ }
+
+ @Override
+ public boolean isTrusted(String issuerUrl) {
+ return false;
+ }
+
+ @Override
+ public boolean isDynamicJwks(String issuerUrl) {
+ return false;
+ }
+
+ @Override
+ public Optional resolveJwksUri(String issuerUrl) {
+ return Optional.empty();
+ }
+
+ @Override
+ public void refreshJwksUri(String issuerUrl) {
+ }
+
+ @Override
+ public void register(TrustedOidcIssuer issuer) {
+ throw new UnsupportedOperationException("TrustedOidcIssuerService is not enabled; "
+ + "deploy the KNOXIDF or KNOXIDF_ADMIN service role to activate it.");
+ }
+
+ @Override
+ public void deregister(String issuerUrl) {
+ throw new UnsupportedOperationException("TrustedOidcIssuerService is not enabled; "
+ + "deploy the KNOXIDF or KNOXIDF_ADMIN service role to activate it.");
+ }
+
+ @Override
+ public List list() {
+ return Collections.emptyList();
+ }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/JdbcTrustedOidcIssuerService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/JdbcTrustedOidcIssuerService.java
new file mode 100644
index 0000000000..0b1e0a31e7
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/trustedoidcissuer/JdbcTrustedOidcIssuerService.java
@@ -0,0 +1,200 @@
+/*
+ * 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
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.hadoop.conf.Configuration;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.database.DataSourceProvider;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * JDBC-backed implementation of {@link TrustedOidcIssuerService}.
+ *
+ * Maintains an in-memory registry snapshot as an {@link AtomicReference} to an immutable
+ * {@link Map}. Reads ({@link #isTrusted}, {@link #isDynamicJwks}, {@link #list}) are
+ * lock-free and always see a consistent snapshot. Writes ({@link #register},
+ * {@link #deregister}) are synchronized: the DB is committed first, then the snapshot is
+ * rebuilt from a fresh SELECT to guarantee the in-memory state cannot diverge from
+ * persistent storage.
+ *
+ * HA note: each Knox node maintains its own snapshot. A registration on node A updates
+ * that node's snapshot immediately; other nodes' snapshots remain stale until restart.
+ */
+public class JdbcTrustedOidcIssuerService implements TrustedOidcIssuerService {
+
+ private static final TrustedOidcIssuerServiceMessages LOG =
+ MessagesFactory.get(TrustedOidcIssuerServiceMessages.class);
+
+ static final String MAX_TRUSTED_ISSUERS_CONFIG = "gateway.trustedoidcissuer.max.issuers";
+ private static final int DEFAULT_MAX_TRUSTED_ISSUERS = 10_000;
+
+ private final AtomicBoolean initialized = new AtomicBoolean(false);
+ private final Lock initLock = new ReentrantLock(true);
+
+ private final AtomicReference
+
+