http://git-wip-us.apache.org/repos/asf/knox/blob/58780d37/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java ---------------------------------------------------------------------- diff --cc gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java index a87c8d0,0000000..fe39f25 mode 100644,000000..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 @@@ -1,215 -1,0 +1,214 @@@ +/** + * 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.filter; + +import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import org.apache.knox.gateway.pac4j.Pac4jMessages; +import org.apache.knox.gateway.pac4j.session.KnoxSessionStore; +import org.apache.knox.gateway.services.GatewayServices; +import org.apache.knox.gateway.services.security.KeystoreService; +import org.apache.knox.gateway.services.security.MasterService; +import org.apache.knox.gateway.services.security.AliasService; +import org.apache.knox.gateway.services.security.AliasServiceException; +import org.apache.knox.gateway.services.security.CryptoService; +import org.pac4j.config.client.PropertiesConfigFactory; +import org.pac4j.core.client.Client; +import org.pac4j.core.config.Config; +import org.pac4j.core.config.ConfigSingleton; +import org.pac4j.core.context.J2EContext; - import org.pac4j.core.context.Pac4jConstants; +import org.pac4j.core.util.CommonHelper; +import org.pac4j.http.client.indirect.IndirectBasicAuthClient; +import org.pac4j.http.credentials.authenticator.test.SimpleTestUsernamePasswordAuthenticator; +import org.pac4j.j2e.filter.CallbackFilter; - import org.pac4j.j2e.filter.RequiresAuthenticationFilter; ++import org.pac4j.j2e.filter.SecurityFilter; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * <p>This is the main filter for the pac4j provider. The pac4j provider module heavily relies on the j2e-pac4j library (https://github.com/pac4j/j2e-pac4j).</p> + * <p>This filter dispatches the HTTP calls between the j2e-pac4j filters:</p> + * <ul> + * <li>to the {@link CallbackFilter} if the <code>client_name</code> parameter exists: it finishes the authentication process</li> + * <li>to the {@link RequiresAuthenticationFilter} otherwise: it starts the authentication process (redirection to the identity provider) if the user is not authenticated</li> + * </ul> + * <p>It uses the {@link KnoxSessionStore} to manage session data. The generated cookies are defined on a domain name + * which can be configured via the domain suffix parameter: <code>pac4j.cookie.domain.suffix</code>.</p> + * <p>The callback url must be defined to the current protected url (KnoxSSO service for example) via the parameter: <code>pac4j.callbackUrl</code>.</p> + * + * @since 0.8.0 + */ +public class Pac4jDispatcherFilter implements Filter { + + private static Pac4jMessages log = MessagesFactory.get(Pac4jMessages.class); + + public static final String TEST_BASIC_AUTH = "testBasicAuth"; + + public static final String PAC4J_CALLBACK_URL = "pac4j.callbackUrl"; + + public static final String PAC4J_CALLBACK_PARAMETER = "pac4jCallback"; + + private static final String PAC4J_COOKIE_DOMAIN_SUFFIX_PARAM = "pac4j.cookie.domain.suffix"; + + private CallbackFilter callbackFilter; + - private RequiresAuthenticationFilter requiresAuthenticationFilter; ++ private SecurityFilter securityFilter; + private MasterService masterService = null; + private KeystoreService keystoreService = null; + private AliasService aliasService = null; + + @Override + public void init( FilterConfig filterConfig ) throws ServletException { + // JWT service + final ServletContext context = filterConfig.getServletContext(); + CryptoService cryptoService = null; + String clusterName = null; + if (context != null) { + GatewayServices services = (GatewayServices) context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); + clusterName = (String) context.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE); + if (services != null) { + keystoreService = (KeystoreService) services.getService(GatewayServices.KEYSTORE_SERVICE); + cryptoService = (CryptoService) services.getService(GatewayServices.CRYPTO_SERVICE); + aliasService = (AliasService) services.getService(GatewayServices.ALIAS_SERVICE); + masterService = (MasterService) services.getService("MasterService"); + } + } + // crypto service, alias service and cluster name are mandatory + if (cryptoService == null || aliasService == null || clusterName == null) { + log.cryptoServiceAndAliasServiceAndClusterNameRequired(); + throw new ServletException("The crypto service, alias service and cluster name are required."); + } + try { + aliasService.getPasswordFromAliasForCluster(clusterName, KnoxSessionStore.PAC4J_PASSWORD, true); + } catch (AliasServiceException e) { + log.unableToGenerateAPasswordForEncryption(e); + throw new ServletException("Unable to generate a password for encryption."); + } + + // url to SSO authentication provider + String pac4jCallbackUrl = filterConfig.getInitParameter(PAC4J_CALLBACK_URL); + if (pac4jCallbackUrl == null) { + log.ssoAuthenticationProviderUrlRequired(); + throw new ServletException("Required pac4j callback URL is missing."); + } + // add the callback parameter to know it's a callback + pac4jCallbackUrl = CommonHelper.addParameter(pac4jCallbackUrl, PAC4J_CALLBACK_PARAMETER, "true"); + + final Config config; + final String clientName; + // client name from servlet parameter (mandatory) - final String clientNameParameter = filterConfig.getInitParameter(Pac4jConstants.CLIENT_NAME); ++ final String clientNameParameter = filterConfig.getInitParameter("clientName"); + if (clientNameParameter == null) { + log.clientNameParameterRequired(); + throw new ServletException("Required pac4j clientName parameter is missing."); + } + if (TEST_BASIC_AUTH.equalsIgnoreCase(clientNameParameter)) { + // test configuration + final IndirectBasicAuthClient indirectBasicAuthClient = new IndirectBasicAuthClient(new SimpleTestUsernamePasswordAuthenticator()); + indirectBasicAuthClient.setRealmName("Knox TEST"); + config = new Config(pac4jCallbackUrl, indirectBasicAuthClient); + clientName = "IndirectBasicAuthClient"; + } else { + // get clients from the init parameters + final Map<String, String> properties = new HashMap<>(); + final Enumeration<String> names = filterConfig.getInitParameterNames(); + addDefaultConfig(clientNameParameter, properties); + while (names.hasMoreElements()) { + final String key = names.nextElement(); + properties.put(key, filterConfig.getInitParameter(key)); + } + final PropertiesConfigFactory propertiesConfigFactory = new PropertiesConfigFactory(pac4jCallbackUrl, properties); + config = propertiesConfigFactory.build(); + final List<Client> clients = config.getClients().getClients(); + if (clients == null || clients.size() == 0) { + 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; + } + } + + callbackFilter = new CallbackFilter(); - requiresAuthenticationFilter = new RequiresAuthenticationFilter(); - requiresAuthenticationFilter.setClientName(clientName); - requiresAuthenticationFilter.setConfig(config); ++ securityFilter = new SecurityFilter(); ++ securityFilter.setClients(clientName); ++ securityFilter.setConfig(config); + + final String domainSuffix = filterConfig.getInitParameter(PAC4J_COOKIE_DOMAIN_SUFFIX_PARAM); + config.setSessionStore(new KnoxSessionStore(cryptoService, clusterName, domainSuffix)); + ConfigSingleton.setConfig(config); + } + + private void addDefaultConfig(String clientNameParameter, Map<String, String> properties) { + // add default saml params + if (clientNameParameter.contains("SAML2Client")) { + properties.put(PropertiesConfigFactory.SAML_KEYSTORE_PATH, + keystoreService.getKeystorePath()); + + properties.put(PropertiesConfigFactory.SAML_KEYSTORE_PASSWORD, + new String(masterService.getMasterSecret())); + + // check for provisioned alias for private key + char[] gip = null; + try { + gip = aliasService.getGatewayIdentityPassphrase(); + } + catch(AliasServiceException ase) { + log.noPrivateKeyPasshraseProvisioned(ase); + } + if (gip != null) { + properties.put(PropertiesConfigFactory.SAML_PRIVATE_KEY_PASSWORD, + new String(gip)); + } + else { + // no alias provisioned then use the master + properties.put(PropertiesConfigFactory.SAML_PRIVATE_KEY_PASSWORD, + new String(masterService.getMasterSecret())); + } + } + } + + @Override + public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + + final HttpServletRequest request = (HttpServletRequest) servletRequest; + final HttpServletResponse response = (HttpServletResponse) servletResponse; + final J2EContext context = new J2EContext(request, response, ConfigSingleton.getConfig().getSessionStore()); + + // it's a callback from an identity provider + if (request.getParameter(PAC4J_CALLBACK_PARAMETER) != null) { + // apply CallbackFilter + callbackFilter.doFilter(servletRequest, servletResponse, filterChain); + } else { + // otherwise just apply security and requires authentication + // apply RequiresAuthenticationFilter - requiresAuthenticationFilter.doFilter(servletRequest, servletResponse, filterChain); ++ securityFilter.doFilter(servletRequest, servletResponse, filterChain); + } + } + + @Override + public void destroy() { } +}
http://git-wip-us.apache.org/repos/asf/knox/blob/58780d37/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jIdentityAdapter.java ---------------------------------------------------------------------- diff --cc gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jIdentityAdapter.java index 90395f1,0000000..6387a0b mode 100644,000000..100644 --- a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jIdentityAdapter.java +++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jIdentityAdapter.java @@@ -1,142 -1,0 +1,146 @@@ +/** + * 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.filter; + +import org.apache.knox.gateway.audit.api.Action; +import org.apache.knox.gateway.audit.api.ActionOutcome; +import org.apache.knox.gateway.audit.api.AuditService; +import org.apache.knox.gateway.audit.api.AuditServiceFactory; +import org.apache.knox.gateway.audit.api.Auditor; +import org.apache.knox.gateway.audit.api.ResourceType; +import org.apache.knox.gateway.audit.log4j.audit.AuditConstants; +import org.apache.knox.gateway.filter.AbstractGatewayFilter; +import org.apache.knox.gateway.security.PrimaryPrincipal; +import org.pac4j.core.config.ConfigSingleton; +import org.pac4j.core.context.J2EContext; ++import org.pac4j.core.profile.CommonProfile; +import org.pac4j.core.profile.ProfileManager; - import org.pac4j.core.profile.UserProfile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.Subject; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; ++import java.util.Optional; + +/** + * <p>This filter retrieves the authenticated user saved by the pac4j provider and injects it into the J2E HTTP request.</p> + * + * @since 0.8.0 + */ +public class Pac4jIdentityAdapter implements Filter { + + private static final Logger logger = LoggerFactory.getLogger(Pac4jIdentityAdapter.class); + + private static AuditService auditService = AuditServiceFactory.getAuditService(); + private static Auditor auditor = auditService.getAuditor( + AuditConstants.DEFAULT_AUDITOR_NAME, AuditConstants.KNOX_SERVICE_NAME, + AuditConstants.KNOX_COMPONENT_NAME ); + + private String testIdentifier; + + @Override + public void init( FilterConfig filterConfig ) throws ServletException { + } + + public void destroy() { + } + + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) + throws IOException, ServletException { + + final HttpServletRequest request = (HttpServletRequest) servletRequest; + final HttpServletResponse response = (HttpServletResponse) servletResponse; + final J2EContext context = new J2EContext(request, response, ConfigSingleton.getConfig().getSessionStore()); - final ProfileManager manager = new ProfileManager(context); - final UserProfile profile = manager.get(true); - logger.debug("User authenticated as: {}", profile); - manager.remove(true); - final String id = profile.getId(); - testIdentifier = id; - PrimaryPrincipal pp = new PrimaryPrincipal(id); - Subject subject = new Subject(); - subject.getPrincipals().add(pp); - auditService.getContext().setUsername(id); - String sourceUri = (String)request.getAttribute( AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME ); - auditor.audit(Action.AUTHENTICATION, sourceUri, ResourceType.URI, ActionOutcome.SUCCESS); - - doAs(request, response, chain, subject); ++ final ProfileManager<CommonProfile> manager = new ProfileManager<CommonProfile>(context); ++ final Optional<CommonProfile> optional = manager.get(true); ++ if (optional.isPresent()) { ++ CommonProfile profile = optional.get(); ++ logger.debug("User authenticated as: {}", profile); ++ manager.remove(true); ++ final String id = profile.getId(); ++ testIdentifier = id; ++ PrimaryPrincipal pp = new PrimaryPrincipal(id); ++ Subject subject = new Subject(); ++ subject.getPrincipals().add(pp); ++ auditService.getContext().setUsername(id); ++ String sourceUri = (String)request.getAttribute( AbstractGatewayFilter.SOURCE_REQUEST_CONTEXT_URL_ATTRIBUTE_NAME ); ++ auditor.audit(Action.AUTHENTICATION, sourceUri, ResourceType.URI, ActionOutcome.SUCCESS); ++ ++ doAs(request, response, chain, subject); ++ } + } - ++ + private void doAs(final ServletRequest request, + final ServletResponse response, final FilterChain chain, Subject subject) + throws IOException, ServletException { + try { + Subject.doAs( + subject, + new PrivilegedExceptionAction<Object>() { + public Object run() throws Exception { + chain.doFilter(request, response); + return null; + } + } + ); + } + catch (PrivilegedActionException e) { + Throwable t = e.getCause(); + if (t instanceof IOException) { + throw (IOException) t; + } + else if (t instanceof ServletException) { + throw (ServletException) t; + } + else { + throw new ServletException(t); + } + } + } + + /** + * For tests only. + */ + public static void setAuditService(AuditService auditService) { + Pac4jIdentityAdapter.auditService = auditService; + } + + /** + * For tests only. + */ + public static void setAuditor(Auditor auditor) { + Pac4jIdentityAdapter.auditor = auditor; + } + + /** + * For tests only. + */ + public String getTestIdentifier() { + return testIdentifier; + } +} http://git-wip-us.apache.org/repos/asf/knox/blob/58780d37/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/session/KnoxSessionStore.java ---------------------------------------------------------------------- diff --cc gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/session/KnoxSessionStore.java index 6ce002c,0000000..4ba55ea mode 100644,000000..100644 --- a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/session/KnoxSessionStore.java +++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/session/KnoxSessionStore.java @@@ -1,120 -1,0 +1,146 @@@ +/** + * 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.session; + +import org.apache.commons.codec.binary.Base64; +import org.apache.knox.gateway.services.security.CryptoService; +import org.apache.knox.gateway.services.security.EncryptionResult; +import org.apache.knox.gateway.util.Urls; +import org.pac4j.core.context.ContextHelper; +import org.pac4j.core.context.Cookie; +import org.pac4j.core.context.WebContext; +import org.pac4j.core.context.session.SessionStore; +import org.pac4j.core.exception.TechnicalException; +import org.pac4j.core.util.JavaSerializationHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; ++import java.util.Map; + +/** + * Specific session store where data are saved into cookies (and not in memory). + * Each data is encrypted and base64 encoded before being saved as a cookie (for security reasons). + * + * @since 0.8.0 + */ +public class KnoxSessionStore implements SessionStore { + + private static final Logger logger = LoggerFactory.getLogger(KnoxSessionStore.class); + + public static final String PAC4J_PASSWORD = "pac4j.password"; + + public static final String PAC4J_SESSION_PREFIX = "pac4j.session."; + + private final JavaSerializationHelper javaSerializationHelper; + + private final CryptoService cryptoService; + + private final String clusterName; + + private final String domainSuffix; + + public KnoxSessionStore(final CryptoService cryptoService, final String clusterName, final String domainSuffix) { + javaSerializationHelper = new JavaSerializationHelper(); + this.cryptoService = cryptoService; + this.clusterName = clusterName; + this.domainSuffix = domainSuffix; + } + + public String getOrCreateSessionId(WebContext context) { + return null; + } + + private Serializable decryptBase64(final String v) { + if (v != null && v.length() > 0) { + byte[] bytes = Base64.decodeBase64(v); + EncryptionResult result = EncryptionResult.fromByteArray(bytes); + byte[] clear = cryptoService.decryptForCluster(this.clusterName, + PAC4J_PASSWORD, + result.cipher, + result.iv, + result.salt); + if (clear != null) { + return javaSerializationHelper.unserializeFromBytes(clear); + } + } + return null; + } + + public Object get(WebContext context, String key) { + final Cookie cookie = ContextHelper.getCookie(context, PAC4J_SESSION_PREFIX + key); + Object value = null; + if (cookie != null) { + value = decryptBase64(cookie.getValue()); + } + logger.debug("Get from session: {} = {}", key, value); + return value; + } + + private String encryptBase64(final Object o) { - if (o == null || o.equals("")) { ++ if (o == null || o.equals("") ++ || (o instanceof Map<?,?> && ((Map<?,?>)o).isEmpty())) { + return null; + } else { + final byte[] bytes = javaSerializationHelper.serializeToBytes((Serializable) o); + EncryptionResult result = cryptoService.encryptForCluster(this.clusterName, PAC4J_PASSWORD, bytes); + return Base64.encodeBase64String(result.toByteAray()); + } + } + + public void set(WebContext context, String key, Object value) { + logger.debug("Save in session: {} = {}", key, value); + final Cookie cookie = new Cookie(PAC4J_SESSION_PREFIX + key, encryptBase64(value)); + try { + String domain = Urls.getDomainName(context.getFullRequestURL(), this.domainSuffix); + if (domain == null) { + domain = context.getServerName(); + } + cookie.setDomain(domain); + } catch (final Exception e) { + throw new TechnicalException(e); + } + cookie.setHttpOnly(true); + cookie.setSecure(ContextHelper.isHttpsOrSecure(context)); + context.addResponseCookie(cookie); + } ++ ++ @Override ++ public SessionStore buildFromTrackableSession(WebContext arg0, Object arg1) { ++ // TODO Auto-generated method stub ++ return null; ++ } ++ ++ @Override ++ public boolean destroySession(WebContext arg0) { ++ // TODO Auto-generated method stub ++ return false; ++ } ++ ++ @Override ++ public Object getTrackableSession(WebContext arg0) { ++ // TODO Auto-generated method stub ++ return null; ++ } ++ ++ @Override ++ public boolean renewSession(WebContext arg0) { ++ // TODO Auto-generated method stub ++ return false; ++ } +} http://git-wip-us.apache.org/repos/asf/knox/blob/58780d37/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/Pac4jProviderTest.java ---------------------------------------------------------------------- diff --cc gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/Pac4jProviderTest.java index 606d042,0000000..e4e0462 mode 100644,000000..100644 --- a/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/Pac4jProviderTest.java +++ b/gateway-provider-security-pac4j/src/test/java/org/apache/knox/gateway/pac4j/Pac4jProviderTest.java @@@ -1,150 -1,0 +1,150 @@@ +/** + * 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; + +import org.apache.knox.gateway.audit.api.AuditContext; +import org.apache.knox.gateway.audit.api.AuditService; +import org.apache.knox.gateway.audit.api.Auditor; +import org.apache.knox.gateway.pac4j.filter.Pac4jDispatcherFilter; +import org.apache.knox.gateway.pac4j.filter.Pac4jIdentityAdapter; +import org.apache.knox.gateway.pac4j.session.KnoxSessionStore; +import org.apache.knox.gateway.services.GatewayServices; +import org.apache.knox.gateway.services.security.AliasService; +import org.apache.knox.gateway.services.security.impl.DefaultCryptoService; +import org.junit.Test; +import org.pac4j.core.client.Clients; +import org.pac4j.core.context.Pac4jConstants; +import org.pac4j.http.client.indirect.IndirectBasicAuthClient; + +import javax.servlet.*; +import javax.servlet.http.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +/** + * This class simulates a full authentication process using pac4j. + */ +public class Pac4jProviderTest { + + private static final String LOCALHOST = "127.0.0.1"; + private static final String HADOOP_SERVICE_URL = "https://" + LOCALHOST + ":8443/gateway/sandox/webhdfs/v1/tmp?op=LISTSTATUS"; + private static final String KNOXSSO_SERVICE_URL = "https://" + LOCALHOST + ":8443/gateway/idp/api/v1/websso"; + private static final String PAC4J_CALLBACK_URL = KNOXSSO_SERVICE_URL; + private static final String ORIGINAL_URL = "originalUrl"; + private static final String CLUSTER_NAME = "knox"; + private static final String PAC4J_PASSWORD = "pwdfortest"; + private static final String CLIENT_CLASS = IndirectBasicAuthClient.class.getSimpleName(); + private static final String USERNAME = "jleleu"; + + @Test + public void test() throws Exception { + final AliasService aliasService = mock(AliasService.class); + when(aliasService.getPasswordFromAliasForCluster(CLUSTER_NAME, KnoxSessionStore.PAC4J_PASSWORD, true)).thenReturn(PAC4J_PASSWORD.toCharArray()); + when(aliasService.getPasswordFromAliasForCluster(CLUSTER_NAME, KnoxSessionStore.PAC4J_PASSWORD)).thenReturn(PAC4J_PASSWORD.toCharArray()); + + final DefaultCryptoService cryptoService = new DefaultCryptoService(); + cryptoService.setAliasService(aliasService); + + final GatewayServices services = mock(GatewayServices.class); + when(services.getService(GatewayServices.CRYPTO_SERVICE)).thenReturn(cryptoService); + when(services.getService(GatewayServices.ALIAS_SERVICE)).thenReturn(aliasService); + + final ServletContext context = mock(ServletContext.class); + when(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).thenReturn(services); + when(context.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE)).thenReturn(CLUSTER_NAME); + + final FilterConfig config = mock(FilterConfig.class); + when(config.getServletContext()).thenReturn(context); + when(config.getInitParameter(Pac4jDispatcherFilter.PAC4J_CALLBACK_URL)).thenReturn(PAC4J_CALLBACK_URL); - when(config.getInitParameter(Pac4jConstants.CLIENT_NAME)).thenReturn(Pac4jDispatcherFilter.TEST_BASIC_AUTH); ++ when(config.getInitParameter("clientName")).thenReturn(Pac4jDispatcherFilter.TEST_BASIC_AUTH); + + final Pac4jDispatcherFilter dispatcher = new Pac4jDispatcherFilter(); + dispatcher.init(config); + final Pac4jIdentityAdapter adapter = new Pac4jIdentityAdapter(); + adapter.init(config); - adapter.setAuditor(mock(Auditor.class)); ++ Pac4jIdentityAdapter.setAuditor(mock(Auditor.class)); + final AuditService auditService = mock(AuditService.class); + when(auditService.getContext()).thenReturn(mock(AuditContext.class)); - adapter.setAuditService(auditService); ++ Pac4jIdentityAdapter.setAuditService(auditService); + + // step 1: call the KnoxSSO service with an original url pointing to an Hadoop service (redirected by the SSOCookieProvider) + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURL(KNOXSSO_SERVICE_URL + "?" + ORIGINAL_URL + "=" + HADOOP_SERVICE_URL); + request.setCookies(new Cookie[0]); + request.setServerName(LOCALHOST); + MockHttpServletResponse response = new MockHttpServletResponse(); + FilterChain filterChain = mock(FilterChain.class); + dispatcher.doFilter(request, response, filterChain); + // it should be a redirection to the idp topology + assertEquals(302, response.getStatus()); + assertEquals(PAC4J_CALLBACK_URL + "?" + Pac4jDispatcherFilter.PAC4J_CALLBACK_PARAMETER + "=true&" + Clients.DEFAULT_CLIENT_NAME_PARAMETER + "=" + CLIENT_CLASS, response.getHeaders().get("Location")); + // we should have one cookie for the saved requested url + List<Cookie> cookies = response.getCookies(); + assertEquals(1, cookies.size()); + final Cookie requestedUrlCookie = cookies.get(0); + assertEquals(KnoxSessionStore.PAC4J_SESSION_PREFIX + Pac4jConstants.REQUESTED_URL, requestedUrlCookie.getName()); + + // step 2: send credentials to the callback url (callback from the identity provider) + request = new MockHttpServletRequest(); + request.setCookies(new Cookie[]{requestedUrlCookie}); + request.setRequestURL(PAC4J_CALLBACK_URL + "?" + Pac4jDispatcherFilter.PAC4J_CALLBACK_PARAMETER + "=true&" + Clients.DEFAULT_CLIENT_NAME_PARAMETER + "=" + Clients.DEFAULT_CLIENT_NAME_PARAMETER + "=" + CLIENT_CLASS); + request.addParameter(Pac4jDispatcherFilter.PAC4J_CALLBACK_PARAMETER, "true"); + request.addParameter(Clients.DEFAULT_CLIENT_NAME_PARAMETER, CLIENT_CLASS); + request.addHeader("Authorization", "Basic amxlbGV1OmpsZWxldQ=="); + request.setServerName(LOCALHOST); + response = new MockHttpServletResponse(); + filterChain = mock(FilterChain.class); + dispatcher.doFilter(request, response, filterChain); + // it should be a redirection to the original url + assertEquals(302, response.getStatus()); + assertEquals(KNOXSSO_SERVICE_URL + "?" + ORIGINAL_URL + "=" + HADOOP_SERVICE_URL, response.getHeaders().get("Location")); + // we should have 3 cookies among with the user profile + cookies = response.getCookies(); + Map<String, String> mapCookies = new HashMap<>(); + assertEquals(3, cookies.size()); + for (final Cookie cookie : cookies) { + mapCookies.put(cookie.getName(), cookie.getValue()); + } + assertNull(mapCookies.get(KnoxSessionStore.PAC4J_SESSION_PREFIX + CLIENT_CLASS + "$attemptedAuthentication")); - assertNotNull(mapCookies.get(KnoxSessionStore.PAC4J_SESSION_PREFIX + Pac4jConstants.USER_PROFILE)); ++ assertNotNull(mapCookies.get(KnoxSessionStore.PAC4J_SESSION_PREFIX + Pac4jConstants.USER_PROFILES)); + assertNull(mapCookies.get(KnoxSessionStore.PAC4J_SESSION_PREFIX + Pac4jConstants.REQUESTED_URL)); + + // step 3: turn pac4j identity into KnoxSSO identity + request = new MockHttpServletRequest(); + request.setCookies(cookies.toArray(new Cookie[cookies.size()])); + request.setRequestURL(KNOXSSO_SERVICE_URL + "?" + ORIGINAL_URL + "=" + HADOOP_SERVICE_URL); + request.setServerName(LOCALHOST); + response = new MockHttpServletResponse(); + filterChain = mock(FilterChain.class); + dispatcher.doFilter(request, response, filterChain); + assertEquals(0, response.getStatus()); + adapter.doFilter(request, response, filterChain); + cookies = response.getCookies(); + assertEquals(1, cookies.size()); + final Cookie userProfileCookie = cookies.get(0); + // the user profile has been cleaned - assertEquals(KnoxSessionStore.PAC4J_SESSION_PREFIX + Pac4jConstants.USER_PROFILE, userProfileCookie.getName()); ++ assertEquals(KnoxSessionStore.PAC4J_SESSION_PREFIX + Pac4jConstants.USER_PROFILES, userProfileCookie.getName()); + assertNull(userProfileCookie.getValue()); + assertEquals(USERNAME, adapter.getTestIdentifier()); + } +} http://git-wip-us.apache.org/repos/asf/knox/blob/58780d37/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java ---------------------------------------------------------------------- diff --cc gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java index 7f52b51,0000000..5fc3148 mode 100644,000000..100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenAuthorityService.java @@@ -1,226 -1,0 +1,240 @@@ +/** + * 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.token.impl; + +import java.security.KeyStoreException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Map; ++import java.util.Set; +import java.util.List; +import java.util.ArrayList; ++import java.util.HashSet; + +import javax.security.auth.Subject; + +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.services.Service; +import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.security.AliasService; +import org.apache.knox.gateway.services.security.AliasServiceException; +import org.apache.knox.gateway.services.security.KeystoreService; +import org.apache.knox.gateway.services.security.KeystoreServiceException; +import org.apache.knox.gateway.services.security.token.JWTokenAuthority; +import org.apache.knox.gateway.services.security.token.TokenServiceException; +import org.apache.knox.gateway.services.security.token.impl.JWT; +import org.apache.knox.gateway.services.security.token.impl.JWTToken; + +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.crypto.RSASSAVerifier; + +public class DefaultTokenAuthorityService implements JWTokenAuthority, Service { + + private static final String SIGNING_KEY_PASSPHRASE = "signing.key.passphrase"; ++ private static final Set<String> SUPPORTED_SIG_ALGS = new HashSet<>(); + private AliasService as = null; + private KeystoreService ks = null; + String signingKeyAlias = null; + ++ static { ++ // Only standard RSA signature algorithms are accepted ++ // https://tools.ietf.org/html/rfc7518 ++ SUPPORTED_SIG_ALGS.add("RS256"); ++ SUPPORTED_SIG_ALGS.add("RS384"); ++ SUPPORTED_SIG_ALGS.add("RS512"); ++ SUPPORTED_SIG_ALGS.add("PS256"); ++ SUPPORTED_SIG_ALGS.add("PS384"); ++ SUPPORTED_SIG_ALGS.add("PS512"); ++ } ++ + public void setKeystoreService(KeystoreService ks) { + this.ks = ks; + } + + public void setAliasService(AliasService as) { + this.as = as; + } + + /* (non-Javadoc) + * @see org.apache.knox.gateway.provider.federation.jwt.JWTokenAuthority#issueToken(javax.security.auth.Subject, java.lang.String) + */ + @Override + public JWT issueToken(Subject subject, String algorithm) throws TokenServiceException { + Principal p = (Principal) subject.getPrincipals().toArray()[0]; + return issueToken(p, algorithm); + } + + /* (non-Javadoc) + * @see org.apache.knox.gateway.provider.federation.jwt.JWTokenAuthority#issueToken(java.security.Principal, java.lang.String) + */ + @Override + public JWT issueToken(Principal p, String algorithm) throws TokenServiceException { + return issueToken(p, null, algorithm); + } + + /* (non-Javadoc) + * @see org.apache.knox.gateway.provider.federation.jwt.JWTokenAuthority#issueToken(java.security.Principal, java.lang.String, long expires) + */ + @Override + public JWT issueToken(Principal p, String algorithm, long expires) throws TokenServiceException { + return issueToken(p, (String)null, algorithm, expires); + } + + public JWT issueToken(Principal p, String audience, String algorithm) + throws TokenServiceException { + return issueToken(p, audience, algorithm, -1); + } + + /* (non-Javadoc) + * @see org.apache.knox.gateway.provider.federation.jwt.JWTokenAuthority#issueToken(java.security.Principal, java.lang.String, java.lang.String) + */ + @Override + public JWT issueToken(Principal p, String audience, String algorithm, long expires) + throws TokenServiceException { - ArrayList<String> audiences = null; ++ List<String> audiences = null; + if (audience != null) { + audiences = new ArrayList<String>(); + audiences.add(audience); + } + return issueToken(p, audiences, algorithm, expires); + } + + @Override + public JWT issueToken(Principal p, List<String> audiences, String algorithm, long expires) + throws TokenServiceException { + String[] claimArray = new String[4]; + claimArray[0] = "KNOXSSO"; + claimArray[1] = p.getName(); + claimArray[2] = null; + if (expires == -1) { + claimArray[3] = null; + } + else { + claimArray[3] = String.valueOf(expires); + } + - JWTToken token = null; - if ("RS256".equals(algorithm)) { - token = new JWTToken("RS256", claimArray, audiences); ++ JWT token = null; ++ if (SUPPORTED_SIG_ALGS.contains(algorithm)) { ++ token = new JWTToken(algorithm, claimArray, audiences); + RSAPrivateKey key; + char[] passphrase = null; + try { + passphrase = getSigningKeyPassphrase(); + } catch (AliasServiceException e) { + throw new TokenServiceException(e); + } + try { + key = (RSAPrivateKey) ks.getSigningKey(getSigningKeyAlias(), + passphrase); + JWSSigner signer = new RSASSASigner(key); + token.sign(signer); + } catch (KeystoreServiceException e) { + throw new TokenServiceException(e); + } + } + else { + throw new TokenServiceException("Cannot issue token - Unsupported algorithm"); + } + + return token; + } + + private char[] getSigningKeyPassphrase() throws AliasServiceException { + char[] phrase = as.getPasswordFromAliasForGateway(SIGNING_KEY_PASSPHRASE); + if (phrase == null) { + phrase = as.getGatewayIdentityPassphrase(); + } + return phrase; + } + + private String getSigningKeyAlias() { + if (signingKeyAlias == null) { + return "gateway-identity"; + } + return signingKeyAlias; + } + + @Override + public boolean verifyToken(JWT token) + throws TokenServiceException { + return verifyToken(token, null); + } + + @Override + public boolean verifyToken(JWT token, RSAPublicKey publicKey) + throws TokenServiceException { + boolean rc = false; + PublicKey key; + try { + if (publicKey == null) { + key = ks.getSigningKeystore().getCertificate(getSigningKeyAlias()).getPublicKey(); + } + else { + key = publicKey; + } + JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) key); + // TODO: interrogate the token for issuer claim in order to determine the public key to use for verification + // consider jwk for specifying the key too + rc = token.verify(verifier); + } catch (KeyStoreException e) { + throw new TokenServiceException("Cannot verify token.", e); + } catch (KeystoreServiceException e) { + throw new TokenServiceException("Cannot verify token.", e); + } + return rc; + } + + @Override + public void init(GatewayConfig config, Map<String, String> options) + throws ServiceLifecycleException { + if (as == null || ks == null) { + throw new ServiceLifecycleException("Alias or Keystore service is not set"); + } + signingKeyAlias = config.getSigningKeyAlias(); + + @SuppressWarnings("unused") + RSAPrivateKey key; + char[] passphrase = null; + try { + passphrase = as.getPasswordFromAliasForGateway(SIGNING_KEY_PASSPHRASE); + if (passphrase != null) { + key = (RSAPrivateKey) ks.getSigningKey(getSigningKeyAlias(), + passphrase); + if (key == null) { + throw new ServiceLifecycleException("Provisioned passphrase cannot be used to acquire signing key."); + } + } + } catch (AliasServiceException e) { + throw new ServiceLifecycleException("Provisioned signing key passphrase cannot be acquired.", e); + } catch (KeystoreServiceException e) { + throw new ServiceLifecycleException("Provisioned signing key passphrase cannot be acquired.", e); + } + } + + @Override + public void start() throws ServiceLifecycleException { + } + + @Override + public void stop() throws ServiceLifecycleException { + } +} http://git-wip-us.apache.org/repos/asf/knox/blob/58780d37/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java ---------------------------------------------------------------------- diff --cc gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java index 9f6f762,0000000..455b0fa mode 100644,000000..100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/topology/impl/DefaultTopologyService.java @@@ -1,673 -1,0 +1,689 @@@ +/** + * 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.topology.impl; + + +import org.apache.commons.digester3.Digester; +import org.apache.commons.digester3.binder.DigesterLoader; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.monitor.FileAlterationListener; +import org.apache.commons.io.monitor.FileAlterationListenerAdaptor; +import org.apache.commons.io.monitor.FileAlterationMonitor; +import org.apache.commons.io.monitor.FileAlterationObserver; +import org.apache.knox.gateway.GatewayMessages; +import org.apache.knox.gateway.audit.api.Action; +import org.apache.knox.gateway.audit.api.ActionOutcome; +import org.apache.knox.gateway.audit.api.AuditServiceFactory; +import org.apache.knox.gateway.audit.api.Auditor; +import org.apache.knox.gateway.audit.api.ResourceType; +import org.apache.knox.gateway.audit.log4j.audit.AuditConstants; +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import org.apache.knox.gateway.service.definition.ServiceDefinition; +import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.topology.TopologyService; +import org.apache.knox.gateway.topology.Topology; +import org.apache.knox.gateway.topology.TopologyEvent; +import org.apache.knox.gateway.topology.TopologyListener; +import org.apache.knox.gateway.topology.TopologyMonitor; +import org.apache.knox.gateway.topology.TopologyProvider; +import org.apache.knox.gateway.topology.builder.TopologyBuilder; +import org.apache.knox.gateway.topology.validation.TopologyValidator; +import org.apache.knox.gateway.topology.xml.AmbariFormatXmlTopologyRules; +import org.apache.knox.gateway.topology.xml.KnoxFormatXmlTopologyRules; +import org.apache.knox.gateway.util.ServiceDefinitionsLoader; +import org.apache.knox.gateway.services.security.AliasService; +import org.apache.knox.gateway.topology.simple.SimpleDescriptorHandler; +import org.eclipse.persistence.jaxb.JAXBContextProperties; +import org.xml.sax.SAXException; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.apache.commons.digester3.binder.DigesterLoader.newLoader; + + +public class DefaultTopologyService + extends FileAlterationListenerAdaptor + implements TopologyService, TopologyMonitor, TopologyProvider, FileFilter, FileAlterationListener { + + private static Auditor auditor = AuditServiceFactory.getAuditService().getAuditor( + AuditConstants.DEFAULT_AUDITOR_NAME, AuditConstants.KNOX_SERVICE_NAME, + AuditConstants.KNOX_COMPONENT_NAME); + + private static final List<String> SUPPORTED_TOPOLOGY_FILE_EXTENSIONS = new ArrayList<String>(); + static { + SUPPORTED_TOPOLOGY_FILE_EXTENSIONS.add("xml"); + SUPPORTED_TOPOLOGY_FILE_EXTENSIONS.add("conf"); + } + + private static GatewayMessages log = MessagesFactory.get(GatewayMessages.class); + private static DigesterLoader digesterLoader = newLoader(new KnoxFormatXmlTopologyRules(), new AmbariFormatXmlTopologyRules()); + private List<FileAlterationMonitor> monitors = new ArrayList<>(); + private File topologiesDirectory; + private File descriptorsDirectory; + + private Set<TopologyListener> listeners; + private volatile Map<File, Topology> topologies; + private AliasService aliasService; + + + private Topology loadTopology(File file) throws IOException, SAXException, URISyntaxException, InterruptedException { + final long TIMEOUT = 250; //ms + final long DELAY = 50; //ms + log.loadingTopologyFile(file.getAbsolutePath()); + Topology topology; + long start = System.currentTimeMillis(); + while (true) { + try { + topology = loadTopologyAttempt(file); + break; + } catch (IOException e) { + if (System.currentTimeMillis() - start < TIMEOUT) { + log.failedToLoadTopologyRetrying(file.getAbsolutePath(), Long.toString(DELAY), e); + Thread.sleep(DELAY); + } else { + throw e; + } + } catch (SAXException e) { + if (System.currentTimeMillis() - start < TIMEOUT) { + log.failedToLoadTopologyRetrying(file.getAbsolutePath(), Long.toString(DELAY), e); + Thread.sleep(DELAY); + } else { + throw e; + } + } + } + return topology; + } + + private Topology loadTopologyAttempt(File file) throws IOException, SAXException, URISyntaxException { + Topology topology; + Digester digester = digesterLoader.newDigester(); + TopologyBuilder topologyBuilder = digester.parse(FileUtils.openInputStream(file)); + if (null == topologyBuilder) { + return null; + } + topology = topologyBuilder.build(); + topology.setUri(file.toURI()); + topology.setName(FilenameUtils.removeExtension(file.getName())); + topology.setTimestamp(file.lastModified()); + return topology; + } + + private void redeployTopology(Topology topology) { + File topologyFile = new File(topology.getUri()); + try { + TopologyValidator tv = new TopologyValidator(topology); + + if(tv.validateTopology()) { + throw new SAXException(tv.getErrorString()); + } + + long start = System.currentTimeMillis(); + long limit = 1000L; // One second. + long elapsed = 1; + while (elapsed <= limit) { + try { + long origTimestamp = topologyFile.lastModified(); + long setTimestamp = Math.max(System.currentTimeMillis(), topologyFile.lastModified() + elapsed); + if(topologyFile.setLastModified(setTimestamp)) { + long newTimstamp = topologyFile.lastModified(); + if(newTimstamp > origTimestamp) { + break; + } else { + Thread.sleep(10); + elapsed = System.currentTimeMillis() - start; + continue; + } + } else { + auditor.audit(Action.REDEPLOY, topology.getName(), ResourceType.TOPOLOGY, + ActionOutcome.FAILURE); + log.failedToRedeployTopology(topology.getName()); + break; + } + } catch (InterruptedException e) { + auditor.audit(Action.REDEPLOY, topology.getName(), ResourceType.TOPOLOGY, + ActionOutcome.FAILURE); + log.failedToRedeployTopology(topology.getName(), e); + e.printStackTrace(); + } + } + } catch (SAXException e) { + auditor.audit(Action.REDEPLOY, topology.getName(), ResourceType.TOPOLOGY, ActionOutcome.FAILURE); + log.failedToRedeployTopology(topology.getName(), e); + } + } + + private List<TopologyEvent> createChangeEvents( + Map<File, Topology> oldTopologies, + Map<File, Topology> newTopologies) { + ArrayList<TopologyEvent> events = new ArrayList<TopologyEvent>(); + // Go through the old topologies and find anything that was deleted. + for (File file : oldTopologies.keySet()) { + if (!newTopologies.containsKey(file)) { + events.add(new TopologyEvent(TopologyEvent.Type.DELETED, oldTopologies.get(file))); + } + } + // Go through the new topologies and figure out what was updated vs added. + for (File file : newTopologies.keySet()) { + if (oldTopologies.containsKey(file)) { + Topology oldTopology = oldTopologies.get(file); + Topology newTopology = newTopologies.get(file); + if (newTopology.getTimestamp() > oldTopology.getTimestamp()) { + events.add(new TopologyEvent(TopologyEvent.Type.UPDATED, newTopologies.get(file))); + } + } else { + events.add(new TopologyEvent(TopologyEvent.Type.CREATED, newTopologies.get(file))); + } + } + return events; + } + + private File calculateAbsoluteTopologiesDir(GatewayConfig config) { + String normalizedTopologyDir = FilenameUtils.normalize(config.getGatewayTopologyDir()); + File topoDir = new File(normalizedTopologyDir); + topoDir = topoDir.getAbsoluteFile(); + return topoDir; + } + + private File calculateAbsoluteConfigDir(GatewayConfig config) { + File configDir = null; + + String path = FilenameUtils.normalize(config.getGatewayConfDir()); + if (path != null) { + configDir = new File(config.getGatewayConfDir()); + } else { + configDir = (new File(config.getGatewayTopologyDir())).getParentFile(); + } + configDir = configDir.getAbsoluteFile(); + + return configDir; + } + + private void initListener(FileAlterationMonitor monitor, + File directory, + FileFilter filter, + FileAlterationListener listener) { + monitors.add(monitor); + FileAlterationObserver observer = new FileAlterationObserver(directory, filter); + observer.addListener(listener); + monitor.addObserver(observer); + } + + private void initListener(File directory, FileFilter filter, FileAlterationListener listener) throws IOException, SAXException { + // Increasing the monitoring interval to 5 seconds as profiling has shown + // this is rather expensive in terms of generated garbage objects. + initListener(new FileAlterationMonitor(5000L), directory, filter, listener); + } + + private Map<File, Topology> loadTopologies(File directory) { + Map<File, Topology> map = new HashMap<>(); + if (directory.isDirectory() && directory.canRead()) { + for (File file : directory.listFiles(this)) { + try { + Topology loadTopology = loadTopology(file); + if (null != loadTopology) { + map.put(file, loadTopology); + } else { + auditor.audit(Action.LOAD, file.getAbsolutePath(), ResourceType.TOPOLOGY, + ActionOutcome.FAILURE); + log.failedToLoadTopology(file.getAbsolutePath()); + } + } catch (IOException e) { + // Maybe it makes sense to throw exception + auditor.audit(Action.LOAD, file.getAbsolutePath(), ResourceType.TOPOLOGY, + ActionOutcome.FAILURE); + log.failedToLoadTopology(file.getAbsolutePath(), e); + } catch (SAXException e) { + // Maybe it makes sense to throw exception + auditor.audit(Action.LOAD, file.getAbsolutePath(), ResourceType.TOPOLOGY, + ActionOutcome.FAILURE); + log.failedToLoadTopology(file.getAbsolutePath(), e); + } catch (Exception e) { + // Maybe it makes sense to throw exception + auditor.audit(Action.LOAD, file.getAbsolutePath(), ResourceType.TOPOLOGY, + ActionOutcome.FAILURE); + log.failedToLoadTopology(file.getAbsolutePath(), e); + } + } + } + return map; + } + + public void setAliasService(AliasService as) { + this.aliasService = as; + } + + public void deployTopology(Topology t){ + + try { + File temp = new File(topologiesDirectory.getAbsolutePath() + "/" + t.getName() + ".xml.temp"); + Package topologyPkg = Topology.class.getPackage(); + String pkgName = topologyPkg.getName(); + String bindingFile = pkgName.replace(".", "/") + "/topology_binding-xml.xml"; + + Map<String, Object> properties = new HashMap<>(1); + properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, bindingFile); + JAXBContext jc = JAXBContext.newInstance(pkgName, Topology.class.getClassLoader(), properties); + Marshaller mr = jc.createMarshaller(); + + mr.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + mr.marshal(t, temp); + + File topology = new File(topologiesDirectory.getAbsolutePath() + "/" + t.getName() + ".xml"); + if(!temp.renameTo(topology)) { + FileUtils.forceDelete(temp); + throw new IOException("Could not rename temp file"); + } + + // This code will check if the topology is valid, and retrieve the errors if it is not. + TopologyValidator validator = new TopologyValidator( topology.getAbsolutePath() ); + if( !validator.validateTopology() ){ + throw new SAXException( validator.getErrorString() ); + } + + + } catch (JAXBException e) { + auditor.audit(Action.DEPLOY, t.getName(), ResourceType.TOPOLOGY, ActionOutcome.FAILURE); + log.failedToDeployTopology(t.getName(), e); + } catch (IOException io) { + auditor.audit(Action.DEPLOY, t.getName(), ResourceType.TOPOLOGY, ActionOutcome.FAILURE); + log.failedToDeployTopology(t.getName(), io); + } catch (SAXException sx){ + auditor.audit(Action.DEPLOY, t.getName(), ResourceType.TOPOLOGY, ActionOutcome.FAILURE); + log.failedToDeployTopology(t.getName(), sx); + } + reloadTopologies(); + } + + public void redeployTopologies(String topologyName) { + + for (Topology topology : getTopologies()) { + if (topologyName == null || topologyName.equals(topology.getName())) { + redeployTopology(topology); + } + } + + } + + public void reloadTopologies() { + try { + synchronized (this) { + Map<File, Topology> oldTopologies = topologies; + Map<File, Topology> newTopologies = loadTopologies(topologiesDirectory); + List<TopologyEvent> events = createChangeEvents(oldTopologies, newTopologies); + topologies = newTopologies; + notifyChangeListeners(events); + } + } catch (Exception e) { + // Maybe it makes sense to throw exception + log.failedToReloadTopologies(e); + } + } + + public void deleteTopology(Topology t) { + File topoDir = topologiesDirectory; + + if(topoDir.isDirectory() && topoDir.canRead()) { + File[] results = topoDir.listFiles(); + for (File f : results) { + String fName = FilenameUtils.getBaseName(f.getName()); + if(fName.equals(t.getName())) { + f.delete(); + } + } + } + reloadTopologies(); + } + + private void notifyChangeListeners(List<TopologyEvent> events) { + for (TopologyListener listener : listeners) { + try { + listener.handleTopologyEvent(events); + } catch (RuntimeException e) { + auditor.audit(Action.LOAD, "Topology_Event", ResourceType.TOPOLOGY, ActionOutcome.FAILURE); + log.failedToHandleTopologyEvents(e); + } + } + } + + public Map<String, List<String>> getServiceTestURLs(Topology t, GatewayConfig config) { + File tFile = null; + Map<String, List<String>> urls = new HashMap<>(); + if(topologiesDirectory.isDirectory() && topologiesDirectory.canRead()) { + for(File f : topologiesDirectory.listFiles()){ + if(FilenameUtils.removeExtension(f.getName()).equals(t.getName())){ + tFile = f; + } + } + } + Set<ServiceDefinition> defs; + if(tFile != null) { + defs = ServiceDefinitionsLoader.getServiceDefinitions(new File(config.getGatewayServicesDir())); + + for(ServiceDefinition def : defs) { + urls.put(def.getRole(), def.getTestURLs()); + } + } + return urls; + } + + public Collection<Topology> getTopologies() { + Map<File, Topology> map = topologies; + return Collections.unmodifiableCollection(map.values()); + } + + @Override + public void addTopologyChangeListener(TopologyListener listener) { + listeners.add(listener); + } + + @Override + public void startMonitor() throws Exception { + for (FileAlterationMonitor monitor : monitors) { + monitor.start(); + } + } + + @Override + public void stopMonitor() throws Exception { + for (FileAlterationMonitor monitor : monitors) { + monitor.stop(); + } + } + + @Override + public boolean accept(File file) { + boolean accept = false; + if (!file.isDirectory() && file.canRead()) { + String extension = FilenameUtils.getExtension(file.getName()); + if (SUPPORTED_TOPOLOGY_FILE_EXTENSIONS.contains(extension)) { + accept = true; + } + } + return accept; + } + + @Override + public void onFileCreate(File file) { + onFileChange(file); + } + + @Override + public void onFileDelete(java.io.File file) { + // For full topology descriptors, we need to make sure to delete any corresponding simple descriptors to prevent + // unintended subsequent generation of the topology descriptor + for (String ext : DescriptorsMonitor.SUPPORTED_EXTENSIONS) { + File simpleDesc = + new File(descriptorsDirectory, FilenameUtils.getBaseName(file.getName()) + "." + ext); + if (simpleDesc.exists()) { + simpleDesc.delete(); + } + } + + onFileChange(file); + } + + @Override + public void onFileChange(File file) { + reloadTopologies(); + } + + @Override + public void stop() { + + } + + @Override + public void start() { + + } + + @Override + public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException { + + try { + listeners = new HashSet<>(); + topologies = new HashMap<>(); + + topologiesDirectory = calculateAbsoluteTopologiesDir(config); + + File configDirectory = calculateAbsoluteConfigDir(config); + descriptorsDirectory = new File(configDirectory, "descriptors"); + File sharedProvidersDirectory = new File(configDirectory, "shared-providers"); + + // Add support for conf/topologies + initListener(topologiesDirectory, this, this); + + // Add support for conf/descriptors + DescriptorsMonitor dm = new DescriptorsMonitor(topologiesDirectory, aliasService); + initListener(descriptorsDirectory, + dm, + dm); + + // Add support for conf/shared-providers + SharedProviderConfigMonitor spm = new SharedProviderConfigMonitor(dm, descriptorsDirectory); + initListener(sharedProvidersDirectory, spm, spm); + ++ // For all the descriptors currently in the descriptors dir at start-up time, trigger topology generation. ++ // This happens prior to the start-up loading of the topologies. ++ String[] descriptorFilenames = descriptorsDirectory.list(); ++ if (descriptorFilenames != null) { ++ for (String descriptorFilename : descriptorFilenames) { ++ if (DescriptorsMonitor.isDescriptorFile(descriptorFilename)) { ++ dm.onFileChange(new File(descriptorsDirectory, descriptorFilename)); ++ } ++ } ++ } ++ + } catch (IOException | SAXException io) { + throw new ServiceLifecycleException(io.getMessage()); + } + } + + + /** + * Change handler for simple descriptors + */ + public static class DescriptorsMonitor extends FileAlterationListenerAdaptor + implements FileFilter { + + static final List<String> SUPPORTED_EXTENSIONS = new ArrayList<String>(); + static { + SUPPORTED_EXTENSIONS.add("json"); + SUPPORTED_EXTENSIONS.add("yml"); ++ SUPPORTED_EXTENSIONS.add("yaml"); + } + + private File topologiesDir; + + private AliasService aliasService; + + private Map<String, List<String>> providerConfigReferences = new HashMap<>(); + + ++ static boolean isDescriptorFile(String filename) { ++ return SUPPORTED_EXTENSIONS.contains(FilenameUtils.getExtension(filename)); ++ } ++ + public DescriptorsMonitor(File topologiesDir, AliasService aliasService) { + this.topologiesDir = topologiesDir; + this.aliasService = aliasService; + } + + List<String> getReferencingDescriptors(String providerConfigPath) { + List<String> result = providerConfigReferences.get(providerConfigPath); + if (result == null) { + result = Collections.emptyList(); + } + return result; + } + + @Override + public void onFileCreate(File file) { + onFileChange(file); + } + + @Override + public void onFileDelete(File file) { + // For simple descriptors, we need to make sure to delete any corresponding full topology descriptors to trigger undeployment + for (String ext : DefaultTopologyService.SUPPORTED_TOPOLOGY_FILE_EXTENSIONS) { + File topologyFile = + new File(topologiesDir, FilenameUtils.getBaseName(file.getName()) + "." + ext); + if (topologyFile.exists()) { + topologyFile.delete(); + } + } + + String normalizedFilePath = FilenameUtils.normalize(file.getAbsolutePath()); + String reference = null; + for (Map.Entry<String, List<String>> entry : providerConfigReferences.entrySet()) { + if (entry.getValue().contains(normalizedFilePath)) { + reference = entry.getKey(); + break; + } + } + if (reference != null) { + providerConfigReferences.get(reference).remove(normalizedFilePath); + } + } + + @Override + public void onFileChange(File file) { + try { + // When a simple descriptor has been created or modified, generate the new topology descriptor + Map<String, File> result = SimpleDescriptorHandler.handle(file, topologiesDir, aliasService); + + // Add the provider config reference relationship for handling updates to the provider config + String providerConfig = FilenameUtils.normalize(result.get("reference").getAbsolutePath()); + if (!providerConfigReferences.containsKey(providerConfig)) { + providerConfigReferences.put(providerConfig, new ArrayList<String>()); + } + List<String> refs = providerConfigReferences.get(providerConfig); + String descriptorName = FilenameUtils.normalize(file.getAbsolutePath()); + if (!refs.contains(descriptorName)) { + // Need to check if descriptor had previously referenced another provider config, so it can be removed + for (List<String> descs : providerConfigReferences.values()) { + if (descs.contains(descriptorName)) { + descs.remove(descriptorName); + } + } + + // Add the current reference relationship + refs.add(descriptorName); + } + } catch (Exception e) { + log.simpleDescriptorHandlingError(file.getName(), e); + } + } + + @Override + public boolean accept(File file) { + boolean accept = false; + if (!file.isDirectory() && file.canRead()) { + String extension = FilenameUtils.getExtension(file.getName()); + if (SUPPORTED_EXTENSIONS.contains(extension)) { + accept = true; + } + } + return accept; + } + } + + /** + * Change handler for shared provider configurations + */ + public static class SharedProviderConfigMonitor extends FileAlterationListenerAdaptor + implements FileFilter { + + static final List<String> SUPPORTED_EXTENSIONS = new ArrayList<>(); + static { + SUPPORTED_EXTENSIONS.add("xml"); + } + + private DescriptorsMonitor descriptorsMonitor; + private File descriptorsDir; + + + SharedProviderConfigMonitor(DescriptorsMonitor descMonitor, File descriptorsDir) { + this.descriptorsMonitor = descMonitor; + this.descriptorsDir = descriptorsDir; + } + + @Override + public void onFileCreate(File file) { + onFileChange(file); + } + + @Override + public void onFileDelete(File file) { + onFileChange(file); + } + + @Override + public void onFileChange(File file) { + // For shared provider configuration, we need to update any simple descriptors that reference it + for (File descriptor : getReferencingDescriptors(file)) { + descriptor.setLastModified(System.currentTimeMillis()); + } + } + + private List<File> getReferencingDescriptors(File sharedProviderConfig) { + List<File> references = new ArrayList<>(); + + for (File descriptor : descriptorsDir.listFiles()) { + if (DescriptorsMonitor.SUPPORTED_EXTENSIONS.contains(FilenameUtils.getExtension(descriptor.getName()))) { + for (String reference : descriptorsMonitor.getReferencingDescriptors(FilenameUtils.normalize(sharedProviderConfig.getAbsolutePath()))) { + references.add(new File(reference)); + } + } + } + + return references; + } + + @Override + public boolean accept(File file) { + boolean accept = false; + if (!file.isDirectory() && file.canRead()) { + String extension = FilenameUtils.getExtension(file.getName()); + if (SUPPORTED_EXTENSIONS.contains(extension)) { + accept = true; + } + } + return accept; + } + } + +} http://git-wip-us.apache.org/repos/asf/knox/blob/58780d37/gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptor.java ---------------------------------------------------------------------- diff --cc gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptor.java index 85c0535,0000000..25997b1 mode 100644,000000..100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptor.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptor.java @@@ -1,46 -1,0 +1,48 @@@ +/** + * 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.topology.simple; + +import java.util.List; ++import java.util.Map; + +public interface SimpleDescriptor { + + String getName(); + + String getDiscoveryType(); + + String getDiscoveryAddress(); + + String getDiscoveryUser(); + + String getDiscoveryPasswordAlias(); + + String getClusterName(); + + String getProviderConfig(); + + List<Service> getServices(); + + + interface Service { + String getName(); + ++ Map<String, String> getParams(); ++ + List<String> getURLs(); + } - +} http://git-wip-us.apache.org/repos/asf/knox/blob/58780d37/gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java ---------------------------------------------------------------------- diff --cc gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java index 16d5b81,0000000..b54432d mode 100644,000000..100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorHandler.java @@@ -1,234 -1,0 +1,267 @@@ +/** + * 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.topology.simple; + +import org.apache.knox.gateway.i18n.messages.MessagesFactory; +import org.apache.knox.gateway.services.Service; +import org.apache.knox.gateway.topology.discovery.DefaultServiceDiscoveryConfig; +import org.apache.knox.gateway.topology.discovery.ServiceDiscovery; +import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryFactory; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.InputStreamReader; +import java.io.IOException; + +import java.net.URI; +import java.net.URISyntaxException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + + +/** + * Processes simple topology descriptors, producing full topology files, which can subsequently be deployed to the + * gateway. + */ +public class SimpleDescriptorHandler { + + private static final Service[] NO_GATEWAY_SERVICES = new Service[]{}; + + private static final SimpleDescriptorMessages log = MessagesFactory.get(SimpleDescriptorMessages.class); + + public static Map<String, File> handle(File desc) throws IOException { + return handle(desc, NO_GATEWAY_SERVICES); + } + + public static Map<String, File> handle(File desc, Service...gatewayServices) throws IOException { + return handle(desc, desc.getParentFile(), gatewayServices); + } + + public static Map<String, File> handle(File desc, File destDirectory) throws IOException { + return handle(desc, destDirectory, NO_GATEWAY_SERVICES); + } + + public static Map<String, File> handle(File desc, File destDirectory, Service...gatewayServices) throws IOException { + return handle(SimpleDescriptorFactory.parse(desc.getAbsolutePath()), desc.getParentFile(), destDirectory, gatewayServices); + } + + public static Map<String, File> handle(SimpleDescriptor desc, File srcDirectory, File destDirectory) { + return handle(desc, srcDirectory, destDirectory, NO_GATEWAY_SERVICES); + } + + public static Map<String, File> handle(SimpleDescriptor desc, File srcDirectory, File destDirectory, Service...gatewayServices) { + Map<String, File> result = new HashMap<>(); + + File topologyDescriptor; + + DefaultServiceDiscoveryConfig sdc = new DefaultServiceDiscoveryConfig(desc.getDiscoveryAddress()); + sdc.setUser(desc.getDiscoveryUser()); + sdc.setPasswordAlias(desc.getDiscoveryPasswordAlias()); + ServiceDiscovery sd = ServiceDiscoveryFactory.get(desc.getDiscoveryType(), gatewayServices); + ServiceDiscovery.Cluster cluster = sd.discover(sdc, desc.getClusterName()); + - Map<String, List<String>> serviceURLs = new HashMap<>(); ++ List<String> validServiceNames = new ArrayList<>(); ++ ++ Map<String, Map<String, String>> serviceParams = new HashMap<>(); ++ Map<String, List<String>> serviceURLs = new HashMap<>(); + + if (cluster != null) { + for (SimpleDescriptor.Service descService : desc.getServices()) { + String serviceName = descService.getName(); + + List<String> descServiceURLs = descService.getURLs(); + if (descServiceURLs == null || descServiceURLs.isEmpty()) { + descServiceURLs = cluster.getServiceURLs(serviceName); + } + + // Validate the discovered service URLs + List<String> validURLs = new ArrayList<>(); + if (descServiceURLs != null && !descServiceURLs.isEmpty()) { + // Validate the URL(s) + for (String descServiceURL : descServiceURLs) { + if (validateURL(serviceName, descServiceURL)) { + validURLs.add(descServiceURL); + } + } ++ ++ if (!validURLs.isEmpty()) { ++ validServiceNames.add(serviceName); ++ } + } + + // If there is at least one valid URL associated with the service, then add it to the map + if (!validURLs.isEmpty()) { + serviceURLs.put(serviceName, validURLs); + } else { + log.failedToDiscoverClusterServiceURLs(serviceName, cluster.getName()); + } ++ ++ // Service params ++ if (descService.getParams() != null) { ++ serviceParams.put(serviceName, descService.getParams()); ++ if (!validServiceNames.contains(serviceName)) { ++ validServiceNames.add(serviceName); ++ } ++ } + } + } else { + log.failedToDiscoverClusterServices(desc.getClusterName()); + } + + BufferedWriter fw = null; + topologyDescriptor = null; - File providerConfig = null; ++ File providerConfig; + try { + // Verify that the referenced provider configuration exists before attempting to reading it + providerConfig = resolveProviderConfigurationReference(desc.getProviderConfig(), srcDirectory); + if (providerConfig == null) { + log.failedToResolveProviderConfigRef(desc.getProviderConfig()); + throw new IllegalArgumentException("Unresolved provider configuration reference: " + + desc.getProviderConfig() + " ; Topology update aborted!"); + } + result.put("reference", providerConfig); + + // TODO: Should the contents of the provider config be validated before incorporating it into the topology? + + String topologyFilename = desc.getName(); + if (topologyFilename == null) { + topologyFilename = desc.getClusterName(); + } + topologyDescriptor = new File(destDirectory, topologyFilename + ".xml"); + fw = new BufferedWriter(new FileWriter(topologyDescriptor)); + + fw.write("<topology>\n"); + + // Copy the externalized provider configuration content into the topology descriptor in-line + InputStreamReader policyReader = new InputStreamReader(new FileInputStream(providerConfig)); + char[] buffer = new char[1024]; + int count; + while ((count = policyReader.read(buffer)) > 0) { + fw.write(buffer, 0, count); + } + policyReader.close(); + + // Sort the service names to write the services alphabetically - List<String> serviceNames = new ArrayList<>(serviceURLs.keySet()); ++ List<String> serviceNames = new ArrayList<>(validServiceNames); + Collections.sort(serviceNames); + + // Write the service declarations + for (String serviceName : serviceNames) { + fw.write(" <service>\n"); + fw.write(" <role>" + serviceName + "</role>\n"); - for (String url : serviceURLs.get(serviceName)) { - fw.write(" <url>" + url + "</url>\n"); ++ ++ // URLs ++ List<String> urls = serviceURLs.get(serviceName); ++ if (urls != null) { ++ for (String url : urls) { ++ fw.write(" <url>" + url + "</url>\n"); ++ } + } ++ ++ // Params ++ Map<String, String> svcParams = serviceParams.get(serviceName); ++ if (svcParams != null) { ++ for (String paramName : svcParams.keySet()) { ++ fw.write(" <param>\n"); ++ fw.write(" <name>" + paramName + "</name>\n"); ++ fw.write(" <value>" + svcParams.get(paramName) + "</value>\n"); ++ fw.write(" </param>\n"); ++ } ++ } ++ + fw.write(" </service>\n"); + } + + fw.write("</topology>\n"); + + fw.flush(); + } catch (IOException e) { + log.failedToGenerateTopologyFromSimpleDescriptor(topologyDescriptor.getName(), e); + topologyDescriptor.delete(); + } finally { + if (fw != null) { + try { + fw.close(); + } catch (IOException e) { + // ignore + } + } + } + + result.put("topology", topologyDescriptor); + return result; + } + + private static boolean validateURL(String serviceName, String url) { + boolean result = false; + + if (url != null && !url.isEmpty()) { + try { + new URI(url); + result = true; + } catch (URISyntaxException e) { + log.serviceURLValidationFailed(serviceName, url, e); + } + } + + return result; + } + ++ + private static File resolveProviderConfigurationReference(String reference, File srcDirectory) { + File providerConfig; + + // If the reference includes a path + if (reference.contains(File.separator)) { + // Check if it's an absolute path + providerConfig = new File(reference); + if (!providerConfig.exists()) { + // If it's not an absolute path, try treating it as a relative path + providerConfig = new File(srcDirectory, reference); + if (!providerConfig.exists()) { + providerConfig = null; + } + } + } else { // No file path, just a name + // Check if it's co-located with the referencing descriptor + providerConfig = new File(srcDirectory, reference); + if (!providerConfig.exists()) { + // Check the shared-providers config location + File sharedProvidersDir = new File(srcDirectory, "../shared-providers"); + if (sharedProvidersDir.exists()) { + providerConfig = new File(sharedProvidersDir, reference); + if (!providerConfig.exists()) { + // Check if it's a valid name without the extension + providerConfig = new File(sharedProvidersDir, reference + ".xml"); + if (!providerConfig.exists()) { + providerConfig = null; + } + } + } + } + } + + return providerConfig; + } + +} http://git-wip-us.apache.org/repos/asf/knox/blob/58780d37/gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorImpl.java ---------------------------------------------------------------------- diff --cc gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorImpl.java index 0ec7acf,0000000..4eb1954 mode 100644,000000..100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorImpl.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/topology/simple/SimpleDescriptorImpl.java @@@ -1,111 -1,0 +1,123 @@@ +/** + * 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.topology.simple; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.List; ++import java.util.Map; + +class SimpleDescriptorImpl implements SimpleDescriptor { + + @JsonProperty("discovery-type") + private String discoveryType; + + @JsonProperty("discovery-address") + private String discoveryAddress; + + @JsonProperty("discovery-user") + private String discoveryUser; + + @JsonProperty("discovery-pwd-alias") + private String discoveryPasswordAlias; + + @JsonProperty("provider-config-ref") + private String providerConfig; + + @JsonProperty("cluster") + private String cluster; + + @JsonProperty("services") + private List<ServiceImpl> services; + + private String name = null; + + void setName(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDiscoveryType() { + return discoveryType; + } + + @Override + public String getDiscoveryAddress() { + return discoveryAddress; + } + + @Override + public String getDiscoveryUser() { + return discoveryUser; + } + + @Override + public String getDiscoveryPasswordAlias() { + return discoveryPasswordAlias; + } + + @Override + public String getClusterName() { + return cluster; + } + + @Override + public String getProviderConfig() { + return providerConfig; + } + + @Override + public List<Service> getServices() { + List<Service> result = new ArrayList<>(); + result.addAll(services); + return result; + } + + public static class ServiceImpl implements Service { ++ @JsonProperty("name") + private String name; ++ ++ @JsonProperty("params") ++ private Map<String, String> params; ++ ++ @JsonProperty("urls") + private List<String> urls; + + @Override + public String getName() { + return name; + } + + @Override ++ public Map<String, String> getParams() { ++ return params; ++ } ++ ++ @Override + public List<String> getURLs() { + return urls; + } + } + +}