diff options
Diffstat (limited to 'aai-aaf-auth/src/main/java')
18 files changed, 1903 insertions, 0 deletions
diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AAIAuthCore.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AAIAuthCore.java new file mode 100644 index 00000000..c64251ad --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AAIAuthCore.java @@ -0,0 +1,377 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.auth; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.eclipse.jetty.util.security.Password; +import org.onap.aai.aaf.auth.exceptions.AAIUnrecognizedFunctionException; +import org.onap.aai.logging.ErrorLogHelper; +import org.onap.aai.util.AAIConfig; +import org.onap.aai.util.AAIConstants; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.UnsupportedEncodingException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * The Class AAIAuthCore. + */ +public final class AAIAuthCore { + + private static final Logger LOGGER = LoggerFactory.getLogger(AAIAuthCore.class); + + private static final String ERROR_CODE_AAI_4001 = "AAI_4001"; + + private String globalAuthFileName = AAIConstants.AAI_AUTH_CONFIG_FILENAME; + + private final Pattern AUTH_POLICY_PATTERN; + private final Set<String> validFunctions = new HashSet<>(); + private Map<String, AAIUser> users; + private boolean timerSet = false; + private Timer timer = null; + + private String basePath; + + /** + * Instantiates a new AAI auth core. + */ + public AAIAuthCore(String basePath) { + this(basePath, AAIConstants.AAI_AUTH_CONFIG_FILENAME); + } + + public AAIAuthCore(String basePath, String filename){ + this.basePath = basePath; + this.globalAuthFileName = filename; + AUTH_POLICY_PATTERN = Pattern.compile("^" + this.basePath + "/v\\d+/([\\w\\-]*)"); + init(); + } + + public AAIAuthCore(String basePath, String filename, String pattern){ + this.basePath = basePath; + this.globalAuthFileName = filename; + AUTH_POLICY_PATTERN = Pattern.compile(pattern); + init(); + } + + /** + * Inits the. + */ + private synchronized void init() { + + LOGGER.debug("Initializing Auth Policy Config"); + + reloadUsers(); + + /* + * this timer code is setting up a recurring task that checks if the + * auth config file has been updated and reloads the users if so to get + * the most up to date info (that update check logic is within + * FileWatcher) + * + * the timing this method uses is coarser than the frequency of requests + * AI&I gets so we're looking at better ways of doing this (TODO) + */ + TimerTask task = new FileWatcher(new File(globalAuthFileName)) { + @Override + protected void onChange(File file) { + reloadUsers(); + } + }; + + if (!timerSet) { + timerSet = true; + timer = new Timer(); + + // repeat the check every second + timer.schedule(task, new Date(), 10000); + } + LOGGER.debug("Static Initializiation complete"); + } + + /** + * Cleanup. + */ + // just ends the auth config file update checking timer + public void cleanup() { + timer.cancel(); + } + + /** + * Reload users. + */ + /* + * this essentially takes the data file, which is organized role-first with + * users under each role and converts it to data organized user-first with + * each user containing their role with its associated allowed functions + * this data stored in the class field users + */ + private synchronized void reloadUsers() { + + Map<String, AAIUser> tempUsers = new HashMap<>(); + + try { + LOGGER.debug("Reading from " + globalAuthFileName); + String authFile = new String(Files.readAllBytes(Paths.get(globalAuthFileName))); + + JsonParser parser = new JsonParser(); + JsonObject authObject = parser.parse(authFile).getAsJsonObject(); + if (authObject.has("roles")) { + JsonArray roles = authObject.getAsJsonArray("roles"); + for (JsonElement role : roles) { + if (role.isJsonObject()) { + JsonObject roleObject = role.getAsJsonObject(); + String roleName = roleObject.get("name").getAsString(); + Map<String, Boolean> usrs = this.getUsernamesFromRole(roleObject); + List<String> aaiFunctions = this.getAAIFunctions(roleObject); + + usrs.forEach((key, value) -> { + final AAIUser au = tempUsers.getOrDefault(key, new AAIUser(key, value)); + au.addRole(roleName); + aaiFunctions.forEach(f -> { + List<String> httpMethods = this.getRoleHttpMethods(f, roleObject); + httpMethods.forEach(hm -> au.setUserAccess(f, hm)); + this.validFunctions.add(f); + }); + + tempUsers.put(key, au); + + }); + } + } + if (!tempUsers.isEmpty()) { + users = tempUsers; + } + } + } catch (FileNotFoundException e) { + ErrorLogHelper.logError(ERROR_CODE_AAI_4001, globalAuthFileName + ". Exception: " + e); + } catch (JsonProcessingException e) { + ErrorLogHelper.logError(ERROR_CODE_AAI_4001, globalAuthFileName + ". Not valid JSON: " + e); + } catch (Exception e) { + ErrorLogHelper.logError(ERROR_CODE_AAI_4001, globalAuthFileName + ". Exception caught: " + e); + } + } + + private List<String> getRoleHttpMethods(String aaiFunctionName, JsonObject roleObject) { + List<String> httpMethods = new ArrayList<>(); + + JsonArray ja = roleObject.getAsJsonArray("functions"); + for (JsonElement je : ja) { + if (je.isJsonObject() && je.getAsJsonObject().has("name") + && je.getAsJsonObject().get("name").getAsString().equals(aaiFunctionName)) { + JsonArray jaMeth = je.getAsJsonObject().getAsJsonArray("methods"); + for (JsonElement jeMeth : jaMeth) { + if (jeMeth.isJsonObject() && jeMeth.getAsJsonObject().has("name")) { + httpMethods.add(jeMeth.getAsJsonObject().get("name").getAsString()); + } + } + } + } + + return httpMethods; + } + + private List<String> getAAIFunctions(JsonObject roleObject) { + List<String> aaiFunctions = new ArrayList<>(); + + JsonArray ja = roleObject.getAsJsonArray("functions"); + for (JsonElement je : ja) { + if (je.isJsonObject() && je.getAsJsonObject().has("name")) { + aaiFunctions.add(je.getAsJsonObject().get("name").getAsString()); + } + } + + return aaiFunctions; + } + + private Map<String, Boolean> getUsernamesFromRole(JsonObject roleObject) throws UnsupportedEncodingException { + Map<String, Boolean> usernames = new HashMap<>(); + + JsonArray uja = roleObject.getAsJsonArray("users"); + for (JsonElement je : uja) { + if (je.isJsonObject()) { + if (je.getAsJsonObject().has("username")) { + if (je.getAsJsonObject().has("is-wildcard-id")) { + usernames.put(je.getAsJsonObject().get("username").getAsString().toLowerCase(), + je.getAsJsonObject().get("is-wildcard-id").getAsBoolean()); + } else { + usernames.put(je.getAsJsonObject().get("username").getAsString().toLowerCase(), false); + } + } else if (je.getAsJsonObject().has("user")) { + String auth = je.getAsJsonObject().get("user").getAsString() + ":" + + Password.deobfuscate(je.getAsJsonObject().get("pass").getAsString()); + String authorizationCode = new String(Base64.getEncoder().encode(auth.getBytes("utf-8"))); + usernames.put(authorizationCode, false); + } + } + } + + return usernames; + } + + public String getAuthPolicyFunctName(String uri) { + String authPolicyFunctionName = ""; + if (uri.startsWith(basePath + "/search")) { + authPolicyFunctionName = "search"; + } else if (uri.startsWith(basePath + "/recents")) { + authPolicyFunctionName = "recents"; + } else if (uri.startsWith(basePath + "/cq2gremlin")) { + authPolicyFunctionName = "cq2gremlin"; + } else if (uri.startsWith(basePath + "/cq2gremlintest")) { + authPolicyFunctionName = "cq2gremlintest"; + } else if (uri.startsWith(basePath + "/util/echo")) { + authPolicyFunctionName = "util"; + } else if (uri.startsWith(basePath + "/tools")) { + authPolicyFunctionName = "tools"; + } else { + Matcher match = AUTH_POLICY_PATTERN.matcher(uri); + if (match.find()) { + authPolicyFunctionName = match.group(1); + } + } + return authPolicyFunctionName; + } + + /** + * for backwards compatibility + * + * @param username + * @param uri + * @param httpMethod + * @param haProxyUser + * @return + * @throws AAIUnrecognizedFunctionException + */ + public boolean authorize(String username, String uri, String httpMethod, String haProxyUser) + throws AAIUnrecognizedFunctionException { + return authorize(username, uri, httpMethod, haProxyUser, null); + } + + /** + * + * @param username + * @param uri + * @param httpMethod + * @param haProxyUser + * @param issuer issuer of the cert + * @return + * @throws AAIUnrecognizedFunctionException + */ + public boolean authorize(String username, String uri, String httpMethod, String haProxyUser, String issuer) + throws AAIUnrecognizedFunctionException { + String aaiMethod = this.getAuthPolicyFunctName(uri); + if (!this.validFunctions.contains(aaiMethod) && !("info".equalsIgnoreCase(aaiMethod))) { + throw new AAIUnrecognizedFunctionException(aaiMethod); + } + boolean wildcardCheck = isWildcardIssuer(issuer); + boolean authorized; + LOGGER.debug( + "Authorizing the user for the request cert {}, haproxy header {}, aai method {}, httpMethod {}, cert issuer {}", + username, haProxyUser, aaiMethod, httpMethod, issuer); + Optional<AAIUser> oau = this.getUser(username, wildcardCheck); + if (oau.isPresent()) { + AAIUser au = oau.get(); + if (au.hasRole("HAProxy")) { + LOGGER.debug("User has HAProxy role"); + if ("GET".equalsIgnoreCase(httpMethod) && "util".equalsIgnoreCase(aaiMethod) && haProxyUser.isEmpty()) { + LOGGER.debug("Authorized user has HAProxy role with echo request"); + authorized = this.authorize(au, aaiMethod, httpMethod); + } else { + authorized = this.authorize(haProxyUser, uri, httpMethod, "", issuer); + } + } else { + LOGGER.debug("User doesn't have HAProxy role so assuming its a regular client"); + authorized = this.authorize(au, aaiMethod, httpMethod); + } + } else { + LOGGER.debug("User not found: " + username + " on function " + aaiMethod + " request type " + httpMethod); + authorized = false; + } + + return authorized; + } + + private boolean isWildcardIssuer(String issuer) { + if (issuer != null && !issuer.isEmpty()) { + List<String> validIssuers = Arrays + .asList(AAIConfig.get("aaf.valid.issuer.wildcard", UUID.randomUUID().toString()).split("\\|")); + for (String validIssuer : validIssuers) { + if (issuer.contains(validIssuer)) { + return true; + } + } + } + return false; + } + + /** + * returns aai user either matching the username or containing the wildcard. + * + * @param username + * @return + */ + public Optional<AAIUser> getUser(String username, boolean wildcardCheck) { + if (users.containsKey(username)) { + return Optional.of(users.get(username)); + } else if (wildcardCheck) { + List<AAIUser> laus = + users.entrySet().stream().filter(e -> e.getValue().isWildcard() && username.contains(e.getKey())) + .map(Map.Entry::getValue).collect(Collectors.toList()); + if (!laus.isEmpty()) { + return Optional.of(laus.get(0)); + } + } + return Optional.empty(); + } + + /** + * + * @param aaiUser + * aai user with the username + * @param aaiMethod + * aai function the authorization is required on + * @param httpMethod + * http action user is attempting + * @return true, if successful + */ + private boolean authorize(AAIUser aaiUser, String aaiMethod, String httpMethod) { + if ("info".equalsIgnoreCase(aaiMethod)|| aaiUser.hasAccess(aaiMethod, httpMethod)) { + LOGGER.debug("AUTH ACCEPTED: " + aaiUser.getUsername() + " on function " + aaiMethod + " request type " + + httpMethod); + return true; + } else { + LOGGER.debug("AUTH FAILED: " + aaiUser.getUsername() + " on function " + aaiMethod + " request type " + + httpMethod); + return false; + } + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AAIUser.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AAIUser.java new file mode 100644 index 00000000..4512adb0 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AAIUser.java @@ -0,0 +1,81 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Modifications Copyright © 2018 IBM. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.auth; + +import java.util.*; + +public class AAIUser { + + private String username; + + private boolean isWildcard = false; + private Set<String> roles; + private Map<String, Set<String>> aaiFunctionToHttpMethod; + + public AAIUser(String username) { + this(username, false); + } + + public AAIUser(String username, boolean isWildcard) { + this.username = username; + this.roles = new HashSet<>(); + this.aaiFunctionToHttpMethod = new HashMap<>(); + this.isWildcard = isWildcard; + } + + public boolean isWildcard() { + return isWildcard; + } + + public String getUsername() { + return username; + } + + public void addRole(String role) { + this.roles.add(role); + } + + public boolean hasRole(String role) { + return this.roles.contains(role); + } + + public void setUserAccess(String aaiMethod, String... httpMethods) { + for (String httpMethod : httpMethods) { + this.addUserAccess(aaiMethod, httpMethod); + } + } + + private void addUserAccess(String aaiMethod, String httpMethod) { + Set<String> httpMethods = new HashSet<>(); + if (this.aaiFunctionToHttpMethod.containsKey(aaiMethod)) { + httpMethods = this.aaiFunctionToHttpMethod.get(aaiMethod); + } + httpMethods.add(httpMethod); + this.aaiFunctionToHttpMethod.put(aaiMethod, httpMethods); + } + + public boolean hasAccess(String aaiMethod, String httpMethod) { + return this.aaiFunctionToHttpMethod.getOrDefault(aaiMethod, Collections.emptySet()).contains(httpMethod); + } + +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AafRequestFilter.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AafRequestFilter.java new file mode 100644 index 00000000..9a02fe2c --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AafRequestFilter.java @@ -0,0 +1,114 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.auth; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.onap.aaf.cadi.filter.CadiFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; + +import static org.onap.aai.aaf.auth.ResponseFormatter.errorResponse; + +/** + * The Class AafRequestFilter provides common auth filter methods + */ +public class AafRequestFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AafRequestFilter.class); + + public static void authenticationFilter(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain, CadiFilter cadiFilter, Properties props, String userChainPattern) + throws IOException, ServletException { + if (!request.getRequestURI().matches("^.*/util/echo$")) { + + List<String> cadiConfiguredIssuers = CertUtil.getCadiCertIssuers(props); + String issuer = CertUtil.getCertIssuer(request); + if (issuer == null || issuer.isEmpty()) { + errorResponse(request, response); + return; + } + issuer = issuer.replaceAll("\\s+", "").toUpperCase(); + + if (cadiConfiguredIssuers.contains(issuer)) { + LOGGER.debug("authenticationFilter CADI issuer " + issuer); + if (CertUtil.isHaProxy(request)) { + // get the end user/client mechid and use it in the user chain header value + String user = CertUtil.getMechId(request); + LOGGER.debug("authenticationFilter haProxy sent end user/mechid " + user); + if (user == null || user.isEmpty()) { + errorResponse(request, response); + return; + } + AafRequestWrapper reqWrapper = new AafRequestWrapper(request); + String userChainHdr = CertUtil.buildUserChainHeader(user, userChainPattern); + LOGGER.debug("User chain header value: " + userChainHdr); + reqWrapper.putHeader(CertUtil.AAF_USER_CHAIN_HDR, userChainHdr); + cadiFilter.doFilter(reqWrapper, response, filterChain); + } else { + cadiFilter.doFilter(request, response, filterChain); + } + if (response.getStatus() == 401 || response.getStatus() == 403) { + LOGGER.debug("authenticationFilter failed CADI authentication"); + errorResponse(request, response); + return; + } + } else { + filterChain.doFilter(request, response); + } + } else { + filterChain.doFilter(request, response); + } + } + + public static void authorizationFilter(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain, String permission, Properties props) throws IOException, ServletException { + if (request.getRequestURI().matches("^.*/util/echo$")) { + filterChain.doFilter(request, response); + } + List<String> cadiConfiguredIssuers = CertUtil.getCadiCertIssuers(props); + String issuer = CertUtil.getCertIssuer(request); + if (issuer == null || issuer.isEmpty()) { + errorResponse(request, response); + return; + } + issuer = issuer.replaceAll("\\s+", "").toUpperCase(); + Enumeration hdrs = request.getHeaders(CertUtil.AAF_USER_CHAIN_HDR); + while (hdrs.hasMoreElements()) { + String headerValue = (String) hdrs.nextElement(); + LOGGER.debug("authorizationFilter user chain headerValue=" + headerValue); + } + if ((cadiConfiguredIssuers.contains(issuer)) && (!request.isUserInRole(permission))) { + LOGGER.debug( + "authorizationFilter failed CADI authorization issuer=" + issuer + " permission=" + permission); + errorResponse(request, response); + } else { + filterChain.doFilter(request, response); + } + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AafRequestWrapper.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AafRequestWrapper.java new file mode 100644 index 00000000..0ecca679 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/AafRequestWrapper.java @@ -0,0 +1,78 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.auth; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.util.*; + +/** + * The AafRequestWrapper sets the user in the principal name + */ +public class AafRequestWrapper extends HttpServletRequestWrapper { + + private final Map<String, String> customHeaders; + + public AafRequestWrapper(HttpServletRequest request) { + super(request); + this.customHeaders = new HashMap<String, String>(); + } + + public void putHeader(String name, String value) { + this.customHeaders.put(name, value); + } + + @Override + public String getHeader(String name) { + String headerValue = customHeaders.get(name); + if (headerValue != null) { + return headerValue; + } + return (((HttpServletRequest) getRequest()).getHeader(name)); + } + + @Override + public Enumeration<String> getHeaderNames() { + Set<String> nameSet = new HashSet<String>(customHeaders.keySet()); + + Enumeration<String> e = ((HttpServletRequest) getRequest()).getHeaderNames(); + while (e.hasMoreElements()) { + String headerName = e.nextElement(); + nameSet.add(headerName); + } + return Collections.enumeration(nameSet); + } + + @Override + public Enumeration<String> getHeaders(String name) { + String myHeaderValue = customHeaders.get(name); + Set<String> headerValueSet = new HashSet<String>(); + if (myHeaderValue != null) { + headerValueSet.add(myHeaderValue); + } + Enumeration<String> e = ((HttpServletRequest) getRequest()).getHeaders(name); + while (e.hasMoreElements()) { + String headerValue = e.nextElement(); + headerValueSet.add(headerValue); + } + return Collections.enumeration(headerValueSet); + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/CertUtil.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/CertUtil.java new file mode 100644 index 00000000..334a3060 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/CertUtil.java @@ -0,0 +1,157 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.auth; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.*; +import java.util.stream.Collectors; + +/** + * The Class CertUtil provides cert related utility methods. + */ +public class CertUtil { + public static final String DEFAULT_CADI_ISSUERS = + "CN=ATT AAF CADI Test Issuing CA 01, OU=CSO, O=ATT, C=US:CN=ATT AAF CADI Test Issuing CA 02, OU=CSO, O=ATT, C=US"; + public static final String CADI_PROP_FILES = "cadi_prop_files"; + public static final String CADI_ISSUERS_PROP_NAME = "cadi_x509_issuers"; + public static final String CADI_ISSUERS_SEPARATOR = ":"; + public static final String AAI_SSL_CLIENT_OU_HDR = "X-AAI-SSL-Client-OU"; + public static final String AAI_SSL_ISSUER_HDR = "X-AAI-SSL-Issuer"; + public static final String AAI_SSL_CLIENT_CN_HDR = "X-AAI-SSL-Client-CN"; + public static final String AAI_SSL_CLIENT_O_HDR = "X-AAI-SSL-Client-O"; + public static final String AAI_SSL_CLIENT_L_HDR = "X-AAI-SSL-Client-L"; + public static final String AAI_SSL_CLIENT_ST_HDR = "X-AAI-SSL-Client-ST"; + public static final String AAI_SSL_CLIENT_C_HDR = "X-AAI-SSL-Client-C"; + public static final String AAF_USER_CHAIN_HDR = "USER_CHAIN"; + public static final String AAF_ID = "<AAF-ID>"; + private static final Logger LOGGER = LoggerFactory.getLogger(CertUtil.class); + + public static String getAaiSslClientOuHeader(HttpServletRequest hsr) { + return (hsr.getHeader(AAI_SSL_CLIENT_OU_HDR)); + } + + public static boolean isHaProxy(HttpServletRequest hsr) { + + String haProxyUser = ""; + if (Objects.isNull(hsr.getHeader(AAI_SSL_CLIENT_CN_HDR)) || Objects.isNull(hsr.getHeader(AAI_SSL_CLIENT_OU_HDR)) + || Objects.isNull(hsr.getHeader(AAI_SSL_CLIENT_O_HDR)) + || Objects.isNull(hsr.getHeader(AAI_SSL_CLIENT_L_HDR)) + || Objects.isNull(hsr.getHeader(AAI_SSL_CLIENT_ST_HDR)) + || Objects.isNull(hsr.getHeader(AAI_SSL_CLIENT_C_HDR))) { + haProxyUser = ""; + } else { + haProxyUser = String.format("CN=%s, OU=%s, O=\"%s\", L=%s, ST=%s, C=%s", + Objects.toString(hsr.getHeader(AAI_SSL_CLIENT_CN_HDR), ""), + Objects.toString(hsr.getHeader(AAI_SSL_CLIENT_OU_HDR), ""), + Objects.toString(hsr.getHeader(AAI_SSL_CLIENT_O_HDR), ""), + Objects.toString(hsr.getHeader(AAI_SSL_CLIENT_L_HDR), ""), + Objects.toString(hsr.getHeader(AAI_SSL_CLIENT_ST_HDR), ""), + Objects.toString(hsr.getHeader(AAI_SSL_CLIENT_C_HDR), "")).toLowerCase(); + } + if (!haProxyUser.isEmpty()) { + LOGGER.debug("isHaProxy haProxyUser=" + haProxyUser); + return true; + } + LOGGER.debug("isHaProxy haProxyUser not found"); + return false; + } + + public static String getMechId(HttpServletRequest hsr) { + String mechId = null; + String ou = getAaiSslClientOuHeader(hsr); + if ((ou != null) && (!ou.isEmpty())) { + String[] parts = ou.split(CADI_ISSUERS_SEPARATOR); + if (parts != null && parts.length >= 1) { + mechId = parts[0]; + } + } + LOGGER.debug("getMechId mechId=" + mechId); + return (mechId); + } + + public static String getCertIssuer(HttpServletRequest hsr) { + String issuer = hsr.getHeader(AAI_SSL_ISSUER_HDR); + if (issuer != null && !issuer.isEmpty()) { + LOGGER.debug("getCertIssuer issuer from header " + AAI_SSL_ISSUER_HDR + " " + issuer); + // the haproxy header replaces the ', ' with '/' and reverses on the '/' need to undo that. + List<String> broken = Arrays.asList(issuer.split("/")); + broken = broken.stream().filter(s -> !s.isEmpty()).collect(Collectors.toList()); + Collections.reverse(broken); + issuer = String.join(", ", broken); + } else { + if (hsr.getAttribute("javax.servlet.request.cipher_suite") != null) { + X509Certificate[] certChain = + (X509Certificate[]) hsr.getAttribute("javax.servlet.request.X509Certificate"); + if (certChain != null && certChain.length > 0) { + X509Certificate clientCert = certChain[0]; + issuer = clientCert.getIssuerX500Principal().getName(); + LOGGER.debug("getCertIssuer issuer from client cert " + issuer); + } + } + } + return issuer; + } + + public static List<String> getCadiCertIssuers(Properties cadiProperties) { + + List<String> defaultList = new ArrayList<String>(); + List<String> resultList = new ArrayList<String>(); + + String[] cIssuers = DEFAULT_CADI_ISSUERS.split(CADI_ISSUERS_SEPARATOR); + for (String issuer : cIssuers) { + defaultList.add(issuer.replaceAll("\\s+", "").toUpperCase()); + } + try { + String certPropFileName = cadiProperties.getProperty(CADI_PROP_FILES); + String configuredIssuers = DEFAULT_CADI_ISSUERS; + Properties certProperties = new Properties(); + if (certPropFileName != null) { + certProperties.load(new FileInputStream(new File(certPropFileName))); + configuredIssuers = certProperties.getProperty(CADI_ISSUERS_PROP_NAME); + } + if ((configuredIssuers != null) && (!configuredIssuers.isEmpty())) { + cIssuers = configuredIssuers.split(CADI_ISSUERS_SEPARATOR); + for (String issuer : cIssuers) { + resultList.add(issuer.replaceAll("\\s+", "").toUpperCase()); + } + } + } catch (IOException ioe) { + return (defaultList); + } + if (resultList.isEmpty()) { + return defaultList; + } + LOGGER.debug("getCadiCertIssuers " + resultList.toString()); + return resultList; + } + + public static String buildUserChainHeader(String user, String userChainPattern) { + // aaf.userchain.pattern=<AAF-ID>:${aaf.userchain.service.reference}:${aaf.userchain.auth.type}:AS + return (userChainPattern.replaceAll(AAF_ID, user)); + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/FileWatcher.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/FileWatcher.java new file mode 100644 index 00000000..45331ed9 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/FileWatcher.java @@ -0,0 +1,60 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.auth; + +import java.io.File; +import java.util.TimerTask; + +public abstract class FileWatcher extends TimerTask { + private long timeStamp; + private File file; + + /** + * Instantiates a new file watcher. + * + * @param file the file + */ + public FileWatcher(File file) { + this.file = file; + this.timeStamp = file.lastModified(); + } + + /** + * runs a timer task + * + * @see TimerTask#run + */ + public final void run() { + long timeStamp = file.lastModified(); + + if ((timeStamp - this.timeStamp) > 500) { + this.timeStamp = timeStamp; + onChange(file); + } + } + + /** + * On change. + * + * @param file the file + */ + protected abstract void onChange(File file); +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/ResponseFormatter.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/ResponseFormatter.java new file mode 100644 index 00000000..d7c88e81 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/ResponseFormatter.java @@ -0,0 +1,57 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.auth; + +import org.onap.aai.exceptions.AAIException; +import org.onap.aai.logging.ErrorLogHelper; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.MediaType; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; + +public class ResponseFormatter { + + private static final String ACCEPT_HEADER = "accept"; + + public static void errorResponse(HttpServletRequest request, HttpServletResponse response) throws IOException { + errorResponse(new AAIException("AAI_3300"), request, response); + } + + public static void errorResponse(AAIException exception, HttpServletRequest request, HttpServletResponse response) throws IOException { + + if(response.isCommitted()){ + return; + } + + String accept = request.getHeader(ACCEPT_HEADER) == null ? MediaType.APPLICATION_XML : request.getHeader(ACCEPT_HEADER); + + response.setStatus(exception.getErrorObject().getHTTPResponseCode().getStatusCode()); + response.setHeader("Content-Type", accept); + response.resetBuffer(); + + String resp = ErrorLogHelper.getRESTAPIErrorResponse(Collections.singletonList(MediaType.valueOf(accept)), exception, new ArrayList<>()); + response.getOutputStream().print(resp); + response.flushBuffer(); + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/exceptions/AAIUnrecognizedFunctionException.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/exceptions/AAIUnrecognizedFunctionException.java new file mode 100644 index 00000000..c6a97ac5 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/auth/exceptions/AAIUnrecognizedFunctionException.java @@ -0,0 +1,46 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Modifications Copyright © 2018 IBM. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.auth.exceptions; + +import org.onap.aai.exceptions.AAIException; + +public class AAIUnrecognizedFunctionException extends AAIException { + + private static final String AAI_3012 = "AAI_3012"; + private static final long serialVersionUID = 3297064867724071290L; + + public AAIUnrecognizedFunctionException() { + } + + public AAIUnrecognizedFunctionException(String message) { + super(AAI_3012, message); + } + + public AAIUnrecognizedFunctionException(Throwable cause) { + super(AAI_3012, cause); + } + + public AAIUnrecognizedFunctionException(String message, Throwable cause) { + super(AAI_3012, cause, message); + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafAuthorizationFilter.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafAuthorizationFilter.java new file mode 100644 index 00000000..9f2782a4 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafAuthorizationFilter.java @@ -0,0 +1,119 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.filters; + +import org.onap.aai.aaf.auth.ResponseFormatter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.filter.OrderedRequestContextFilter; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * AAF authorization filter + */ + +@Component +@Profile(AafProfiles.AAF_AUTHENTICATION) +@PropertySource(value = "file:${CONFIG_HOME}/aaf/permissions.properties", ignoreResourceNotFound = true) +@PropertySource(value = "file:${server.local.startpath}/aaf/permissions.properties", ignoreResourceNotFound = true) +public class AafAuthorizationFilter extends OrderedRequestContextFilter { + + private static final String ADVANCED = "advanced"; + private static final String BASIC = "basic"; + + private final String type; + private final String instance; + + private GremlinFilter gremlinFilter; + + private List<String> advancedKeywordsList; + + @Autowired + public AafAuthorizationFilter( + GremlinFilter gremlinFilter, + @Value("${permission.type}") String type, + @Value("${permission.instance}") String instance, + @Value("${advanced.keywords.list:}") String advancedKeys + ) { + this.gremlinFilter = gremlinFilter; + this.type = type; + this.instance = instance; + if(advancedKeys == null || advancedKeys.isEmpty()){ + this.advancedKeywordsList = new ArrayList<>(); + } else { + this.advancedKeywordsList = Arrays.stream(advancedKeys.split(",")) + .collect(Collectors.toList()); + } + this.setOrder(FilterPriority.AAF_AUTHORIZATION.getPriority()); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + if(request.getRequestURI().endsWith("/query")){ + gremlinFilter.doBasicAuthFilter(request, response, filterChain); + } else { + + String permission = null; + + if(advancedKeywordsList == null || advancedKeywordsList.size() == 0) { + permission = String.format("%s|%s|%s", type, instance, request.getMethod().toLowerCase()); + } else { + + boolean isAdvanced = this.containsAdvancedKeywords(request); + + //if the URI contains advanced.keywords it's an advanced query + String queryType = isAdvanced ? ADVANCED : BASIC; + permission = String.format("%s|%s|%s", type, instance, queryType); + } + + boolean isAuthorized = request.isUserInRole(permission); + + if(!isAuthorized){ + ResponseFormatter.errorResponse(request, response); + } else { + filterChain.doFilter(request,response); + } + + } + } + + private boolean containsAdvancedKeywords(HttpServletRequest request) { + String uri = request.getRequestURI(); + for (String keyword: advancedKeywordsList) { + if (uri.contains(keyword)) { + return true; + } + } + return false; + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafCertAuthorizationFilter.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafCertAuthorizationFilter.java new file mode 100644 index 00000000..7ec6bb64 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafCertAuthorizationFilter.java @@ -0,0 +1,107 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.filters; + +import org.onap.aai.aaf.auth.AafRequestFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.filter.OrderedRequestContextFilter; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + + +/** + * AAF with client cert authorization filter + */ + +@Component +@Profile(AafProfiles.AAF_CERT_AUTHENTICATION) +@PropertySource(value = "file:${CONFIG_HOME}/aaf/permissions.properties", ignoreResourceNotFound = true) +@PropertySource(value = "file:${server.local.startpath}/aaf/permissions.properties", ignoreResourceNotFound = true) +public class AafCertAuthorizationFilter extends OrderedRequestContextFilter { + + private static final String ADVANCED = "advanced"; + private static final String BASIC = "basic"; + + String type; + + String instance; + + private CadiProps cadiProps; + + private List<String> advancedKeywordsList; + + @Autowired + public AafCertAuthorizationFilter( + @Value("${permission.type}") String type, + @Value("${permission.instance}") String instance, + @Value("${advanced.keywords.list:}") String advancedKeys, + CadiProps cadiProps + ) { + this.type = type; + this.instance = instance; + this.cadiProps = cadiProps; + if(advancedKeys == null || advancedKeys.isEmpty()){ + this.advancedKeywordsList = new ArrayList<>(); + } else { + this.advancedKeywordsList = Arrays.stream(advancedKeys.split(",")) + .collect(Collectors.toList()); + } + this.setOrder(FilterPriority.AAF_CERT_AUTHORIZATION.getPriority()); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + if(advancedKeywordsList == null || advancedKeywordsList.size() == 0){ + String permission = String.format("%s|%s|%s", type, instance, request.getMethod().toLowerCase()); + AafRequestFilter.authorizationFilter(request, response, filterChain, permission, cadiProps.getCadiProperties()); + } else { + boolean isAdvanced = this.containsAdvancedKeywords(request); + + //if the URI contains advanced.keywords it's an advanced query + String queryType = isAdvanced ? ADVANCED : BASIC; + String permission = String.format("%s|%s|%s", type, instance, queryType); + AafRequestFilter.authorizationFilter(request, response, filterChain, permission, cadiProps.getCadiProperties()); + } + } + + private boolean containsAdvancedKeywords(HttpServletRequest request) { + String uri = request.getRequestURI(); + for (String keyword: advancedKeywordsList) { + if (uri.contains(keyword)) { + return true; + } + } + return false; + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafCertFilter.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafCertFilter.java new file mode 100644 index 00000000..71238cfc --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafCertFilter.java @@ -0,0 +1,111 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.filters; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.onap.aaf.cadi.PropAccess; +import org.onap.aaf.cadi.filter.CadiFilter; +import org.onap.aai.aaf.auth.AafRequestFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.filter.OrderedRequestContextFilter; +import org.springframework.context.annotation.Profile; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * AAF with client cert authentication filter + */ + +@Component +@Profile(AafProfiles.AAF_CERT_AUTHENTICATION) +@PropertySource(value = "file:${CONFIG_HOME}/aaf/permissions.properties", ignoreResourceNotFound = true) +@PropertySource(value = "file:${server.local.startpath}/aaf/permissions.properties", ignoreResourceNotFound = true) +public class AafCertFilter extends OrderedRequestContextFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AafCertFilter.class); + + String aafUserChainPattern; + + private final CadiFilter cadiFilter; + + private final CadiProps cadiProps; + + @Autowired + public AafCertFilter( @Value("${aaf.userchain.pattern}") String aafUserChainPattern, + CadiProps cadiProps) throws IOException, ServletException { + + this.aafUserChainPattern = aafUserChainPattern; + this.cadiProps = cadiProps; + cadiFilter = new CadiFilter(new PropAccess((level,element)->{ + switch (level) { + case DEBUG: + LOGGER.debug(buildMsg(element)); + break; + case INFO: + case AUDIT: + LOGGER.info(buildMsg(element)); + break; + case WARN: + LOGGER.warn(buildMsg(element)); + break; + case ERROR: + LOGGER.error(buildMsg(element)); + break; + case INIT: + LOGGER.info(buildMsg(element)); + break; + case TRACE: + LOGGER.trace(buildMsg(element)); + break; + case NONE: + break; + } + }, new String[]{"cadi_prop_files=" + cadiProps.getCadiFileName()} )); + this.setOrder(FilterPriority.AAF_CERT_AUTHENTICATION.getPriority()); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + AafRequestFilter.authenticationFilter(request, response, filterChain, cadiFilter, cadiProps.getCadiProperties(), aafUserChainPattern); + } + private String buildMsg(Object[] objects) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for ( Object o: objects ) { + if (first) { + first = false; + } + else { + sb.append(' '); + } + sb.append(o.toString()); + } + return (sb.toString()); + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafFilter.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafFilter.java new file mode 100644 index 00000000..16d9afd2 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafFilter.java @@ -0,0 +1,108 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +package org.onap.aai.aaf.filters; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.onap.aaf.cadi.PropAccess; +import org.onap.aaf.cadi.filter.CadiFilter; +import org.onap.aai.aaf.auth.ResponseFormatter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.filter.OrderedRequestContextFilter; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + + +/** + * AAF authentication filter + */ + +@Component +@Profile(AafProfiles.AAF_AUTHENTICATION) +public class AafFilter extends OrderedRequestContextFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(AafCertFilter.class); + + private final CadiFilter cadiFilter; + + @Autowired + public AafFilter(CadiProps cadiProps) throws IOException, ServletException { + cadiFilter = new CadiFilter(new PropAccess((level,element)->{ + switch (level) { + case DEBUG: + LOGGER.debug(buildMsg(element)); + break; + case INFO: + case AUDIT: + LOGGER.info(buildMsg(element)); + break; + case WARN: + LOGGER.warn(buildMsg(element)); + break; + case ERROR: + LOGGER.error(buildMsg(element)); + break; + case INIT: + LOGGER.info(buildMsg(element)); + break; + case TRACE: + LOGGER.trace(buildMsg(element)); + break; + case NONE: + break; + } + }, new String[]{"cadi_prop_files=" + cadiProps.getCadiFileName()} )); + this.setOrder(FilterPriority.AAF_AUTHENTICATION.getPriority()); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + if (!request.getRequestURI().matches("^.*/util/echo$")) { + cadiFilter.doFilter(request, response, filterChain); + if (response.getStatus() == 401 || response.getStatus() == 403) { + ResponseFormatter.errorResponse(request, response); + } + } else { + filterChain.doFilter(request, response); + } + } + + private String buildMsg(Object[] objects) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for ( Object o: objects ) { + if (first) { + first = false; + } + else { + sb.append(' '); + } + sb.append(o.toString()); + } + return (sb.toString()); + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafProfiles.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafProfiles.java new file mode 100644 index 00000000..b587716e --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/AafProfiles.java @@ -0,0 +1,33 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ +package org.onap.aai.aaf.filters; + +public class AafProfiles { + + // AAF Basic Auth + public static final String AAF_AUTHENTICATION = "aaf-auth"; + + // AAF Auth with Client Certs + public static final String AAF_CERT_AUTHENTICATION = "aaf-cert-auth"; + + public static final String TWO_WAY_SSL = "two-way-ssl"; + + private AafProfiles(){} +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/CadiProps.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/CadiProps.java new file mode 100644 index 00000000..35e88f5f --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/CadiProps.java @@ -0,0 +1,81 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ +package org.onap.aai.aaf.filters; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +// This component will be created if and only if any of the following profiles are active +@Component +@Profile({ + AafProfiles.AAF_CERT_AUTHENTICATION, + AafProfiles.AAF_AUTHENTICATION, + AafProfiles.TWO_WAY_SSL +}) +public class CadiProps { + + private static final Logger LOGGER = LoggerFactory.getLogger(CadiProps.class); + + private String cadiFileName; + + private Properties cadiProperties; + + @Autowired + public CadiProps(@Value("${aaf.cadi.file:./resources/cadi.properties}") String filename){ + cadiFileName = filename; + cadiProperties = new Properties(); + } + + @PostConstruct + public void init() throws IOException { + + File cadiFile = new File(cadiFileName); + + if(!cadiFile.exists()){ + LOGGER.warn("Unable to find the cadi file in the given path {} so loading cadi.properties from classloader", cadiFileName); + InputStream is = this.getClass().getClassLoader().getResourceAsStream("cadi.properties"); + cadiProperties.load(is); + } else { + LOGGER.info("Successfully found the file {} and started loading the properties from it", cadiFileName); + cadiFileName = cadiFile.getAbsolutePath(); + try (InputStream inputStream = new FileInputStream(cadiFile)) { + cadiProperties.load(inputStream); + } + + } + } + public String getCadiFileName() { + return cadiFileName; + } + public Properties getCadiProperties(){ + return cadiProperties; + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/FilterPriority.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/FilterPriority.java new file mode 100644 index 00000000..17b9f0e4 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/FilterPriority.java @@ -0,0 +1,39 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ +package org.onap.aai.aaf.filters; + +import org.springframework.core.Ordered; + +public enum FilterPriority { + + AAF_AUTHENTICATION(Ordered.HIGHEST_PRECEDENCE), + AAF_AUTHORIZATION(Ordered.HIGHEST_PRECEDENCE + 1), //higher number = lower priority + AAF_CERT_AUTHENTICATION(Ordered.HIGHEST_PRECEDENCE + 2 ), + AAF_CERT_AUTHORIZATION(Ordered.HIGHEST_PRECEDENCE + 3), + TWO_WAY_SSL_AUTH(Ordered.HIGHEST_PRECEDENCE + 4); + + private final int priority; + + FilterPriority(final int p) { + priority = p; + } + + public int getPriority() { return priority; } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/GremlinFilter.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/GremlinFilter.java new file mode 100644 index 00000000..23ff0c6d --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/GremlinFilter.java @@ -0,0 +1,99 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ +package org.onap.aai.aaf.filters; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.commons.io.IOUtils; +import org.onap.aai.aaf.auth.ResponseFormatter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; + +@Component +@Profile({ + AafProfiles.AAF_CERT_AUTHENTICATION, + AafProfiles.AAF_AUTHENTICATION +}) +public class GremlinFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(GremlinFilter.class); + + private static final String ADVANCED = "advanced"; + private static final String BASIC = "basic"; + private static final Pattern ECHO_ENDPOINT = Pattern.compile("^.*/util/echo$"); + + String type; + + String instance; + + private CadiProps cadiProps; + + @Autowired + public GremlinFilter( + @Value("${permission.type}") String type, + @Value("${permission.instance}") String instance, + CadiProps cadiProps + ) { + this.type = type; + this.instance = instance; + this.cadiProps = cadiProps; + } + + public void doBasicAuthFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + PayloadBufferingRequestWrapper requestBufferWrapper = new PayloadBufferingRequestWrapper(request); + + if(ECHO_ENDPOINT.matcher(request.getRequestURI()).matches()){ + filterChain.doFilter(requestBufferWrapper, response); + } + + String payload = IOUtils.toString(requestBufferWrapper.getInputStream(), StandardCharsets.UTF_8.name()); + boolean containsWordGremlin = payload.contains("\"gremlin\""); + + //if the requestBufferWrapper contains the word "gremlin" it's an "advanced" query needing an "advanced" role + String permissionBasic = String.format("%s|%s|%s", type, instance, BASIC); + String permissionAdvanced = String.format("%s|%s|%s", type, instance, ADVANCED); + + boolean isAuthorized; + + if(containsWordGremlin){ + isAuthorized = requestBufferWrapper.isUserInRole(permissionAdvanced); + }else{ + isAuthorized = requestBufferWrapper.isUserInRole(permissionAdvanced) || requestBufferWrapper.isUserInRole(permissionBasic); + } + + if(!isAuthorized){ + String name = requestBufferWrapper.getUserPrincipal() != null ? requestBufferWrapper.getUserPrincipal().getName() : "unknown"; + LOGGER.info("User " + name + " does not have a role for " + (containsWordGremlin ? "gremlin" : "non-gremlin") + " query" ); + ResponseFormatter.errorResponse(request, response); + } else { + filterChain.doFilter(requestBufferWrapper,response); + } + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/PayloadBufferingRequestWrapper.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/PayloadBufferingRequestWrapper.java new file mode 100644 index 00000000..eb8a61dd --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/PayloadBufferingRequestWrapper.java @@ -0,0 +1,50 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2018 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ +package org.onap.aai.aaf.filters; + +import org.apache.commons.io.IOUtils; +import org.onap.aaf.cadi.BufferedServletInputStream; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +/** + * This class buffers the payload of the servlet request. The reason is that we access the payload multiple times, + * which is not supported by the request per se. + */ + +class PayloadBufferingRequestWrapper extends HttpServletRequestWrapper { + + private byte[] buffer; + + PayloadBufferingRequestWrapper(HttpServletRequest req) throws IOException { + super(req); + this.buffer = IOUtils.toByteArray(req.getInputStream()); + } + + @Override + public ServletInputStream getInputStream() { + ByteArrayInputStream bais = new ByteArrayInputStream(this.buffer); + return new BufferedServletInputStream(bais); + } +} diff --git a/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/TwoWaySslAuthorization.java b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/TwoWaySslAuthorization.java new file mode 100644 index 00000000..0e206c50 --- /dev/null +++ b/aai-aaf-auth/src/main/java/org/onap/aai/aaf/filters/TwoWaySslAuthorization.java @@ -0,0 +1,186 @@ +/** + * ============LICENSE_START======================================================= + * org.onap.aai + * ================================================================================ + * Copyright © 2017-2019 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed 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. + * ============LICENSE_END========================================================= + */ +package org.onap.aai.aaf.filters; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.onap.aai.aaf.auth.AAIAuthCore; +import org.onap.aai.aaf.auth.CertUtil; +import org.onap.aai.aaf.auth.ResponseFormatter; +import org.onap.aai.exceptions.AAIException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.filter.OrderedRequestContextFilter; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.security.auth.x500.X500Principal; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.*; + +@Component +@Profile("two-way-ssl") +public class TwoWaySslAuthorization extends OrderedRequestContextFilter { + + private static final Logger LOGGER = LoggerFactory.getLogger(TwoWaySslAuthorization.class); + + public static final String HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override"; + + public static final String MERGE_PATCH = "MERGE_PATCH"; + + @Autowired + private Environment environment; + + @Autowired + private AAIAuthCore aaiAuthCore; + + @Autowired + private CadiProps cadiProps; + + public TwoWaySslAuthorization(){ + this.setOrder(FilterPriority.TWO_WAY_SSL_AUTH.getPriority()); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + + String uri = request.getRequestURI(); + String httpMethod = getHttpMethod(request); + + Optional<String> authUser = getUser(request); + + if (authUser.isPresent()) { + Properties cadiProperties = cadiProps.getCadiProperties(); + + String issuer = CertUtil.getCertIssuer(request); + if (issuer == null || issuer.isEmpty()) { + AAIException aaie = new AAIException("AAI_9107"); + ResponseFormatter.errorResponse(aaie, request, response); + return; + } + issuer = issuer.replaceAll("\\s+","").toUpperCase(); + + List<String> cadiConfiguredIssuers = CertUtil.getCadiCertIssuers(cadiProperties); + boolean isAafAuthProfileActive = this.isAafAuthProfileActive(); + if ((!isAafAuthProfileActive) || (!cadiConfiguredIssuers.contains(issuer)) ) { + try { + this.authorize(uri, httpMethod, authUser.get(), this.getHaProxyUser(request), issuer); + } catch (AAIException e) { + ResponseFormatter.errorResponse(e, request, response); + return; + } + } + } else { + AAIException aaie = new AAIException("AAI_9107"); + ResponseFormatter.errorResponse(aaie, request, response); + return; + } + filterChain.doFilter(request, response); + } + + + private String getHttpMethod(HttpServletRequest request) { + String httpMethod = request.getMethod(); + if ("POST".equalsIgnoreCase(httpMethod) + && "PATCH".equals(request.getHeader(HTTP_METHOD_OVERRIDE))) { + httpMethod = MERGE_PATCH; + } + if (httpMethod.equalsIgnoreCase(MERGE_PATCH) || "patch".equalsIgnoreCase(httpMethod)) { + httpMethod = "PUT"; + } + return httpMethod; + } + + private Optional<String> getUser(HttpServletRequest hsr) { + String authUser = null; + if (hsr.getAttribute("javax.servlet.request.cipher_suite") != null) { + X509Certificate[] certChain = (X509Certificate[]) hsr.getAttribute("javax.servlet.request.X509Certificate"); + + /* + * If the certificate is null or the certificate chain length is zero Then + * retrieve the authorization in the request header Authorization Check that it + * is not null and that it starts with Basic and then strip the basic portion to + * get the base64 credentials Check if this is contained in the AAIBasicAuth + * Singleton class If it is, retrieve the username associated with that + * credentials and set to authUser Otherwise, get the principal from certificate + * and use that authUser + */ + + if (certChain == null || certChain.length == 0) { + + String authorization = hsr.getHeader("Authorization"); + + if (authorization != null && authorization.startsWith("Basic ")) { + authUser = authorization.replace("Basic ", ""); + } + + } else { + X509Certificate clientCert = certChain[0]; + X500Principal subjectDN = clientCert.getSubjectX500Principal(); + authUser = subjectDN.toString().toLowerCase(); + } + } + + return Optional.ofNullable(authUser); + } + + private String getHaProxyUser(HttpServletRequest hsr) { + String haProxyUser; + if (Objects.isNull(hsr.getHeader("X-AAI-SSL-Client-CN")) + || Objects.isNull(hsr.getHeader("X-AAI-SSL-Client-OU")) + || Objects.isNull(hsr.getHeader("X-AAI-SSL-Client-O")) + || Objects.isNull(hsr.getHeader("X-AAI-SSL-Client-L")) + || Objects.isNull(hsr.getHeader("X-AAI-SSL-Client-ST")) + || Objects.isNull(hsr.getHeader("X-AAI-SSL-Client-C"))) { + haProxyUser = ""; + } else { + haProxyUser = String.format("CN=%s, OU=%s, O=\"%s\", L=%s, ST=%s, C=%s", + Objects.toString(hsr.getHeader("X-AAI-SSL-Client-CN"), ""), + Objects.toString(hsr.getHeader("X-AAI-SSL-Client-OU"), ""), + Objects.toString(hsr.getHeader("X-AAI-SSL-Client-O"), ""), + Objects.toString(hsr.getHeader("X-AAI-SSL-Client-L"), ""), + Objects.toString(hsr.getHeader("X-AAI-SSL-Client-ST"), ""), + Objects.toString(hsr.getHeader("X-AAI-SSL-Client-C"), "")).toLowerCase(); + } + return haProxyUser; + } + + private void authorize(String uri, String httpMethod, String authUser, String haProxyUser, String issuer) throws AAIException { + if (!aaiAuthCore.authorize(authUser, uri, httpMethod, haProxyUser, issuer)) { + throw new AAIException("AAI_9101", "Request on " + httpMethod + " " + uri + " status is not OK"); + } + } + + private boolean isAafAuthProfileActive() { + String[] profiles = environment.getActiveProfiles(); + if (profiles != null) { + if (Arrays.stream(profiles).anyMatch( + env -> (env.equalsIgnoreCase(AafProfiles.AAF_CERT_AUTHENTICATION)))) { + return true; + } + } + return false; + } +} |