/* * 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.ambari.server.security.authorization; import java.io.IOException; import java.security.Principal; import java.util.EnumSet; import java.util.regex.Pattern; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.ambari.server.audit.AuditLogger; import org.apache.ambari.server.audit.event.AccessUnauthorizedAuditEvent; import org.apache.ambari.server.audit.event.AuditEvent; import org.apache.ambari.server.audit.event.LoginAuditEvent; import org.apache.ambari.server.configuration.Configuration; import org.apache.ambari.server.orm.entities.PermissionEntity; import org.apache.ambari.server.orm.entities.PrivilegeEntity; import org.apache.ambari.server.security.AmbariEntryPoint; import org.apache.ambari.server.security.authorization.internal.InternalAuthenticationToken; import org.apache.ambari.server.utils.RequestUtils; import org.apache.ambari.server.view.ViewRegistry; import org.apache.commons.lang.StringUtils; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; public class AmbariAuthorizationFilter implements Filter { private static final String REALM_PARAM = "realm"; private static final String DEFAULT_REALM = "AuthFilter"; private static final String INTERNAL_TOKEN_HEADER = "X-Internal-Token"; private static final Pattern STACK_ADVISOR_REGEX = Pattern.compile("/api/v[0-9]+/stacks/[^/]+/versions/[^/]+/(validations|recommendations).*"); public static final String API_VERSION_PREFIX = "/api/v[0-9]+"; public static final String VIEWS_CONTEXT_PATH_PREFIX = "/views/"; private static final String VIEWS_CONTEXT_PATH_PATTERN = VIEWS_CONTEXT_PATH_PREFIX + "([^/]+)/([^/]+)/([^/]+)(.*)"; private static final String VIEWS_CONTEXT_ALL_PATTERN = VIEWS_CONTEXT_PATH_PREFIX + ".*"; private static final String API_USERS_ALL_PATTERN = API_VERSION_PREFIX + "/users.*"; private static final String API_PRIVILEGES_ALL_PATTERN = API_VERSION_PREFIX + "/privileges.*"; private static final String API_GROUPS_ALL_PATTERN = API_VERSION_PREFIX + "/groups.*"; private static final String API_CLUSTERS_PATTERN = API_VERSION_PREFIX + "/clusters/(\\w+/?)?"; private static final String API_WIDGET_LAYOUTS_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/widget_layouts.*?"; private static final String API_CLUSTERS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters.*"; private static final String API_VIEWS_ALL_PATTERN = API_VERSION_PREFIX + "/views.*"; private static final String API_PERSIST_ALL_PATTERN = API_VERSION_PREFIX + "/persist.*"; private static final String API_LDAP_SYNC_EVENTS_ALL_PATTERN = API_VERSION_PREFIX + "/ldap_sync_events.*"; private static final String API_CREDENTIALS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/credentials.*"; private static final String API_CREDENTIALS_AMBARI_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/credentials/ambari\\..*"; private static final String API_CLUSTER_REQUESTS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/requests.*"; private static final String API_CLUSTER_SERVICES_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/services.*"; private static final String API_CLUSTER_ALERT_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/alert.*"; private static final String API_CLUSTER_HOSTS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/hosts.*"; private static final String API_CLUSTER_CONFIGURATIONS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/configurations.*"; private static final String API_CLUSTER_COMPONENTS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/components.*"; private static final String API_CLUSTER_HOST_COMPONENTS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/host_components.*"; private static final String API_CLUSTER_CONFIG_GROUPS_ALL_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/config_groups.*"; private static final String API_STACK_VERSIONS_PATTERN = API_VERSION_PREFIX + "/stacks/.*?/versions/.*"; private static final String API_HOSTS_ALL_PATTERN = API_VERSION_PREFIX + "/hosts.*"; private static final String API_ALERT_TARGETS_ALL_PATTERN = API_VERSION_PREFIX + "/alert_targets.*"; private static final String API_BOOTSTRAP_PATTERN_ALL = API_VERSION_PREFIX + "/bootstrap.*"; private static final String API_REQUESTS_ALL_PATTERN = API_VERSION_PREFIX + "/requests.*"; private static final String API_CLUSTERS_UPGRADES_PATTERN = API_VERSION_PREFIX + "/clusters/.*?/upgrades.*"; protected static final String LOGIN_REDIRECT_BASE = "/#/login?targetURI="; /** * The Ambari authentication entry point */ private final AmbariEntryPoint entryPoint; /** * Access to Ambari configuration data */ private final Configuration configuration; /** * Access to user information */ private final Users users; /** * The audit logger */ private final AuditLogger auditLogger; /** * A Permission Helper used to provided inforamtion for the Audit Logger */ private final PermissionHelper permissionHelper; /** * The realm to use for the basic http auth */ private String realm; /** * Constructor. * * @param entryPoint the authentication entrypoint * @param configuration the Ambari configuration * @param users Ambari user access * @param auditLogger the Audit logger * @param permissionHelper the permission helper */ public AmbariAuthorizationFilter(AmbariEntryPoint entryPoint, Configuration configuration, Users users, AuditLogger auditLogger, PermissionHelper permissionHelper) { this.entryPoint = entryPoint; this.configuration = configuration; this.users = users; this.auditLogger = auditLogger; this.permissionHelper = permissionHelper; } @Override public void init(FilterConfig filterConfig) throws ServletException { realm = getParameterValue(filterConfig, REALM_PARAM, DEFAULT_REALM); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String requestURI = httpRequest.getRequestURI(); SecurityContext context = getSecurityContext(); Authentication authentication = context.getAuthentication(); AuditEvent auditEvent = null; // If no explicit authenticated user is set, set it to the default user (if one is specified) if (authentication == null || authentication instanceof AnonymousAuthenticationToken) { Authentication defaultAuthentication = getDefaultAuthentication(); if (defaultAuthentication != null) { context.setAuthentication(defaultAuthentication); authentication = defaultAuthentication; } } if (authentication == null || authentication instanceof AnonymousAuthenticationToken || !authentication.isAuthenticated()) { String token = httpRequest.getHeader(INTERNAL_TOKEN_HEADER); if (token != null) { InternalAuthenticationToken internalAuthenticationToken = new InternalAuthenticationToken(token); context.setAuthentication(internalAuthenticationToken); if(auditLogger.isEnabled()) { LoginAuditEvent loginAuditEvent = LoginAuditEvent.builder() .withUserName(internalAuthenticationToken.getName()) .withRemoteIp(RequestUtils.getRemoteAddress(httpRequest)) .withRoles(permissionHelper.getPermissionLabels(authentication)) .withTimestamp(System.currentTimeMillis()).build(); auditLogger.log(loginAuditEvent); } } else { // for view access, we should redirect to the Ambari login if (requestURI.matches(VIEWS_CONTEXT_ALL_PATTERN)) { String queryString = httpRequest.getQueryString(); String requestedURL = queryString == null ? requestURI : (requestURI + '?' + queryString); String redirectURL = httpResponse.encodeRedirectURL(LOGIN_REDIRECT_BASE + requestedURL); httpResponse.sendRedirect(redirectURL); } else { entryPoint.commence(httpRequest, httpResponse, new AuthenticationCredentialsNotFoundException("Missing authentication token")); } return; } } else if (!authorizationPerformedInternally(requestURI)) { boolean authorized = false; if (requestURI.matches(API_BOOTSTRAP_PATTERN_ALL)) { authorized = AuthorizationHelper.isAuthorized(authentication, ResourceType.CLUSTER, null, EnumSet.of(RoleAuthorization.HOST_ADD_DELETE_HOSTS)); } else { for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) { if (grantedAuthority instanceof AmbariGrantedAuthority) { AmbariGrantedAuthority ambariGrantedAuthority = (AmbariGrantedAuthority) grantedAuthority; PrivilegeEntity privilegeEntity = ambariGrantedAuthority.getPrivilegeEntity(); Integer permissionId = privilegeEntity.getPermission().getId(); // admin has full access if (permissionId.equals(PermissionEntity.AMBARI_ADMINISTRATOR_PERMISSION)) { authorized = true; break; } // clusters require permission if (!"GET".equalsIgnoreCase(httpRequest.getMethod()) && requestURI.matches(API_CREDENTIALS_AMBARI_PATTERN)) { // Only the administrator can operate on credentials where the alias starts with "ambari." if (permissionId.equals(PermissionEntity.AMBARI_ADMINISTRATOR_PERMISSION)) { authorized = true; break; } } else if (requestURI.matches(API_CLUSTERS_ALL_PATTERN)) { if (permissionId.equals(PermissionEntity.CLUSTER_USER_PERMISSION) || permissionId.equals(PermissionEntity.CLUSTER_ADMINISTRATOR_PERMISSION)) { authorized = true; break; } } else if (STACK_ADVISOR_REGEX.matcher(requestURI).matches()) { //TODO permissions model doesn't manage stacks api, but we need access to stack advisor to save configs if (permissionId.equals(PermissionEntity.CLUSTER_USER_PERMISSION) || permissionId.equals(PermissionEntity.CLUSTER_ADMINISTRATOR_PERMISSION)) { authorized = true; break; } } else if (requestURI.matches(API_VIEWS_ALL_PATTERN)) { // views require permission if (permissionId.equals(PermissionEntity.VIEW_USER_PERMISSION)) { authorized = true; break; } } } } // Allow all GETs that are not LDAP sync events... authorized = authorized || (httpRequest.getMethod().equals("GET") && !requestURI.matches(API_LDAP_SYNC_EVENTS_ALL_PATTERN)); } if (!authorized) { if(auditLogger.isEnabled()) { auditEvent = AccessUnauthorizedAuditEvent.builder() .withHttpMethodName(httpRequest.getMethod()) .withRemoteIp(RequestUtils.getRemoteAddress(httpRequest)) .withResourcePath(httpRequest.getRequestURI()) .withUserName(AuthorizationHelper.getAuthenticatedName()) .withTimestamp(System.currentTimeMillis()) .build(); auditLogger.log(auditEvent); } httpResponse.setHeader("WWW-Authenticate", "Basic realm=\"" + realm + "\""); httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "You do not have permissions to access this resource."); httpResponse.flushBuffer(); return; } } if (AuthorizationHelper.getAuthenticatedName() != null) { httpResponse.setHeader("User", AuthorizationHelper.getAuthenticatedName()); if (auditLogger.isEnabled() && httpResponse.getStatus() == HttpServletResponse.SC_FORBIDDEN) { auditEvent = AccessUnauthorizedAuditEvent.builder() .withHttpMethodName(httpRequest.getMethod()) .withRemoteIp(RequestUtils.getRemoteAddress(httpRequest)) .withResourcePath(httpRequest.getRequestURI()) .withUserName(AuthorizationHelper.getAuthenticatedName()) .withTimestamp(System.currentTimeMillis()) .build(); auditLogger.log(auditEvent); } } chain.doFilter(request, response); } /** * Creates the default Authentication if a default user is configured * * @return an Authentication representing the default user */ private Authentication getDefaultAuthentication() { Authentication defaultUser = null; if ((configuration != null) && (users != null)) { String username = configuration.getDefaultApiAuthenticatedUser(); if (!StringUtils.isEmpty(username)) { final User user = users.getUser(username, UserType.LOCAL); if (user != null) { Principal principal = new Principal() { @Override public String getName() { return user.getUserName(); } }; defaultUser = new UsernamePasswordAuthenticationToken(principal, null, users.getUserAuthorities(user.getUserName(), user.getUserType())); } } } return defaultUser; } /** * Tests the URI to determine if authorization checks are performed internally or should be * performed in the filter. * * @param requestURI the request uri * @return true if handled internally; otherwise false */ private boolean authorizationPerformedInternally(String requestURI) { return requestURI.matches(API_USERS_ALL_PATTERN) || requestURI.matches(API_REQUESTS_ALL_PATTERN) || requestURI.matches(API_GROUPS_ALL_PATTERN) || requestURI.matches(API_CREDENTIALS_ALL_PATTERN) || requestURI.matches(API_PRIVILEGES_ALL_PATTERN) || requestURI.matches(API_CLUSTER_REQUESTS_ALL_PATTERN) || requestURI.matches(API_CLUSTER_SERVICES_ALL_PATTERN) || requestURI.matches(API_CLUSTER_ALERT_ALL_PATTERN) || requestURI.matches(API_CLUSTERS_PATTERN) || requestURI.matches(API_STACK_VERSIONS_PATTERN) || requestURI.matches(API_VIEWS_ALL_PATTERN) || requestURI.matches(VIEWS_CONTEXT_PATH_PATTERN) || requestURI.matches(API_WIDGET_LAYOUTS_PATTERN) || requestURI.matches(API_CLUSTER_HOSTS_ALL_PATTERN) || requestURI.matches(API_CLUSTER_CONFIGURATIONS_ALL_PATTERN) || requestURI.matches(API_CLUSTER_COMPONENTS_ALL_PATTERN) || requestURI.matches(API_CLUSTER_HOST_COMPONENTS_ALL_PATTERN) || requestURI.matches(API_CLUSTER_CONFIG_GROUPS_ALL_PATTERN) || requestURI.matches(API_HOSTS_ALL_PATTERN) || requestURI.matches(API_ALERT_TARGETS_ALL_PATTERN) || requestURI.matches(API_PRIVILEGES_ALL_PATTERN) || requestURI.matches(API_PERSIST_ALL_PATTERN) || requestURI.matches(API_CLUSTERS_UPGRADES_PATTERN); } @Override public void destroy() { // do nothing } // ----- helper methods ---------------------------------------------------- /** * Get the parameter value from the given servlet filter configuration. * * @param filterConfig the servlet configuration * @param parameterName the parameter name * @param defaultValue the default value * @return the parameter value or the default value if not set */ private static String getParameterValue( FilterConfig filterConfig, String parameterName, String defaultValue) { String value = filterConfig.getInitParameter(parameterName); if (value == null || value.length() == 0) { value = filterConfig.getServletContext().getInitParameter(parameterName); } return value == null || value.length() == 0 ? defaultValue : value; } SecurityContext getSecurityContext() { return SecurityContextHolder.getContext(); } ViewRegistry getViewRegistry() { return ViewRegistry.getInstance(); } }