diff options
Diffstat (limited to 'odl-aaa-moon/aaa-authn-federation/src')
11 files changed, 966 insertions, 0 deletions
diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java new file mode 100644 index 00000000..4ae027c8 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/Activator.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.federation; + +import java.util.Dictionary; +import org.apache.felix.dm.DependencyActivatorBase; +import org.apache.felix.dm.DependencyManager; +import org.opendaylight.aaa.api.ClaimAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.TokenStore; +import org.osgi.framework.BundleContext; +import org.osgi.framework.Constants; +import org.osgi.service.cm.ManagedService; + +/** + * An activator for the secure token server to inject in a + * <code>CredentialAuth</code> implementation. + * + * @author liemmn + * + */ +public class Activator extends DependencyActivatorBase { + private static final String FEDERATION_PID = "org.opendaylight.aaa.federation"; + + @Override + public void init(BundleContext context, DependencyManager manager) throws Exception { + manager.add(createComponent() + .setImplementation(ServiceLocator.getInstance()) + .add(createServiceDependency().setService(TokenStore.class).setRequired(true)) + .add(createServiceDependency().setService(IdMService.class).setRequired(true)) + .add(createServiceDependency().setService(ClaimAuth.class).setRequired(false) + .setCallbacks("claimAuthAdded", "claimAuthRemoved"))); + context.registerService(ManagedService.class, FederationConfiguration.instance(), + addPid(FederationConfiguration.defaults)); + } + + @Override + public void destroy(BundleContext context, DependencyManager manager) throws Exception { + } + + private Dictionary<String, ?> addPid(Dictionary<String, String> dict) { + dict.put(Constants.SERVICE_PID, FEDERATION_PID); + return dict; + } +} diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java new file mode 100644 index 00000000..10a1277d --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ClaimAuthFilter.java @@ -0,0 +1,249 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.federation; + +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; +import static org.opendaylight.aaa.federation.FederationEndpoint.AUTH_CLAIM; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.ClaimAuth; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A generic {@link Filter} for {@link ClaimAuth} implementations. + * <p> + * This filter trusts any authentication metadata bound to a request. A request + * with fake authentication claims could be forged by an attacker and submitted + * to one of the Connector ports the engine is listening on and we would blindly + * accept the forged information in this filter. Therefore it is vital we only + * accept authentication claims from a trusted proxy. It is incumbent upon the + * site administrator to dedicate specific connector ports on which previously + * authenticated requests from a trusted proxy will be sent to and to assure + * only a trusted proxy can connect to that port. The site administrator must + * enumerate those ports in the configuration. We reject any request which did + * not originate on one of the configured secure proxy ports. + * + * @author liemmn + * + */ +public class ClaimAuthFilter implements Filter { + private static final Logger LOG = LoggerFactory.getLogger(ClaimAuthFilter.class); + + private static final String CGI_AUTH_TYPE = "AUTH_TYPE"; + private static final String CGI_PATH_INFO = "PATH_INFO"; + private static final String CGI_PATH_TRANSLATED = "PATH_TRANSLATED"; + private static final String CGI_QUERY_STRING = "QUERY_STRING"; + private static final String CGI_REMOTE_ADDR = "REMOTE_ADDR"; + private static final String CGI_REMOTE_HOST = "REMOTE_HOST"; + private static final String CGI_REMOTE_PORT = "REMOTE_PORT"; + private static final String CGI_REMOTE_USER = "REMOTE_USER"; + private static final String CGI_REMOTE_USER_GROUPS = "REMOTE_USER_GROUPS"; + private static final String CGI_REQUEST_METHOD = "REQUEST_METHOD"; + private static final String CGI_SCRIPT_NAME = "SCRIPT_NAME"; + private static final String CGI_SERVER_PROTOCOL = "SERVER_PROTOCOL"; + + static final String UNAUTHORIZED_PORT_ERR = "Unauthorized proxy port"; + + @Override + public void init(FilterConfig fc) throws ServletException { + } + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) + throws IOException, ServletException { + Set<Integer> secureProxyPorts; + int localPort; + + // Check to see if we are communicated over an authorized port or not + secureProxyPorts = FederationConfiguration.instance().secureProxyPorts(); + localPort = req.getLocalPort(); + if (!secureProxyPorts.contains(localPort)) { + ((HttpServletResponse) resp).sendError(SC_UNAUTHORIZED, UNAUTHORIZED_PORT_ERR); + return; + } + + // Let's do some transformation! + List<ClaimAuth> claimAuthCollection = ServiceLocator.getInstance().getClaimAuthCollection(); + for (ClaimAuth ca : claimAuthCollection) { + Claim claim = ca.transform(claims((HttpServletRequest) req)); + if (claim != null) { + req.setAttribute(AUTH_CLAIM, claim); + // No need to do further transformation since it has been done + break; + } + } + chain.doFilter(req, resp); + } + + // Extract attributes and headers out of the request + private Map<String, Object> claims(HttpServletRequest req) { + String name; + Object objectValue; + String stringValue; + Map<String, Object> claims = new HashMap<>(); + + /* + * Tomcat has a bug/feature, not all attributes are enumerated by + * getAttributeNames() therefore getAttributeNames() cannot be used to + * obtain the full set of attributes. However if you know the name of + * the attribute a priori you can call getAttribute() and obtain the + * value. Therefore we maintain a list of attribute names + * (httpAttributes) which will be used to call getAttribute() with so we + * don't miss essential attributes. + * + * This is the Tomcat bug, note it is marked WONTFIX. Bug 25363 - + * request.getAttributeNames() not working properly Status: RESOLVED + * WONTFIX https://issues.apache.org/bugzilla/show_bug.cgi?id=25363 + * + * The solution adopted by Tomcat is to document the behavior in the + * "The Apache Tomcat Connector - Reference Guide" under the JkEnvVar + * property where is says: + * + * You can retrieve the variables on Tomcat as request attributes via + * request.getAttribute(attributeName). Note that the variables send via + * JkEnvVar will not be listed in request.getAttributeNames(). + */ + + // Capture attributes which can be enumerated ... + @SuppressWarnings("unchecked") + Enumeration<String> attrs = req.getAttributeNames(); + while (attrs.hasMoreElements()) { + name = attrs.nextElement(); + objectValue = req.getAttribute(name); + if (objectValue instanceof String) { + // metadata might be i18n, assume UTF8 and decode + stringValue = decodeUTF8((String) objectValue); + objectValue = stringValue; + } + claims.put(name, objectValue); + } + + // Capture specific attributes which cannot be enumerated ... + for (String attr : FederationConfiguration.instance().httpAttributes()) { + name = attr; + objectValue = req.getAttribute(name); + if (objectValue instanceof String) { + // metadata might be i18n, assume UTF8 and decode + stringValue = decodeUTF8((String) objectValue); + objectValue = stringValue; + } + claims.put(name, objectValue); + } + + /* + * In general we should not utilize HTTP headers as validated security + * assertions because they are too easy to forge. Therefore in general + * we don't include HTTP headers, however in certain circumstances + * specific headers may be acceptable, thus we permit an admin to + * configure the capture of specific headers. + */ + for (String header : FederationConfiguration.instance().httpHeaders()) { + claims.put(header, req.getHeader(header)); + } + + // Capture standard CGI variables... + claims.put(CGI_AUTH_TYPE, req.getAuthType()); + claims.put(CGI_PATH_INFO, req.getPathInfo()); + claims.put(CGI_PATH_TRANSLATED, req.getPathTranslated()); + claims.put(CGI_QUERY_STRING, req.getQueryString()); + claims.put(CGI_REMOTE_ADDR, req.getRemoteAddr()); + claims.put(CGI_REMOTE_HOST, req.getRemoteHost()); + claims.put(CGI_REMOTE_PORT, req.getRemotePort()); + // remote user might be i18n, assume UTF8 and decode + claims.put(CGI_REMOTE_USER, decodeUTF8(req.getRemoteUser())); + claims.put(CGI_REMOTE_USER_GROUPS, req.getAttribute(CGI_REMOTE_USER_GROUPS)); + claims.put(CGI_REQUEST_METHOD, req.getMethod()); + claims.put(CGI_SCRIPT_NAME, req.getServletPath()); + claims.put(CGI_SERVER_PROTOCOL, req.getProtocol()); + + if (LOG.isDebugEnabled()) { + LOG.debug("ClaimAuthFilter claims = {}", claims.toString()); + } + + return claims; + } + + /** + * Decode from UTF-8, return Unicode. + * + * If we're unable to UTF-8 decode the string the fallback is to return the + * string unmodified and log a warning. + * + * Some data, especially metadata attached to a user principal may be + * internationalized (i18n). The classic examples are the user's name, + * location, organization, etc. We need to be able to read this metadata and + * decode it into unicode characters so that we properly handle i18n string + * values. + * + * One of the the prolems is we often don't know the encoding (i.e. charset) + * of the string. RFC-5987 is supposed to define how non-ASCII values are + * transmitted in HTTP headers, this is a follow on from the work in + * RFC-2231. However at the time of this writing these RFC's are not + * implemented in the Servlet Request classes. Not only are these RFC's + * unimplemented but they are specific to HTTP headers, much of our metadata + * arrives via attributes as opposed to being in a header. + * + * Note: ASCII encoding is a subset of UTF-8 encoding therefore any strings + * which are pure ASCII will decode from UTF-8 just fine. However on the + * other hand Latin-1 (ISO-8859-1) encoding is not compatible with UTF-8 for + * code points in the range 128-255 (i.e. beyond 7-bit ascii). ISO-8859-1 is + * the default encoding for HTTP and HTML 4, however the consensus is the + * use of ISO-8859-1 was a mistake and Unicode with UTF-8 encoding is now + * the norm. If a string value is transmitted encoded in ISO-8859-1 + * contaiing code points in the range 128-255 and we try to UTF-8 decode it + * it will either not be the correct decoded string or it will throw a + * decoding exception. + * + * Conventional practice at the moment is for the sending side to encode + * internationalized values in UTF-8 with the receving end decoding the + * value back from UTF-8. We do not expect the use of ISO-8859-1 on these + * attributes. However due to peculiarities of the Java String + * implementation we have to specify the raw bytes are encoded in ISO-8859-1 + * just to get back the raw bytes to be able to feed into the UTF-8 decoder. + * This doesn't seem right but it is because we need the full 8-bit byte and + * the only way to say "unmodified 8-bit bytes" in Java is to call it + * ISO-8859-1. Ugh! + * + * @param string + * The input string in UTF-8 to be decoded. + * @return Unicode string + */ + private String decodeUTF8(String string) { + if (string == null) { + return null; + } + try { + return new String(string.getBytes("ISO8859-1"), "UTF-8"); + } catch (UnsupportedEncodingException e) { + LOG.warn("Unable to UTF-8 decode: ", string, e); + return string; + } + } + +} diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java new file mode 100644 index 00000000..a68dc15c --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationConfiguration.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.federation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import org.osgi.service.cm.ConfigurationException; +import org.osgi.service.cm.ManagedService; + +/** + * AAA federation configurations in OSGi. + * + * @author liemmn + * + */ +public class FederationConfiguration implements ManagedService { + private static final String FEDERATION_CONFIG_ERR = "Error saving federation configuration"; + + static final String HTTP_HEADERS = "httpHeaders"; + static final String HTTP_ATTRIBUTES = "httpAttributes"; + static final String SECURE_PROXY_PORTS = "secureProxyPorts"; + + static FederationConfiguration instance = new FederationConfiguration(); + + static final Hashtable<String, String> defaults = new Hashtable<>(); + static { + defaults.put(HTTP_HEADERS, ""); + defaults.put(HTTP_ATTRIBUTES, ""); + } + private static Map<String, String> configs = new ConcurrentHashMap<>(); + + // singleton + private FederationConfiguration() { + } + + public static FederationConfiguration instance() { + return instance; + } + + @Override + public void updated(Dictionary<String, ?> props) throws ConfigurationException { + if (props == null) { + configs.clear(); + configs.putAll(defaults); + } else { + try { + Enumeration<String> keys = props.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + configs.put(key, (String) props.get(key)); + } + } catch (Throwable t) { + throw new ConfigurationException(null, FEDERATION_CONFIG_ERR, t); + } + } + } + + public List<String> httpHeaders() { + String headers = configs.get(HTTP_HEADERS); + return (headers == null) ? new ArrayList<String>() : Arrays.asList(headers.split(" ")); + } + + public List<String> httpAttributes() { + String attributes = configs.get(HTTP_ATTRIBUTES); + return (attributes == null) ? new ArrayList<String>() : Arrays + .asList(attributes.split(" ")); + } + + public Set<Integer> secureProxyPorts() { + String ports = configs.get(SECURE_PROXY_PORTS); + Set<Integer> secureProxyPorts = new TreeSet<Integer>(); + + if (ports != null && !ports.isEmpty()) { + for (String port : ports.split(" ")) { + secureProxyPorts.add(Integer.parseInt(port)); + } + } + return secureProxyPorts; + } + +} diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java new file mode 100644 index 00000000..6ac76c0a --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/FederationEndpoint.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.federation; + +import static javax.servlet.http.HttpServletResponse.SC_CREATED; +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.oltu.oauth2.as.issuer.OAuthIssuer; +import org.apache.oltu.oauth2.as.issuer.OAuthIssuerImpl; +import org.apache.oltu.oauth2.as.issuer.UUIDValueGenerator; +import org.apache.oltu.oauth2.as.response.OAuthASResponse; +import org.apache.oltu.oauth2.common.exception.OAuthSystemException; +import org.apache.oltu.oauth2.common.message.OAuthResponse; +import org.opendaylight.aaa.AuthenticationBuilder; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.Authentication; +import org.opendaylight.aaa.api.AuthenticationException; +import org.opendaylight.aaa.api.Claim; + +/** + * An endpoint for claim-based authentication federation (in-bound). + * + * @author liemmn + * + */ +public class FederationEndpoint extends HttpServlet { + + private static final long serialVersionUID = -5553885846238987245L; + + /** An in-bound authentication claim */ + static final String AUTH_CLAIM = "AAA-CLAIM"; + + private static final String UNAUTHORIZED = "unauthorized"; + + private transient OAuthIssuer oi; + + @Override + public void init(ServletConfig config) throws ServletException { + oi = new OAuthIssuerImpl(new UUIDValueGenerator()); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, + ServletException { + try { + createRefreshToken(req, resp); + } catch (Exception e) { + error(resp, SC_UNAUTHORIZED, e.getMessage()); + } + } + + // Create a refresh token + private void createRefreshToken(HttpServletRequest req, HttpServletResponse resp) + throws OAuthSystemException, IOException { + Claim claim = (Claim) req.getAttribute(AUTH_CLAIM); + oauthRefreshTokenResponse(resp, claim); + } + + // Build OAuth refresh token response from the given claim mapped and + // injected by the external IdP + private void oauthRefreshTokenResponse(HttpServletResponse resp, Claim claim) + throws OAuthSystemException, IOException { + if (claim == null) { + throw new AuthenticationException(UNAUTHORIZED); + } + + String userName = claim.user(); + // Need to have at least a mapped username! + if (userName == null) { + throw new AuthenticationException(UNAUTHORIZED); + } + + String domain = claim.domain(); + // Need to have at least a domain! + if (domain == null) { + throw new AuthenticationException(UNAUTHORIZED); + } + + String userId = userName + "@" + domain; + + // Create an unscoped ODL context from the external claim + Authentication auth = new AuthenticationBuilder(new ClaimBuilder(claim).setUserId(userId) + .build()).setExpiration(tokenExpiration()).build(); + + // Create OAuth response + String token = oi.refreshToken(); + OAuthResponse r = OAuthASResponse + .tokenResponse(SC_CREATED) + .setRefreshToken(token) + .setExpiresIn(Long.toString(auth.expiration())) + .setScope( + // Use mapped domain if there is one, else list + // all the ones that this user has access to + (claim.domain().isEmpty()) ? listToString(ServiceLocator.getInstance() + .getIdmService().listDomains(userId)) : claim.domain()) + .buildJSONMessage(); + // Cache this token... + ServiceLocator.getInstance().getTokenStore().put(token, auth); + write(resp, r); + } + + // Token expiration + private long tokenExpiration() { + return ServiceLocator.getInstance().getTokenStore().tokenExpiration(); + } + + // Space-delimited string from a list of strings + private String listToString(List<String> list) { + StringBuffer sb = new StringBuffer(); + for (String s : list) { + sb.append(s).append(" "); + } + return sb.toString().trim(); + } + + // Emit an error OAuthResponse with the given HTTP code + private void error(HttpServletResponse resp, int httpCode, String error) { + try { + OAuthResponse r = OAuthResponse.errorResponse(httpCode).setError(error) + .buildJSONMessage(); + write(resp, r); + } catch (Exception e1) { + // Nothing to do here + } + } + + // Write out an OAuthResponse + private void write(HttpServletResponse resp, OAuthResponse r) throws IOException { + resp.setStatus(r.getResponseStatus()); + PrintWriter pw = resp.getWriter(); + pw.print(r.getBody()); + pw.flush(); + pw.close(); + } +} diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java new file mode 100644 index 00000000..dd861514 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/ServiceLocator.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.federation; + +import java.util.List; +import java.util.Vector; +import org.opendaylight.aaa.api.ClaimAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.TokenStore; + +/** + * A service locator to bridge between the web world and OSGi world. + * + * @author liemmn + * + */ +public class ServiceLocator { + + private static final ServiceLocator instance = new ServiceLocator(); + + protected volatile List<ClaimAuth> claimAuthCollection = new Vector<>(); + + protected volatile TokenStore tokenStore; + + protected volatile IdMService idmService; + + private ServiceLocator() { + } + + public static ServiceLocator getInstance() { + return instance; + } + + /** + * Called through reflection from the federation Activator + * + * @see org.opendaylight.aaa.federation.ServiceLocator + * @param ca the injected claims implementation + */ + protected void claimAuthAdded(ClaimAuth ca) { + this.claimAuthCollection.add(ca); + } + + /** + * Called through reflection from the federation Activator + * + * @see org.opendaylight.aaa.federation.Activator + * @param ca the claims implementation to remove + */ + protected void claimAuthRemoved(ClaimAuth ca) { + this.claimAuthCollection.remove(ca); + } + + public List<ClaimAuth> getClaimAuthCollection() { + return claimAuthCollection; + } + + public void setClaimAuthCollection(List<ClaimAuth> claimAuthCollection) { + this.claimAuthCollection = claimAuthCollection; + } + + public TokenStore getTokenStore() { + return tokenStore; + } + + public void setTokenStore(TokenStore tokenStore) { + this.tokenStore = tokenStore; + } + + public IdMService getIdmService() { + return idmService; + } + + public void setIdmService(IdMService idmService) { + this.idmService = idmService; + } +} diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java new file mode 100644 index 00000000..9223c6dd --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/java/org/opendaylight/aaa/federation/SssdFilter.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2014, 2015 Red Hat, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.federation; + +import java.io.IOException; +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.HttpServletRequestWrapper; + +class SssdHeadersRequest extends HttpServletRequestWrapper { + private static final String headerPrefix = "X-SSSD-"; + + public SssdHeadersRequest(HttpServletRequest request) { + super(request); + } + + public Object getAttribute(String name) { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + name); + if (headerValue != null) { + return headerValue; + } else { + return request.getAttribute(name); + } + } + + @Override + public String getRemoteUser() { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + "REMOTE_USER"); + if (headerValue != null) { + return headerValue; + } else { + return request.getRemoteUser(); + } + } + + @Override + public String getAuthType() { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + "AUTH_TYPE"); + if (headerValue != null) { + return headerValue; + } else { + return request.getAuthType(); + } + } + + @Override + public String getRemoteAddr() { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + "REMOTE_ADDR"); + if (headerValue != null) { + return headerValue; + } else { + return request.getRemoteAddr(); + } + } + + @Override + public String getRemoteHost() { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + "REMOTE_HOST"); + if (headerValue != null) { + return headerValue; + } else { + return request.getRemoteHost(); + } + } + + @Override + public int getRemotePort() { + HttpServletRequest request = (HttpServletRequest) getRequest(); + String headerValue; + + headerValue = request.getHeader(headerPrefix + "REMOTE_PORT"); + if (headerValue != null) { + return Integer.parseInt(headerValue); + } else { + return request.getRemotePort(); + } + } + +} + +/** + * Populate HttpRequestServlet API data from HTTP extension headers. + * + * When SSSD is used for authentication and identity lookup those actions occur + * in an Apache HTTP server which is fronting the servlet container. After + * successful authentication Apache will proxy the request to the container + * along with additional authentication and identity metadata. + * + * The preferred way to transport the metadata and have it appear seamlessly in + * the servlet API is via the AJP protocol. However AJP may not be available or + * desirable. An alternative method is to transport the metadata in extension + * HTTP headers. However we still want the standard servlet request API methods + * to work. Another way to say this is we do not want upper layers to be aware + * of the transport mechanism. To achieve this we wrap the HttpServletRequest + * class and override specific methods which need to extract the data from the + * extension HTTP headers. (This is roughly equivalent to what happens when AJP + * is implemented natively in the container). + * + * The extension HTTP headers are identified by the prefix "X-SSSD-". The + * overridden methods check for the existence of the appropriate extension + * header and if present returns the value found in the extension header, + * otherwise it returns the value from the method it's wrapping. + * + */ +public class SssdFilter implements Filter { + @Override + public void init(FilterConfig fc) throws ServletException { + } + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, + FilterChain filterChain) throws IOException, ServletException { + if (servletRequest instanceof HttpServletRequest) { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + SssdHeadersRequest request = new SssdHeadersRequest(httpServletRequest); + filterChain.doFilter(request, servletResponse); + } else { + filterChain.doFilter(servletRequest, servletResponse); + } + } +} diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties b/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties new file mode 100644 index 00000000..4323c04d --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.properties @@ -0,0 +1,11 @@ +org.opendaylight.aaa.federation.name = Opendaylight AAA Federation Configuration +org.opendaylight.aaa.federation.description = Configuration for AAA federation +org.opendaylight.aaa.federation.httpHeaders.name = Custom HTTP Headers +org.opendaylight.aaa.federation.httpHeaders.description = Space-delimited list of \ +specific HTTP headers to capture for authentication federation. +org.opendaylight.aaa.federation.httpAttributes.name = Custom HTTP Attributes +org.opendaylight.aaa.federation.httpAttributes.description = Space-delimited list of \ +specific HTTP attributes to capture for authentication federation. +org.opendaylight.aaa.federation.secureProxyPorts.name = Secure Proxy Ports +org.opendaylight.aaa.federation.secureProxyPorts.description = Space-delimited list of \ +port numbers on which a trusted HTTP proxy performing authentication forwards pre-authenticated requests. diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml b/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml new file mode 100644 index 00000000..e2efd3d4 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/resources/OSGI-INF/metatype/metatype.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<metatype:MetaData xmlns:metatype="http://www.osgi.org/xmlns/metatype/v1.0.0" + localization="OSGI-INF/metatype/metatype"> + <OCD id="org.opendaylight.aaa.federation" name="%org.opendaylight.aaa.federation.name" + description="%org.opendaylight.aaa.federation.description"> + <AD id="httpHeaders" type="String" default="" + name="%org.opendaylight.aaa.federation.httpHeaders.name" + description="%org.opendaylight.aaa.federation.httpHeaders.description" /> + <AD id="httpAttributes" type="String" default="" + name="%org.opendaylight.aaa.federation.httpAttributes.name" + description="%org.opendaylight.aaa.federation.httpAttributes.description" /> + <AD id="secureProxyPorts" type="String" default="" + name="%org.opendaylight.aaa.federation.secureProxyPorts.name" + description="%org.opendaylight.aaa.federation.secureProxyPorts.description" /> + </OCD> + <Designate pid="org.opendaylight.aaa.federation"> + <Object ocdref="org.opendaylight.aaa.federation" /> + </Designate> +</metatype:MetaData> diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml b/odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml new file mode 100644 index 00000000..9fd9751f --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/resources/WEB-INF/web.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" + version="3.0"> + + <servlet> + <servlet-name>federation</servlet-name> + <servlet-class>org.opendaylight.aaa.federation.FederationEndpoint</servlet-class> + <load-on-startup>1</load-on-startup> + </servlet> + <servlet-mapping> + <servlet-name>federation</servlet-name> + <url-pattern>/*</url-pattern> + </servlet-mapping> + + <!-- Federation Auth filter --> + <filter> + <filter-name>SssdFilter</filter-name> + <filter-class>org.opendaylight.aaa.federation.SssdFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>SssdFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + <filter> + <filter-name>ClaimAuthFilter</filter-name> + <filter-class>org.opendaylight.aaa.federation.ClaimAuthFilter</filter-class> + </filter> + <filter-mapping> + <filter-name>ClaimAuthFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + +</web-app> diff --git a/odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg b/odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg new file mode 100644 index 00000000..60ef1c46 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/main/resources/federation.cfg @@ -0,0 +1,3 @@ +httpHeaders= +httpAttributes= +secureProxyPorts= diff --git a/odl-aaa-moon/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java b/odl-aaa-moon/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java new file mode 100644 index 00000000..ae098652 --- /dev/null +++ b/odl-aaa-moon/aaa-authn-federation/src/test/java/org/opendaylight/aaa/federation/FederationEndpointTest.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2014, 2015 Hewlett-Packard Development Company, L.P. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +package org.opendaylight.aaa.federation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.anyMap; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.TreeSet; +import org.eclipse.jetty.testing.HttpTester; +import org.eclipse.jetty.testing.ServletTester; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.opendaylight.aaa.ClaimBuilder; +import org.opendaylight.aaa.api.Claim; +import org.opendaylight.aaa.api.ClaimAuth; +import org.opendaylight.aaa.api.IdMService; +import org.opendaylight.aaa.api.TokenStore; + +/** + * A unit test for federation endpoint. + * + * @author liemmn + * + */ +public class FederationEndpointTest { + private static final long TOKEN_TIMEOUT_SECS = 10; + private static final String CONTEXT = "/oauth2/federation"; + + private final static ServletTester server = new ServletTester(); + private static final Claim claim = new ClaimBuilder().setUser("bob").setUserId("1234") + .addRole("admin").build(); + + @BeforeClass + public static void init() throws Exception { + // Set up server + server.setContextPath(CONTEXT); + + // Add our servlet under test + server.addServlet(FederationEndpoint.class, "/*"); + + // Add ClaimAuth filter + server.addFilter(ClaimAuthFilter.class, "/*", 0); + + // Let's do dis + server.start(); + } + + @AfterClass + public static void shutdown() throws Exception { + server.stop(); + } + + @Before + public void setup() { + mockServiceLocator(); + when(ServiceLocator.getInstance().getTokenStore().tokenExpiration()).thenReturn( + TOKEN_TIMEOUT_SECS); + } + + @After + public void teardown() { + ServiceLocator.getInstance().getClaimAuthCollection().clear(); + } + + @Test + public void testFederationUnconfiguredProxyPort() throws Exception { + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setURI(CONTEXT + "/"); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(401, resp.getStatus()); + } + + @Test + @SuppressWarnings("unchecked") + public void testFederation() throws Exception { + when(ServiceLocator.getInstance().getClaimAuthCollection().get(0).transform(anyMap())) + .thenReturn(claim); + when(ServiceLocator.getInstance().getIdmService().listDomains(anyString())).thenReturn( + Arrays.asList("pepsi", "coke")); + + // Configure secure port (of zero) + FederationConfiguration.instance = mock(FederationConfiguration.class); + when(FederationConfiguration.instance.secureProxyPorts()).thenReturn( + new TreeSet<Integer>(Arrays.asList(0))); + + HttpTester req = new HttpTester(); + req.setMethod("POST"); + req.setURI(CONTEXT + "/"); + req.setVersion("HTTP/1.0"); + + HttpTester resp = new HttpTester(); + resp.parse(server.getResponses(req.generate())); + assertEquals(201, resp.getStatus()); + String content = resp.getContent(); + assertTrue(content.contains("pepsi coke")); + } + + private static void mockServiceLocator() { + ServiceLocator.getInstance().setIdmService(mock(IdMService.class)); + ServiceLocator.getInstance().setTokenStore(mock(TokenStore.class)); + ServiceLocator.getInstance().getClaimAuthCollection().add(mock(ClaimAuth.class)); + } +} |