/*
* Copyright 2008-2013 EMC Corporation
* Copyright 2016 Intel Corporation
*
* 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.
*
*/
package com.emc.storageos.security.authentication;
import static com.emc.storageos.svcs.errorhandling.mappers.ServiceCodeExceptionMapper.getHTTPStatus;
import static com.emc.storageos.svcs.errorhandling.mappers.ServiceCodeExceptionMapper.getPreferedLocale;
import static com.emc.storageos.svcs.errorhandling.mappers.ServiceCodeExceptionMapper.toServiceError;
import static com.emc.storageos.svcs.errorhandling.resources.ServiceErrorFactory.toXml;
import java.io.IOException;
import java.net.URI;
import java.security.Principal;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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 javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import com.emc.storageos.cinder.CinderConstants;
import com.emc.storageos.db.client.URIUtil;
import com.emc.storageos.db.client.model.*;
import com.emc.storageos.keystone.restapi.model.response.KeystoneTenant;
import com.emc.storageos.keystone.restapi.utils.KeystoneUtils;
import com.emc.storageos.model.project.ProjectElement;
import com.emc.storageos.model.project.ProjectParam;
import com.emc.storageos.model.tenant.TenantOrgRestRep;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.emc.storageos.db.client.DbClient;
import com.emc.storageos.keystone.KeystoneConstants;
import com.emc.storageos.keystone.restapi.KeystoneApiClient;
import com.emc.storageos.keystone.restapi.KeystoneRestClientFactory;
import com.emc.storageos.keystone.restapi.model.response.AuthTokenResponse;
import com.emc.storageos.security.authorization.Role;
import com.emc.storageos.svcs.errorhandling.model.ServiceCoded;
import com.emc.storageos.svcs.errorhandling.resources.APIException;
import com.emc.storageos.svcs.errorhandling.resources.InternalException;
/**
* Base class for all the filters that need to create a Bourne understandable user principal
*/
public abstract class AbstractRequestWrapperFilter implements Filter {
private static final Logger _log = LoggerFactory.getLogger(AbstractRequestWrapperFilter.class);
private static final String OS_QUOTA_SETS = "os-quota-sets";
private static final String DEFAULTS = "defaults";
private static final String METHOD_PUT = "PUT";
@Autowired
private TokenValidator _tokenValidator;
@Autowired
private DbClient _dbClient;
@Autowired
protected StorageOSUserRepository _userRepo;
@Autowired
private KeystoneRestClientFactory _keystoneFactory;
@Autowired
private KeystoneUtils _keystoneUtils;
@Autowired
private InternalTenantServiceClient _internalTenantServiceClient;
@Context
protected HttpHeaders headers;
@Override
public void destroy() {
// nothing to do
}
public KeystoneRestClientFactory getKeystoneFactory() {
return _keystoneFactory;
}
/**
* Creates and returns StorageOSUser from the StorageOSUserDAO given
* also, populates roles for the user
*
* @param userDAO
* @param token sets the auth token for the user
* @param proxyToken sets the proxy token for the user (optional)
* @return
*/
private StorageOSUser getStorageOSUser(final StorageOSUserDAO userDAO,
final String token, final String proxyToken) {
if (userDAO == null) {
return null;
}
StorageOSUser user = _userRepo.findOne(userDAO);
if (user != null) {
user.setToken(token);
if (proxyToken != null && !proxyToken.isEmpty()) {
user.setProxyToken(proxyToken);
}
}
return user;
}
/**
*
* Looks at the tokens in a request. It will first look at the
* authentication token. - If the token is valid and considerProxyToken is
* false, return the corresponding user. - If the token is valid, and
* considerProxyToken is true, and the corresponding user has the PROXY_USER
* role, and there is a proxy token on the request, return the corresponding
* proxy user. Strip it of SECURITY_ADMIN role, and mark is as a proxy user.
*
* @param servletRequest
* @param considerProxyToken
* @return
*/
protected StorageOSUser getStorageOSUserFromRequest(
final ServletRequest servletRequest, boolean considerProxyToken) {
StorageOSUser authenticatingUser = getUserFromRequestInternal(servletRequest, false);
if (authenticatingUser == null) {
_log.debug("No token");
return null;
}
if (!considerProxyToken || !containsProxyToken(servletRequest)) {
return authenticatingUser;
} else {
if (!authenticatingUser.getRoles().contains(Role.PROXY_USER.toString())) {
_log.error("User {} does not have PROXY_USER role and is attempting to use a proxy token.",
authenticatingUser.getUserName());
throw APIException.forbidden.userCannotUseProxyTokens(authenticatingUser
.getUserName());
}
StorageOSUser proxiedUser = getUserFromRequestInternal(servletRequest, true);
if (proxiedUser == null) {
_log.error("Could not find proxied user or proxy token is invalid.");
throw APIException.unauthorized.invalidProxyToken(authenticatingUser
.getUserName());
}
_log.info("Proxy user {} running as proxied user {}",
authenticatingUser.getUserName(),
proxiedUser.getUserName());
// Remove the SECURITY_ADMIN role for the proxy user.
// Have to rebuild the role set because roles is an unmodifiable collection.
Set<String> originalRoles = proxiedUser.getRoles();
HashSet<String> newRoles = new HashSet<String>();
for (String r : originalRoles) {
if (!r.equalsIgnoreCase(Role.SECURITY_ADMIN.toString()) &&
!r.equalsIgnoreCase(Role.RESTRICTED_SECURITY_ADMIN.toString())) {
newRoles.add(r);
}
}
proxiedUser.setRoles(newRoles);
proxiedUser.setIsProxied(true);
return proxiedUser;
}
}
/**
* Retrieves token from the request header or cookie, and looks up StorageOSUser from it
*
* @param servletRequest
* @param proxyLookup: If true, will look for a proxy token. If false, look for a auth token only.
* @return StorageOSUser if a validation of the token succeeds. Otherwise null
*/
private StorageOSUser getUserFromRequestInternal(final ServletRequest servletRequest,
boolean proxyLookup) {
final HttpServletRequest req = (HttpServletRequest) servletRequest;
String authToken = req.getHeader(RequestProcessingUtils.AUTH_TOKEN_HEADER);
String proxyToken = req.getHeader(RequestProcessingUtils.AUTH_PROXY_TOKEN_HEADER);
String keystoneUserAuthToken = req.getHeader(RequestProcessingUtils.KEYSTONE_AUTH_TOKEN_HEADER);
if (authToken != null) {
if (proxyLookup) {
if (proxyToken != null) {
return getStorageOSUser(_tokenValidator.validateToken(proxyToken), authToken, proxyToken);
}
} else {
return getStorageOSUser(_tokenValidator.validateToken(authToken), authToken, null);
}
}
if (proxyLookup) {
_log.error("No token found for proxy token lookup. Returning null.");
return null;
}
if (null != keystoneUserAuthToken) {
_log.info("The request is for keystone - with token - " + keystoneUserAuthToken);
createTenantNProject(req);
return createStorageOSUserUsingKeystone(keystoneUserAuthToken);
}
// in cookies
if (req.getCookies() != null) {
for (Cookie cookie : req.getCookies()) {
if (cookie.getName().equalsIgnoreCase(RequestProcessingUtils.AUTH_TOKEN_HEADER)) {
authToken = cookie.getValue();
StorageOSUser user = getStorageOSUser(_tokenValidator.validateToken(authToken), authToken, null);
if (user != null) {
return user;
} else {
// To Do - cache invalid tokens in memory,
// just so we don't try to use them next time ?
}
}
}
}
return null;
}
/**
* Search through request URL for PUT api call updating quota. In case of finding, get OS Tenant id from the request.
* If the id is not null and Tenant with given id was not created in CoprHD before, then create the Tenant and Project.
*
* @param httpServletRequest
*/
private void createTenantNProject(HttpServletRequest httpServletRequest) {
String requestUrl = httpServletRequest.getRequestURI();
// We are looking only for PUT API call that updates quota.
if (httpServletRequest.getMethod().equals(METHOD_PUT) && requestUrl.contains(OS_QUOTA_SETS) && !requestUrl.contains(DEFAULTS)) {
// Returns target OpenStack Tenant ID from quota request, otherwise null.
String targetTenantId = getOpenstackTenantIdFromQuotaRequest(requestUrl);
// Try to create project and tenant in CoprHD when tenant ID is present.
if (targetTenantId != null) {
// Check whether Tenant already exists in CoprHD for given ID. If not, then create one.
TenantOrg coprhdTenant = _keystoneUtils.getCoprhdTenantWithOpenstackId(targetTenantId);
if (coprhdTenant == null) {
// Check whether Tenant with ID from request exists in OpenStack.
KeystoneTenant tenant = _keystoneUtils.getTenantWithId(targetTenantId);
if (tenant == null) {
throw APIException.notFound.openstackTenantNotFound(targetTenantId);
}
// Check whether Tenant with given ID is already imported to the CoprHD.
OSTenant osTenant = _keystoneUtils.findOpenstackTenantInCoprhd(targetTenantId);
if (osTenant == null) {
createTenantNProject(tenant);
}
}
}
}
}
private String getOpenstackTenantIdFromQuotaRequest(String requestUrl) {
if (requestUrl != null) {
return requestUrl.split("/")[4];
}
return null;
}
private void createTenantNProject(KeystoneTenant tenant) {
_internalTenantServiceClient.setServer(_keystoneUtils.getVIP());
// Create Tenant via internal API call.
TenantOrgRestRep tenantResp = _internalTenantServiceClient.createTenant(_keystoneUtils.prepareTenantParam(tenant));
// Create Project via internal API call.
ProjectParam projectParam = new ProjectParam(tenant.getName() + CinderConstants.PROJECT_NAME_SUFFIX);
ProjectElement projectResp = _internalTenantServiceClient.createProject(tenantResp.getId(), projectParam);
_keystoneUtils.tagProjectWithOpenstackId(projectResp.getId(), tenant.getId(), tenantResp.getId().toString());
// Creates OSTenant representation of Openstack Tenant
OSTenant osTenant = _keystoneUtils.mapToOsTenant(tenant);
osTenant.setId(URIUtil.createId(OSTenant.class));
_dbClient.createObject(osTenant);
}
private StorageOSUser createStorageOSUserUsingKeystone(String keystoneUserAuthToken) {
_log.debug("START - createStorageOSUserUsingKeystone ");
StorageOSUser osUser = null;
// Get the required AuthenticationProvider
List<URI> authProvidersUri = _dbClient.queryByType(AuthnProvider.class, true);
List<AuthnProvider> allProviders = _dbClient.queryObject(AuthnProvider.class, authProvidersUri);
AuthnProvider keystoneAuthProvider = null;
for (AuthnProvider provider : allProviders) {
if (AuthnProvider.ProvidersType.keystone.toString().equalsIgnoreCase(provider.getMode())) {
keystoneAuthProvider = provider;
break; // We are interested in keystone provider only
}
}
if (null != keystoneAuthProvider) {
// From the AuthProvider, get the, managedDn, password, server URL and the admin token
Set<String> serverUris = keystoneAuthProvider.getServerUrls();
URI baseUri = null;
for (String uri : serverUris) {
baseUri = URI.create(uri);
// Single URI will be present
break;
}
String managerDn = keystoneAuthProvider.getManagerDN();
String password = keystoneAuthProvider.getManagerPassword();
Set<String> domains = keystoneAuthProvider.getDomains();
String adminToken = keystoneAuthProvider.getKeys().get(KeystoneConstants.AUTH_TOKEN);
String userName = managerDn.split(",")[0].split("=")[1];
String tenantName = managerDn.split(",")[1].split("=")[1];
// Invoke keystone API to validate the token
KeystoneApiClient apiClient = (KeystoneApiClient) _keystoneFactory.getRESTClient(baseUri, userName, password);
apiClient.setTenantName(tenantName);
apiClient.setAuthToken(adminToken);
// From the validation result, read the user role and tenantId
AuthTokenResponse validToken = apiClient.validateUserToken(keystoneUserAuthToken);
String openstackTenantId = validToken.getAccess().getToken().getTenant().getId();
String tempDomain = "";
for (String domain : domains) {
tempDomain = domain;
userName = userName + "@" + domain;
break;// There will be a single domain
}
// convert the openstack tenant id to vipr tenant id
String viprTenantId = getViPRTenantId(openstackTenantId, tempDomain);
if (null == viprTenantId) {
_log.warn("There is no mapping for the OpenStack Tenant in ViPR");
throw APIException.notFound.openstackTenantNotFound(openstackTenantId);
}
_log.debug("Creating OSuser with userName:" + userName + " tenantId:" + viprTenantId);
osUser = new StorageOSUser(userName, viprTenantId);
// TODO - remove this once the keystone api is fixed to is_admin=1|0 based on the roles in OpenStack
osUser.addRole(Role.TENANT_ADMIN.toString());
// Map the role to ViPR role
int role_num = validToken.getAccess().getMetadata().getIs_admin();
if (role_num == 1) {
osUser.addRole(Role.TENANT_ADMIN.toString());
}
}
_log.debug("END - createStorageOSUserUsingKeystone ");
return osUser;
}
/**
* Convert openstack tenant id to ViPR tenant id
*
* @param openstackTenantId
* @return
*/
private String getViPRTenantId(String openstackTenantId, String domain) {
String tenantId = null;
List<URI> tenantOrgUris = _dbClient.queryByType(TenantOrg.class, true);
List<TenantOrg> tenantOrgList = _dbClient.queryObject(TenantOrg.class, tenantOrgUris);
boolean found = false;
for (TenantOrg singleTenant : tenantOrgList) {
StringSetMap userMappings = singleTenant.getUserMappings();
if (null != userMappings) {
StringSet mappingSet = userMappings.get(domain);
if (null != mappingSet) {
for (String str : mappingSet) {
if (str.contains(openstackTenantId)) {
tenantId = singleTenant.getId().toString();
found = true;
break;// found the required tenant
}
}
}
}
if (found) {
break;// exit outer for loop
}
}
return tenantId;
}
/*
* Convenience function to return if the request has a proxy token header
* specified or not
*
* @param request
*
* @return true if the proxy token header is present
*/
private boolean containsProxyToken(ServletRequest request) {
final HttpServletRequest req = (HttpServletRequest) request;
return req.getHeader(RequestProcessingUtils.AUTH_PROXY_TOKEN_HEADER) == null ? false : true;
}
@Override
public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final HttpServletRequest request = (HttpServletRequest) servletRequest;
try {
final AbstractRequestWrapper reqWrapper = authenticate(servletRequest);
filterChain.doFilter(reqWrapper, servletResponse);
} catch (APIException e) {
_log.debug("unauthorized request: serviceUrl = " + request.getRequestURI(), e);
response.sendError(toHTTPStatus(e), toServiceErrorXml(e));
return;
} catch (final InternalException e) {
response.sendError(toHTTPStatus(e), toServiceErrorXml(e));
return;
}
}
/**
* Method to be overloaded to extract Principal information from request context
*
* @param servletRequest
* @return AbstractRequestWrapper
* @throws InternalException
*/
protected abstract AbstractRequestWrapper authenticate(
final ServletRequest servletRequest);
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
// nothing to do
}
protected String toServiceErrorXml(final Exception e) {
return toXml(toServiceError(e, getPreferedLocale(headers)));
}
protected int toHTTPStatus(final ServiceCoded e) {
return getHTTPStatus(e).getStatusCode();
}
protected int toHTTPStatus(final WebApplicationException e) {
return getHTTPStatus(e).getStatusCode();
}
final public class AbstractRequestWrapper extends HttpServletRequestWrapper {
private final Principal principal;
public AbstractRequestWrapper(final HttpServletRequest request,
final Principal principal) {
super(request);
this.principal = principal;
}
@Override
public Principal getUserPrincipal() {
return this.principal;
}
@Override
public String getRemoteUser() {
return principal != null ? this.principal.getName() : null;
}
@Override
public boolean isUserInRole(final String role) {
return false;
}
}
}