merrimanr closed pull request #1281: METRON-1895: Add Knox SSO as an option in Metron URL: https://github.com/apache/metron/pull/1281
This is a PR merged from a forked repository. As GitHub hides the original diff on merge, it is displayed below for the sake of provenance: As this is a foreign pull request (from a fork), the diff is supplied below (as it won't show otherwise due to GitHub magic): diff --git a/dependencies_with_url.csv b/dependencies_with_url.csv index bdc2508377..fdc88be9a0 100644 --- a/dependencies_with_url.csv +++ b/dependencies_with_url.csv @@ -489,3 +489,5 @@ org.hibernate.validator:hibernate-validator:jar:6.0.9.Final:compile,ASLv2,https: com.github.palindromicity:simple-syslog-5424:jar:0.0.9:compile,ASLv2,https://github.com/palindromicity/simple-syslog-5424 org.elasticsearch.client:elasticsearch-rest-high-level-client:jar:5.6.2:compile,ASLv2,https://github.com/elastic/elasticsearch/blob/master/LICENSE.txt org.elasticsearch.plugin:aggs-matrix-stats-client:jar:5.6.2:compile,ASLv2,https://github.com/elastic/elasticsearch/blob/master/LICENSE.txt +com.github.stephenc.jcip:jcip-annotations:jar:1.0-1:compile,ASLv2,http://stephenc.github.io/jcip-annotations/ +com.nimbusds:nimbus-jose-jwt:jar:4.41.2:compile,ASLv2,https://bitbucket.org/connect2id/nimbus-jose-jwt/wiki/Home diff --git a/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec b/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec index 2839ffac2e..f6ad9661a4 100644 --- a/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec +++ b/metron-deployment/packaging/docker/rpm-docker/SPECS/metron.spec @@ -424,8 +424,14 @@ This package installs the Metron Rest %{metron_home} %dir %{metron_home}/bin %dir %{metron_home}/lib %{metron_home}/config/rest_application.yml +%{metron_home}/config/knox/conf/topologies/metron.xml +%{metron_home}/config/knox/data/services/alerts/rewrite.xml +%{metron_home}/config/knox/data/services/alerts/service.xml +%{metron_home}/config/knox/data/services/rest/rewrite.xml +%{metron_home}/config/knox/data/services/rest/service.xml %{metron_home}/bin/metron-rest.sh %{metron_home}/bin/pcap_to_pdml.sh +%{metron_home}/bin/install_metron_knox.sh %attr(0644,root,root) %{metron_home}/lib/metron-rest-%{full_version}.jar # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/metron-interface/metron-alerts/cypress/integration/pcap/pcap.spec.js b/metron-interface/metron-alerts/cypress/integration/pcap/pcap.spec.js index 58f7d26b41..5c1629fd9d 100644 --- a/metron-interface/metron-alerts/cypress/integration/pcap/pcap.spec.js +++ b/metron-interface/metron-alerts/cypress/integration/pcap/pcap.spec.js @@ -25,6 +25,11 @@ context('PCAP Tab', () => { url: '/api/v1/user', response: 'user' }); + cy.route({ + method: 'POST', + url: '/api/v1/logout', + response: [] + }); cy.route('GET', 'config', 'fixture:config.json'); cy.route('POST', 'search', 'fixture:search.json'); diff --git a/metron-interface/metron-alerts/proxy.conf.json b/metron-interface/metron-alerts/proxy.conf.json index 612bd67420..571af9af91 100644 --- a/metron-interface/metron-alerts/proxy.conf.json +++ b/metron-interface/metron-alerts/proxy.conf.json @@ -2,9 +2,5 @@ "/api/v1": { "target": "http://node1:8082", "secure": false - }, - "/logout": { - "target": "http://node1:8082", - "secure": false } } diff --git a/metron-interface/metron-alerts/src/app/app.component.html b/metron-interface/metron-alerts/src/app/app.component.html index d94a6d72d1..1e1e9bf67c 100644 --- a/metron-interface/metron-alerts/src/app/app.component.html +++ b/metron-interface/metron-alerts/src/app/app.component.html @@ -14,7 +14,7 @@ <div class="container-fluid px-0" [class.notransition]="noTransition"> <nav class="navbar" *ngIf="loggedIn"> <a class="" href="#"> - <img alt="" src="../assets/images/logo.png" width="135" height="45"> + <img alt="" src="assets/images/logo.png" width="135" height="45"> </a> <ul class="nav ml-4"> <li class="nav-item" routerLinkActive="active"> diff --git a/metron-interface/metron-alerts/src/app/service/authentication.service.ts b/metron-interface/metron-alerts/src/app/service/authentication.service.ts index 7739be128d..f8b58451e2 100644 --- a/metron-interface/metron-alerts/src/app/service/authentication.service.ts +++ b/metron-interface/metron-alerts/src/app/service/authentication.service.ts @@ -30,7 +30,7 @@ export class AuthenticationService { private static USER_NOT_VERIFIED = 'USER-NOT-VERIFIED'; private currentUser: string = AuthenticationService.USER_NOT_VERIFIED; loginUrl = this.appConfigService.getApiRoot() + '/user'; - logoutUrl = '/logout'; + logoutUrl = this.appConfigService.getApiRoot() + '/logout'; onLoginEvent: ReplaySubject<boolean> = new ReplaySubject<boolean>(); $onLoginEvent = this.onLoginEvent.asObservable(); diff --git a/metron-interface/metron-alerts/src/app/shared/directives/alert-search.directive.ts b/metron-interface/metron-alerts/src/app/shared/directives/alert-search.directive.ts index eb69b56cea..6af5baddd2 100644 --- a/metron-interface/metron-alerts/src/app/shared/directives/alert-search.directive.ts +++ b/metron-interface/metron-alerts/src/app/shared/directives/alert-search.directive.ts @@ -38,7 +38,7 @@ export class AlertSearchDirective implements AfterViewInit, OnChanges { const el = elementRef.nativeElement; el.classList.add('editor'); - ace.config.set('basePath', '/assets/ace'); + ace.config.set('basePath', 'assets/ace'); this.editor = ace.edit(el); this.editor.$blockScrolling = Infinity; diff --git a/metron-interface/metron-alerts/src/index.html b/metron-interface/metron-alerts/src/index.html index e051e27f78..2eba72b40b 100644 --- a/metron-interface/metron-alerts/src/index.html +++ b/metron-interface/metron-alerts/src/index.html @@ -16,7 +16,7 @@ <head> <meta charset="utf-8"> <title>Metron Alerts</title> - <base href="/"> + <base href="./"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> diff --git a/metron-interface/metron-rest/pom.xml b/metron-interface/metron-rest/pom.xml index 52b01754b2..e6d5fb8086 100644 --- a/metron-interface/metron-rest/pom.xml +++ b/metron-interface/metron-rest/pom.xml @@ -39,6 +39,7 @@ <spring.version>5.0.5.RELEASE</spring.version> <eclipse.link.version>2.6.4</eclipse.link.version> <jsonpath.version>2.4.0</jsonpath.version> + <jwt.version>4.41.2</jwt.version> </properties> <dependencies> <dependency> @@ -426,6 +427,11 @@ </exclusion> </exclusions> </dependency> + <dependency> + <groupId>com.nimbusds</groupId> + <artifactId>nimbus-jose-jwt</artifactId> + <version>${jwt.version}</version> + </dependency> </dependencies> <dependencyManagement> diff --git a/metron-interface/metron-rest/src/main/config/knox/conf/topologies/metron.xml b/metron-interface/metron-rest/src/main/config/knox/conf/topologies/metron.xml new file mode 100644 index 0000000000..3a8897bc5d --- /dev/null +++ b/metron-interface/metron-rest/src/main/config/knox/conf/topologies/metron.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. + --> +<topology> + + <gateway> + <provider> + <role>authentication</role> + <name>ShiroProvider</name> + <enabled>true</enabled> + <param> + <name>sessionTimeout</name> + <value>30</value> + </param> + <param> + <name>main.ldapRealm</name> + <value>org.apache.hadoop.gateway.shirorealm.KnoxLdapRealm</value> + </param> + <param> + <name>main.ldapRealm.userDnTemplate</name> + <value>uid={0},ou=people,dc=hadoop,dc=apache,dc=org</value> + </param> + <param> + <name>main.ldapRealm.contextFactory.url</name> + <value>ldap://localhost:33389</value> + </param> + <param> + <name>main.ldapRealm.contextFactory.authenticationMechanism</name> + <value>simple</value> + </param> + <param> + <name>urls./**</name> + <value>authcBasic</value> + </param> + </provider> + + <provider> + <role>identity-assertion</role> + <name>Default</name> + <enabled>true</enabled> + </provider> + + <provider> + <role>authorization</role> + <name>AclsAuthz</name> + <enabled>true</enabled> + </provider> + + </gateway> + + <service> + <role>METRON-REST</role> + <url>http://localhost:8082</url> + </service> + + <service> + <role>METRON-ALERTS</role> + <url>http://localhost:4201</url> + </service> + +</topology> \ No newline at end of file diff --git a/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/rewrite.xml b/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/rewrite.xml new file mode 100644 index 0000000000..9fecf9ed4c --- /dev/null +++ b/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/rewrite.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. + --> +<rules> + <rule dir="IN" name="METRON-ALERTS/metron-alerts/inbound/root" pattern="*://*:*/**/metron-alerts/"> + <rewrite template="{$serviceUrl[METRON-ALERTS-UI]}/"/> + </rule> + <rule dir="IN" name="METRON-ALERTS/metron-alerts/inbound/path" pattern="*://*:*/**/metron-alerts/{**}"> + <rewrite template="{$serviceUrl[METRON-ALERTS-UI]}/{**}"/> + </rule> + <rule dir="IN" name="METRON-ALERTS/metron-alerts/inbound/query" pattern="*://*:*/**/metron-alerts/{**}?{**}"> + <rewrite template="{$serviceUrl[METRON-ALERTS]}/{**}?{**}"/> + </rule> +</rules> \ No newline at end of file diff --git a/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/service.xml b/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/service.xml new file mode 100644 index 0000000000..a09be990de --- /dev/null +++ b/metron-interface/metron-rest/src/main/config/knox/data/services/alerts/service.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. + --> +<service role="METRON-ALERTS" name="metron-alerts" version="0.6.1"> + <routes> + <route path="/metron-alerts/"/> + <route path="/metron-alerts/**"/> + <route path="/metron-alerts/**?**"/> + </routes> + <dispatch classname="org.apache.hadoop.gateway.dispatch.PassAllHeadersDispatch"/> +</service> \ No newline at end of file diff --git a/metron-interface/metron-rest/src/main/config/knox/data/services/rest/rewrite.xml b/metron-interface/metron-rest/src/main/config/knox/data/services/rest/rewrite.xml new file mode 100644 index 0000000000..fabed06985 --- /dev/null +++ b/metron-interface/metron-rest/src/main/config/knox/data/services/rest/rewrite.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. + --> +<rules> + <rule dir="IN" name="METRON-REST/metron-rest/inbound" pattern="*://*:*/**/metron-rest/{path=**}?{**}"> + <rewrite template="{$serviceUrl[METRON-REST]}/{path=**}?{**}"/> + </rule> +</rules> \ No newline at end of file diff --git a/metron-interface/metron-rest/src/main/config/knox/data/services/rest/service.xml b/metron-interface/metron-rest/src/main/config/knox/data/services/rest/service.xml new file mode 100644 index 0000000000..d9bb56bced --- /dev/null +++ b/metron-interface/metron-rest/src/main/config/knox/data/services/rest/service.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + 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. + --> +<service role="METRON-REST" name="metron-rest" version="0.6.1"> + <routes> + <route path="/metron-rest/**"/> + </routes> + <dispatch classname="org.apache.hadoop.gateway.dispatch.PassAllHeadersDispatch"/> +</service> \ No newline at end of file diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java index 80ac2bf3ab..b8a8306cdd 100644 --- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/MetronRestConstants.java @@ -25,6 +25,7 @@ public static final String TEST_PROFILE = "test"; public static final String LDAP_PROFILE = "ldap"; public static final String DOCKER_PROFILE = "docker"; + public static final String KNOX_PROFILE = "knox"; public static final String CSRF_ENABLE_PROFILE = "csrf-enable"; public static final String GROK_TEMP_PATH_SPRING_PROPERTY = "grok.path.temp"; @@ -71,6 +72,7 @@ public static final String META_DAO_IMPL = "meta.dao.impl"; public static final String META_DAO_SORT = "meta.dao.sort"; + public static final String SECURITY_ROLE_PREFIX = "ROLE_"; public static final String SECURITY_ROLE_USER = "USER"; public static final String SECURITY_ROLE_ADMIN = "ADMIN"; @@ -87,4 +89,10 @@ public static final String PCAP_PDML_SCRIPT_PATH_SPRING_PROPERTY = "pcap.pdml.script.path"; public static final String PCAP_YARN_QUEUE_SPRING_PROPERTY = "pcap.yarn.queue"; public static final String PCAP_FINALIZER_THREADPOOL_SIZE_SPRING_PROPERTY = "pcap.finalizer.threadpool.size"; + + public static final String LDAP_PROVIDER_URL_SPRING_PROPERTY = "ldap.provider.url"; + public static final String LDAP_PROVIDER_USERDN_SPRING_PROPERTY = "ldap.provider.userdn"; + public static final String LDAP_PROVIDER_PASSWORD_SPRING_PROPERTY = "ldap.provider.password"; + + public static final String KNOX_ROOT_SPRING_PROPERTY = "knox.root"; } diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/KnoxSSOAuthenticationFilter.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/KnoxSSOAuthenticationFilter.java new file mode 100644 index 0000000000..e6528f2159 --- /dev/null +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/KnoxSSOAuthenticationFilter.java @@ -0,0 +1,281 @@ +/** + * 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.metron.rest.config; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jwt.SignedJWT; +import com.sun.org.apache.xml.internal.security.algorithms.SignatureAlgorithm; +import org.apache.metron.rest.security.SecurityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.support.LdapNameBuilder; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetails; + +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.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.cert.CertificateException; +import java.text.ParseException; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_PREFIX; +import static org.springframework.ldap.query.LdapQueryBuilder.query; + +/** + * This class is a Servlet Filter that authenticates a Knox SSO token. The token is stored in a cookie and is + * verified against a public Knox key. The token expiration and begin time are also validated. Upon successful validation, + * a Spring Authentication object is built from the user name and user groups queried from LDAP. Currently, user groups are + * mapped directly to Spring roles and prepended with "ROLE_". + */ +public class KnoxSSOAuthenticationFilter implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(KnoxSSOAuthenticationFilter.class); + + private String userSearchBase; + private Path knoxKeyFile; + private String knoxKeyString; + private String knoxCookie; + private LdapTemplate ldapTemplate; + + public KnoxSSOAuthenticationFilter(String userSearchBase, + Path knoxKeyFile, + String knoxKeyString, + String knoxCookie, + LdapTemplate ldapTemplate) { + this.userSearchBase = userSearchBase; + this.knoxKeyFile = knoxKeyFile; + this.knoxKeyString = knoxKeyString; + this.knoxCookie = knoxCookie; + if (ldapTemplate == null) { + throw new IllegalStateException("KnoxSSO requires LDAP. You must add 'ldap' to the active profiles."); + } + this.ldapTemplate = ldapTemplate; + } + + @Override + public void init(FilterConfig filterConfig) { + } + + @Override + public void destroy() { + } + + /** + * Extracts the Knox token from the configured cookie. If basic authentication headers are present, SSO authentication + * is skipped. + * @param request ServletRequest + * @param response ServletResponse + * @param chain FilterChain + * @throws IOException + * @throws ServletException + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + + // If a basic authentication header is present, use that to authenticate and skip SSO + String authHeader = httpRequest.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Basic")) { + String serializedJWT = getJWTFromCookie(httpRequest); + if (serializedJWT != null) { + SignedJWT jwtToken = null; + try { + jwtToken = SignedJWT.parse(serializedJWT); + String userName = jwtToken.getJWTClaimsSet().getSubject(); + LOG.info("SSO login user : {} ", userName); + if (isValid(jwtToken, userName)) { + Authentication authentication = getAuthentication(userName, httpRequest); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (ParseException e) { + LOG.warn("Unable to parse the JWT token", e); + } + } + } + chain.doFilter(request, response); + } + + /** + * Validates a Knox token with expiration and begin times and verifies the token with a public Knox key. + * @param jwtToken Knox token + * @param userName User name associated with the token + * @return Whether a token is valid or not + * @throws ParseException JWT Token could not be parsed. + */ + protected boolean isValid(SignedJWT jwtToken, String userName) throws ParseException { + // Verify the user name is present + if (userName == null || userName.isEmpty()) { + LOG.info("Could not find user name in SSO token"); + return false; + } + + Date now = new Date(); + + // Verify the token has not expired + Date expirationTime = jwtToken.getJWTClaimsSet().getExpirationTime(); + if (expirationTime != null && now.after(expirationTime)) { + LOG.info("SSO token expired: {} ", userName); + return false; + } + + // Verify the token is not before time + Date notBeforeTime = jwtToken.getJWTClaimsSet().getNotBeforeTime(); + if (notBeforeTime != null && now.before(notBeforeTime)) { + LOG.info("SSO token not yet valid: {} ", userName); + return false; + } + + return validateSignature(jwtToken); + } + + /** + * Verify the signature of the JWT token in this method. This method depends on + * the public key that was established during init based upon the provisioned + * public key. Override this method in subclasses in order to customize the + * signature verification behavior. + * + * @param jwtToken The token that contains the signature to be validated. + * @return valid true if signature verifies successfully; false otherwise + */ + protected boolean validateSignature(SignedJWT jwtToken) { + // Verify the token signature algorithm was as expected + String receivedSigAlg = jwtToken.getHeader().getAlgorithm().getName(); + + if (!receivedSigAlg.equals(JWSAlgorithm.RS256.getName())) { + return false; + } + + // Verify the token has been properly signed + if (JWSObject.State.SIGNED == jwtToken.getState()) { + LOG.debug("SSO token is in a SIGNED state"); + if (jwtToken.getSignature() != null) { + LOG.debug("SSO token signature is not null"); + try { + JWSVerifier verifier = new RSASSAVerifier(SecurityUtils.parseRSAPublicKey(getKnoxKey())); + if (jwtToken.verify(verifier)) { + LOG.debug("SSO token has been successfully verified"); + return true; + } else { + LOG.warn("SSO signature verification failed. Please check the public key."); + } + } catch (Exception e) { + LOG.warn("Error while validating signature", e); + } + } + } + return false; + } + + /** + * Encapsulate the acquisition of the JWT token from HTTP cookies within the + * request. + * + * @param req ServletRequest to get the JWT token from + * @return serialized JWT token + */ + protected String getJWTFromCookie(HttpServletRequest req) { + String serializedJWT = null; + Cookie[] cookies = req.getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + LOG.debug(String.format("Found cookie: %s [%s]", cookie.getName(), cookie.getValue())); + if (knoxCookie.equals(cookie.getName())) { + if (LOG.isDebugEnabled()) { + LOG.debug(knoxCookie + " cookie has been found and is being processed"); + } + serializedJWT = cookie.getValue(); + break; + } + } + } else { + if (LOG.isDebugEnabled()) { + LOG.debug(knoxCookie + " not found"); + } + } + return serializedJWT; + } + + /** + * A public Knox key can either be passed in directly or read from a file. + * @return Public Knox key + * @throws IOException There was a problem reading the Knox key file. + */ + private String getKnoxKey() throws IOException { + String knoxKey; + if ((this.knoxKeyString == null || this.knoxKeyString.isEmpty()) && this.knoxKeyFile != null) { + List<String> keyLines = Files.readAllLines(knoxKeyFile, StandardCharsets.UTF_8); + knoxKey = String.join("", keyLines); + } else { + knoxKey = this.knoxKeyString; + } + return knoxKey; + } + + /** + * Builds the Spring Authentication object using the supplied user name and groups looked up from LDAP. Groups are currently + * mapped directly to Spring roles by converting to upper case and prepending the name with "ROLE_". + * @param userName The username to build the Authentication object with. + * @param httpRequest HttpServletRequest + * @return Authentication object for the given user. + */ + private Authentication getAuthentication(String userName, HttpServletRequest httpRequest) { + String ldapName = LdapNameBuilder.newInstance().add(userSearchBase).add("uid", userName).build().toString(); + + // Search ldap for a user's groups and convert to a Spring role + List<GrantedAuthority> grantedAuths = ldapTemplate.search(query() + .where("objectclass") + .is("groupOfNames") + .and("member") + .is(ldapName), (AttributesMapper<String>) attrs -> (String) attrs.get("cn").get()) + .stream() + .map(group -> String.format("%s%s", SECURITY_ROLE_PREFIX, group.toUpperCase())) + .map(SimpleGrantedAuthority::new).collect(Collectors.toList()); + + final UserDetails principal = new User(userName, "", grantedAuths); + final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + principal, "", grantedAuths); + WebAuthenticationDetails webDetails = new WebAuthenticationDetails(httpRequest); + authentication.setDetails(webDetails); + return authentication; + } + +} diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/LdapConfig.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/LdapConfig.java new file mode 100644 index 0000000000..3bb4cd1195 --- /dev/null +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/LdapConfig.java @@ -0,0 +1,52 @@ +/** + * 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.metron.rest.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.LdapContextSource; + +import static org.apache.metron.rest.MetronRestConstants.LDAP_PROFILE; +import static org.apache.metron.rest.MetronRestConstants.LDAP_PROVIDER_PASSWORD_SPRING_PROPERTY; +import static org.apache.metron.rest.MetronRestConstants.LDAP_PROVIDER_URL_SPRING_PROPERTY; +import static org.apache.metron.rest.MetronRestConstants.LDAP_PROVIDER_USERDN_SPRING_PROPERTY; + +@Configuration +@Profile(LDAP_PROFILE) +public class LdapConfig { + + @Autowired + private Environment environment; + + @Bean + public LdapTemplate ldapTemplate() { + LdapContextSource contextSource = new LdapContextSource(); + + contextSource.setUrl(environment.getProperty(LDAP_PROVIDER_URL_SPRING_PROPERTY)); + contextSource.setUserDn(environment.getProperty(LDAP_PROVIDER_USERDN_SPRING_PROPERTY)); + contextSource.setPassword(environment.getProperty(LDAP_PROVIDER_PASSWORD_SPRING_PROPERTY)); + contextSource.afterPropertiesSet(); + + return new LdapTemplate(contextSource); + } + +} diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/SwaggerConfig.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/SwaggerConfig.java index 564ff32896..d0da8cc861 100644 --- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/SwaggerConfig.java +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/SwaggerConfig.java @@ -17,22 +17,64 @@ */ package org.apache.metron.rest.config; +import org.apache.metron.rest.MetronRestConstants; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; import org.springframework.web.bind.annotation.RestController; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.paths.RelativePathProvider; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; +import javax.servlet.ServletContext; +import java.util.Arrays; +import java.util.List; + +import static org.apache.metron.rest.MetronRestConstants.KNOX_PROFILE; + @Configuration @EnableSwagger2 public class SwaggerConfig { + + @Autowired + private Environment environment; + + @Autowired + ServletContext servletContext; + @Bean public Docket api() { - return new Docket(DocumentationType.SWAGGER_2) - .select() + List<String> activeProfiles = Arrays.asList(environment.getActiveProfiles()); + Docket docket = new Docket(DocumentationType.SWAGGER_2); + if (activeProfiles.contains(KNOX_PROFILE)) { + String knoxRoot = environment.getProperty(MetronRestConstants.KNOX_ROOT_SPRING_PROPERTY, String.class, ""); + docket = docket.pathProvider(new RelativePathProvider (servletContext) { + @Override + protected String applicationPath() { + return knoxRoot; + } + + @Override + protected String getDocumentationPath() { + return knoxRoot; + } + + @Override + public String getApplicationBasePath() { + return knoxRoot; + } + + @Override + public String getOperationPath(String operationPath) { + return knoxRoot + super.getOperationPath(operationPath); + } + }); + } + return docket.select() .apis(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .paths(PathSelectors.any()) .build(); diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java index 7ca3a46e4b..835889c6a9 100644 --- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/config/WebSecurityConfig.java @@ -20,8 +20,16 @@ import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_ADMIN; import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_USER; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.interfaces.RSAPublicKey; import java.util.Arrays; import java.util.List; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import javax.sql.DataSource; import org.apache.metron.rest.MetronRestConstants; import org.slf4j.Logger; @@ -31,16 +39,32 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; +import org.springframework.http.HttpMethod; +import org.springframework.ldap.core.AttributesMapper; +import org.springframework.ldap.core.DirContextOperations; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.security.authentication.ProviderManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.LdapShaPasswordEncoder; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; +import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -58,27 +82,49 @@ @Autowired private DataSource dataSource; + @Autowired(required = false) + private LdapTemplate ldapTemplate; + @Value("${ldap.provider.url}") private String providerUrl; + @Value("${ldap.provider.userdn}") private String providerUserDn; + @Value("${ldap.provider.password}") private String providerPassword; + @Value("${ldap.user.dn.patterns}") private String userDnPatterns; + @Value("${ldap.user.passwordAttribute}") private String passwordAttribute; + @Value("${ldap.user.searchBase}") private String userSearchBase; + @Value("${ldap.user.searchFilter}") private String userSearchFilter; + @Value("${ldap.group.searchBase}") private String groupSearchBase; + @Value("${ldap.group.roleAttribute}") private String groupRoleAttribute; + @Value("${ldap.group.searchFilter}") private String groupSearchFilter; + @Value("${knox.sso.pubkeyFile:}") + private Path knoxKeyFile; + + @Value("${knox.sso.pubkey:}") + private String knoxKeyString; + + @Value("${knox.sso.cookie:hadoop-jwt}") + private String knoxCookie; + + @RequestMapping(value = {"/login", "/logout", "/sensors", "/sensors*/**"}, method = RequestMethod.GET) public String handleNGRequests() { return "forward:/index.html"; @@ -100,14 +146,21 @@ protected void configure(HttpSecurity http) throws Exception { .and().httpBasic() .and() .logout() + .logoutUrl("/api/v1/logout") .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()) .invalidateHttpSession(true) - .deleteCookies("JSESSIONID"); - if (Arrays.asList(environment.getActiveProfiles()).contains(MetronRestConstants.CSRF_ENABLE_PROFILE)) { + .deleteCookies("JSESSIONID", knoxCookie); + + List<String> activeProfiles = Arrays.asList(environment.getActiveProfiles()); + if (activeProfiles.contains(MetronRestConstants.CSRF_ENABLE_PROFILE)) { http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } else { http.csrf().disable(); } + if (activeProfiles.contains(MetronRestConstants.KNOX_PROFILE)) { + http.addFilterAt(new KnoxSSOAuthenticationFilter(userSearchBase, knoxKeyFile, knoxKeyString, + knoxCookie, ldapTemplate), UsernamePasswordAuthenticationFilter.class); + } } @Autowired diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsUIController.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsUIController.java index fe2968fe67..6125017d61 100644 --- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsUIController.java +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/controller/AlertsUIController.java @@ -18,6 +18,7 @@ package org.apache.metron.rest.controller; import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_ADMIN; +import static org.apache.metron.rest.MetronRestConstants.SECURITY_ROLE_PREFIX; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; @@ -74,7 +75,7 @@ } } - @Secured({"ROLE_" + SECURITY_ROLE_ADMIN}) + @Secured({SECURITY_ROLE_PREFIX + SECURITY_ROLE_ADMIN}) @ApiOperation(value = "Retrieves all users' settings. Only users that are part of " + "the \"ROLE_ADMIN\" role are allowed to get all user settings.") @ApiResponses(value = {@ApiResponse(message = "List of all user settings", code = 200), @@ -103,7 +104,7 @@ return responseEntity; } - @Secured({"ROLE_" + SECURITY_ROLE_ADMIN}) + @Secured({SECURITY_ROLE_PREFIX + SECURITY_ROLE_ADMIN}) @ApiOperation(value = "Deletes a user's settings. Only users that are part of " + "the \"ROLE_ADMIN\" role are allowed to delete user settings.") @ApiResponses(value = {@ApiResponse(message = "User settings were deleted", code = 200), diff --git a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java index ed10f145f9..5da1a06b30 100644 --- a/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java +++ b/metron-interface/metron-rest/src/main/java/org/apache/metron/rest/security/SecurityUtils.java @@ -21,6 +21,15 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPublicKey; + public class SecurityUtils { /** Returns the username of the currently logged in user. @@ -37,4 +46,28 @@ public static String getCurrentUser() { } return user; } + + public static RSAPublicKey parseRSAPublicKey(String pem) + throws CertificateException, UnsupportedEncodingException { + String PEM_HEADER = "-----BEGIN CERTIFICATE-----\n"; + String PEM_FOOTER = "\n-----END CERTIFICATE-----"; + String fullPem = (pem.startsWith(PEM_HEADER) && pem.endsWith(PEM_FOOTER)) ? pem : PEM_HEADER + pem + PEM_FOOTER; + PublicKey key = null; + try { + CertificateFactory fact = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream is = new ByteArrayInputStream(fullPem.getBytes(StandardCharsets.UTF_8)); + X509Certificate cer = (X509Certificate) fact.generateCertificate(is); + key = cer.getPublicKey(); + } catch (CertificateException ce) { + String message = null; + if (pem.startsWith(PEM_HEADER)) { + message = "CertificateException - be sure not to include PEM header " + + "and footer in the PEM configuration element."; + } else { + message = "CertificateException - PEM may be corrupt"; + } + throw new CertificateException(message, ce); + } + return (RSAPublicKey) key; + } } diff --git a/metron-interface/metron-rest/src/main/scripts/install_metron_knox.sh b/metron-interface/metron-rest/src/main/scripts/install_metron_knox.sh new file mode 100755 index 0000000000..befc294ebd --- /dev/null +++ b/metron-interface/metron-rest/src/main/scripts/install_metron_knox.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# +# 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. +# +METRON_VERSION=${project.version} +METRON_HOME=${METRON_HOME:-/usr/metron/${METRON_VERSION}} +KNOX_HOME=${KNOX_HOME:-/usr/hdp/current/knox-server} +KNOX_METRON_REST_DIR=$KNOX_HOME/data/services/metron-rest/$METRON_VERSION +KNOX_METRON_ALERTS_DIR=$KNOX_HOME/data/services/metron-alerts/$METRON_VERSION + +mkdir -p $KNOX_METRON_REST_DIR +mkdir -p $KNOX_METRON_ALERTS_DIR + +cp $METRON_HOME/config/knox/data/services/rest/* $KNOX_METRON_REST_DIR +cp $METRON_HOME/config/knox/data/services/alerts/* $KNOX_METRON_ALERTS_DIR +cp $METRON_HOME/config/knox/conf/topologies/metron.xml $KNOX_HOME/conf/topologies ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: [email protected] With regards, Apache Git Services
