KNOX-2390 - Let end-users configure SAML2 client configuration using Pac4J provider parameters (#348)
diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecorator.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecorator.java
new file mode 100644
index 0000000..5021536
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecorator.java
@@ -0,0 +1,40 @@
+/*
+ * 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.pac4j.config;
+
+import java.util.List;
+import java.util.Map;
+
+import org.pac4j.core.client.Client;
+import org.pac4j.core.http.callback.PathParameterCallbackUrlResolver;
+import org.pac4j.oidc.client.AzureAdClient;
+
+public class AzureADClientConfigurationDecorator implements ClientConfigurationDecorator {
+ private static final String AZURE_AD_CLIENT_CLASS_NAME = AzureAdClient.class.getSimpleName();
+
+ @Override
+ public void decorateClients(List<Client> clients, Map<String, String> properties) {
+ for (Client client : clients) {
+ if (AZURE_AD_CLIENT_CLASS_NAME.equalsIgnoreCase(client.getName())) {
+ // special handling for Azure AD, use path separators instead of query params
+ ((AzureAdClient) client).setCallbackUrlResolver(new PathParameterCallbackUrlResolver());
+ }
+ }
+ }
+
+}
diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/ClientConfigurationDecorator.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/ClientConfigurationDecorator.java
new file mode 100644
index 0000000..91bb099
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/ClientConfigurationDecorator.java
@@ -0,0 +1,40 @@
+/*
+ * 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.pac4j.config;
+
+import java.util.List;
+import java.util.Map;
+
+import org.pac4j.core.client.Client;
+
+/**
+ * Defines the contract of decorating different type of Pac4J client configurations
+ */
+public interface ClientConfigurationDecorator {
+
+ /**
+ * Decorates the given clients' configuration using the given properties (if applicable)
+ *
+ * @param clients
+ * the client, whose configuration should be decorated
+ * @param properties
+ * the properties which may contain the required information to decorate the clients
+ */
+ void decorateClients(List<Client> clients, Map<String, String> properties);
+
+}
diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecorator.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecorator.java
new file mode 100644
index 0000000..8039c46
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecorator.java
@@ -0,0 +1,45 @@
+/*
+ * 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.pac4j.config;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import org.pac4j.core.client.Client;
+
+public class Pac4jClientConfigurationDecorator implements ClientConfigurationDecorator {
+
+ private static final List<ClientConfigurationDecorator> DEFAULT_DECORATORS = Arrays.asList(new SAML2ClientConfigurationDecorator(), new AzureADClientConfigurationDecorator());
+ private final List<ClientConfigurationDecorator> decorators;
+
+ public Pac4jClientConfigurationDecorator() {
+ this(DEFAULT_DECORATORS);
+ }
+
+ // package protected so that it's visible in unit tests
+ Pac4jClientConfigurationDecorator(List<ClientConfigurationDecorator> decorators) {
+ this.decorators = decorators;
+ }
+
+ @Override
+ public void decorateClients(List<Client> clients, Map<String, String> properties) {
+ decorators.forEach(decorator -> decorator.decorateClients(clients, properties));
+ }
+
+}
diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecorator.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecorator.java
new file mode 100644
index 0000000..1444890
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecorator.java
@@ -0,0 +1,75 @@
+/*
+ * 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.pac4j.config;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.pac4j.core.client.Client;
+import org.pac4j.saml.client.SAML2Client;
+
+public class SAML2ClientConfigurationDecorator implements ClientConfigurationDecorator {
+
+ private static final String SAML2_CLIENT_CLASS_NAME = SAML2Client.class.getSimpleName();
+ private static final String CONFIG_NAME_USE_NAME_QUALIFIER = "useNameQualifier";
+ private static final String CONFIG_NAME_USE_FORCE_AUTH = "forceAuth";
+ private static final String CONFIG_NAME_USE_PASSIVE = "passive";
+ private static final String CONFIG_NAME_NAMEID_POLICY_FORMAT = "nameIdPolicyFormat";
+
+ @Override
+ public void decorateClients(List<Client> clients, Map<String, String> properties) {
+ for (Client client : clients) {
+ if (SAML2_CLIENT_CLASS_NAME.equalsIgnoreCase(client.getName())) {
+ final SAML2Client saml2Client = (SAML2Client) client;
+ setUseNameQualifierFlag(properties, saml2Client);
+ setForceAuthFlag(properties, saml2Client);
+ setPassiveFlag(properties, saml2Client);
+ setNameIdPolicyFormat(properties, saml2Client);
+ }
+ }
+ }
+
+ private void setUseNameQualifierFlag(Map<String, String> properties, final SAML2Client saml2Client) {
+ final String useNameQualifier = properties.get(CONFIG_NAME_USE_NAME_QUALIFIER);
+ if (StringUtils.isNotBlank(useNameQualifier)) {
+ saml2Client.getConfiguration().setUseNameQualifier(Boolean.valueOf(useNameQualifier));
+ }
+ }
+
+ private void setForceAuthFlag(Map<String, String> properties, final SAML2Client saml2Client) {
+ final String forceAuth = properties.get(CONFIG_NAME_USE_FORCE_AUTH);
+ if (StringUtils.isNotBlank(forceAuth)) {
+ saml2Client.getConfiguration().setForceAuth(Boolean.valueOf(forceAuth));
+ }
+ }
+
+ private void setPassiveFlag(Map<String, String> properties, final SAML2Client saml2Client) {
+ final String passive = properties.get(CONFIG_NAME_USE_PASSIVE);
+ if (StringUtils.isNotBlank(passive)) {
+ saml2Client.getConfiguration().setPassive(Boolean.valueOf(passive));
+ }
+ }
+
+ private void setNameIdPolicyFormat(Map<String, String> properties, final SAML2Client saml2Client) {
+ final String nameIdPolicyFormat = properties.get(CONFIG_NAME_NAMEID_POLICY_FORMAT);
+ if (StringUtils.isNotBlank(nameIdPolicyFormat)) {
+ saml2Client.getConfiguration().setNameIdPolicyFormat(nameIdPolicyFormat);
+ }
+ }
+}
diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java
index 4eaabaa..cd6beaf 100644
--- a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java
+++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java
@@ -20,6 +20,8 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.pac4j.Pac4jMessages;
+import org.apache.knox.gateway.pac4j.config.ClientConfigurationDecorator;
+import org.apache.knox.gateway.pac4j.config.Pac4jClientConfigurationDecorator;
import org.apache.knox.gateway.pac4j.session.KnoxSessionStore;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.GatewayServices;
@@ -34,7 +36,6 @@
import org.pac4j.core.config.Config;
import org.pac4j.core.context.session.J2ESessionStore;
import org.pac4j.core.context.session.SessionStore;
-import org.pac4j.core.http.callback.PathParameterCallbackUrlResolver;
import org.pac4j.core.util.CommonHelper;
import org.pac4j.http.client.indirect.IndirectBasicAuthClient;
import org.pac4j.http.credentials.authenticator.test.SimpleTestUsernamePasswordAuthenticator;
@@ -74,6 +75,8 @@
private static Pac4jMessages log = MessagesFactory.get(Pac4jMessages.class);
+ private static final ClientConfigurationDecorator PAC4J_CLIENT_CONFIGURATION_DECORATOR = new Pac4jClientConfigurationDecorator();
+
public static final String TEST_BASIC_AUTH = "testBasicAuth";
public static final String PAC4J_CALLBACK_URL = "pac4j.callbackUrl";
@@ -181,19 +184,11 @@
log.atLeastOnePac4jClientMustBeDefined();
throw new ServletException("At least one pac4j client must be defined.");
}
- if (CommonHelper.isBlank(clientNameParameter)) {
- clientName = clients.get(0).getName();
- } else {
- clientName = clientNameParameter;
- }
- /* special handling for Azure AD, use path separators instead of query params */
- clients.forEach( client -> {
- if(client.getName().equalsIgnoreCase(AzureAdClient.class.getSimpleName())) {
- ((AzureAdClient)client).setCallbackUrlResolver(new PathParameterCallbackUrlResolver());
- }
- });
+ clientName = CommonHelper.isBlank(clientNameParameter) ? clients.get(0).getName() : clientNameParameter;
+ //decorating client configuration (if needed)
+ PAC4J_CLIENT_CONFIGURATION_DECORATOR.decorateClients(clients, properties);
}
diff --git a/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecoratorTest.java b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecoratorTest.java
new file mode 100644
index 0000000..0605025
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/AzureADClientConfigurationDecoratorTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.pac4j.config;
+
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+
+import org.junit.Test;
+import org.pac4j.core.http.callback.PathParameterCallbackUrlResolver;
+import org.pac4j.oidc.client.AzureAdClient;
+import org.pac4j.oidc.config.AzureAdOidcConfiguration;
+
+public class AzureADClientConfigurationDecoratorTest {
+
+ @Test
+ public void testAzureADClientConfigurationDecoration() throws Exception {
+ final AzureAdOidcConfiguration azureAdConfig = new AzureAdOidcConfiguration();
+ final AzureAdClient client = new AzureAdClient(azureAdConfig);
+ final AzureADClientConfigurationDecorator azureConfigDecorator = new AzureADClientConfigurationDecorator();
+ azureConfigDecorator.decorateClients(Collections.singletonList(client), null);
+ assertTrue(client.getCallbackUrlResolver() instanceof PathParameterCallbackUrlResolver);
+ }
+
+}
diff --git a/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecoratorTest.java b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecoratorTest.java
new file mode 100644
index 0000000..f40aacf
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/Pac4jClientConfigurationDecoratorTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.pac4j.config;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+import org.pac4j.core.client.Client;
+
+public class Pac4jClientConfigurationDecoratorTest {
+
+ @Test
+ public void testClientConfigDecoration() throws Exception {
+ final AtomicInteger tested = new AtomicInteger(0);
+ final AtomicInteger decorated = new AtomicInteger(0);
+
+ final ClientConfigurationDecorator passiveDecorator = new TestClientConfigurationDecorator(tested, decorated, false);
+ final ClientConfigurationDecorator activeDecorator = new TestClientConfigurationDecorator(tested, decorated, true);
+ final Pac4jClientConfigurationDecorator pac4jConfigurationDecorator = new Pac4jClientConfigurationDecorator(Arrays.asList(passiveDecorator, activeDecorator));
+ final Client client = EasyMock.createNiceMock(Client.class);
+ pac4jConfigurationDecorator.decorateClients(Collections.singletonList(client), null);
+ assertEquals(2, tested.get());
+ assertEquals(1, decorated.get());
+ }
+
+ private static class TestClientConfigurationDecorator implements ClientConfigurationDecorator {
+
+ private final AtomicInteger tested;
+ private final AtomicInteger decorated;
+ private final boolean decorate;
+
+ TestClientConfigurationDecorator(AtomicInteger testedClientsNum, AtomicInteger decoratedClientsNum, boolean decorate) {
+ this.tested = testedClientsNum;
+ this.decorated = decoratedClientsNum;
+ this.decorate = decorate;
+ }
+
+ @Override
+ public void decorateClients(List<Client> clients, Map<String, String> properties) {
+ clients.forEach(client -> {
+ tested.incrementAndGet();
+ if (decorate) {
+ decorated.incrementAndGet();
+ }
+ });
+ }
+ }
+
+}
diff --git a/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecoratorTest.java b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecoratorTest.java
new file mode 100644
index 0000000..c78c6fc
--- /dev/null
+++ b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/config/SAML2ClientConfigurationDecoratorTest.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.pac4j.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+import org.pac4j.saml.client.SAML2Client;
+import org.pac4j.saml.config.SAML2Configuration;
+
+public class SAML2ClientConfigurationDecoratorTest {
+
+ @Test
+ public void testSaml2ClientConfigurationDecoration() throws Exception {
+ final SAML2Configuration saml2Configuration = new SAML2Configuration();
+ final SAML2Client client = new SAML2Client(saml2Configuration);
+ final Map<String, String> properties = new HashMap<>();
+ properties.put("useNameQualifier", "true");
+ properties.put("forceAuth", "true");
+ properties.put("passive", "true");
+ properties.put("nameIdPolicyFormat", "testPolicyFormat");
+
+ final SAML2ClientConfigurationDecorator saml2ConfigurationDecorator = new SAML2ClientConfigurationDecorator();
+ saml2ConfigurationDecorator.decorateClients(Collections.singletonList(client), properties);
+ assertTrue(saml2Configuration.isUseNameQualifier());
+ assertTrue(saml2Configuration.isForceAuth());
+ assertTrue(saml2Configuration.isPassive());
+ assertEquals("testPolicyFormat", saml2Configuration.getNameIdPolicyFormat());
+ }
+
+}