This is an automated email from the ASF dual-hosted git repository. heneveld pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/brooklyn-server.git
commit 9ee99f441af1ecb5698d0f8993e0e031f8de088f Author: Alex Heneveld <[email protected]> AuthorDate: Thu Dec 13 16:05:15 2018 +0000 switching to security provider filter from login module fragmented partial commit with lots of potentially useful snippets --- .../brooklyn/launcher/BrooklynWebServer.java | 4 +- .../apache/brooklyn/rest/BrooklynWebConfig.java | 5 + .../filter/BrooklynSecurityProviderFilter.java | 222 +++++++++++++ .../rest/filter/EntitlementContextFilter.java | 2 +- .../brooklyn/rest/filter/GoogleOauthFilter.java | 349 +++++++++++---------- .../rest/security/jaas/BrooklynLoginModule.java | 277 +++++++++------- .../rest/security/jaas/GoogleOauthLoginModule.java | 6 +- .../security/provider/AnyoneSecurityProvider.java | 7 +- .../provider/BlackholeSecurityProvider.java | 8 +- ...klynUserWithRandomPasswordSecurityProvider.java | 5 + .../provider/DelegatingSecurityProvider.java | 41 ++- .../provider/ExplicitUsersSecurityProvider.java | 5 + .../provider/GoogleOauthSecurityProvider.java | 269 +++++++++++++++- .../security/provider/LdapSecurityProvider.java | 5 + .../rest/security/provider/SecurityProvider.java | 11 +- .../main/resources/OSGI-INF/blueprint/service.xml | 10 +- rest/rest-resources/src/main/resources/jaas.conf | 21 -- .../filter/BrooklynPropertiesSecurityFilter.java | 2 +- .../AuthenticateAnyoneSecurityProvider.java | 5 + .../security/provider/TestSecurityProvider.java | 5 + 20 files changed, 913 insertions(+), 346 deletions(-) diff --git a/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java b/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java index cd0cb00..ad50454 100644 --- a/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java +++ b/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java @@ -199,7 +199,7 @@ public class BrooklynWebServer { * {@link BrooklynLoginModule} used by default. */ @Deprecated - private Class<org.apache.brooklyn.rest.filter.BrooklynPropertiesSecurityFilter> securityFilterClazz; + private Class<org.apache.brooklyn.rest.filter.BrooklynSecurityProviderFilter> securityFilterClazz; @SetFromFlag private boolean skipSecurity = false; @@ -236,7 +236,7 @@ public class BrooklynWebServer { /** @deprecated since 0.9.0, use {@link #skipSecurity} or {@link BrooklynLoginModule} */ @Deprecated - public void setSecurityFilter(Class<org.apache.brooklyn.rest.filter.BrooklynPropertiesSecurityFilter> filterClazz) { + public void setSecurityFilter(Class<org.apache.brooklyn.rest.filter.BrooklynSecurityProviderFilter> filterClazz) { this.securityFilterClazz = filterClazz; } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynWebConfig.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynWebConfig.java index 79ca0bf..f731c79 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynWebConfig.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/BrooklynWebConfig.java @@ -48,6 +48,11 @@ public class BrooklynWebConfig { ExplicitUsersSecurityProvider.class.getCanonicalName()); public final static ConfigKey<SecurityProvider> SECURITY_PROVIDER_INSTANCE = ConfigKeys.newConfigKey(SecurityProvider.class, SECURITY_PROVIDER_CLASSNAME.getName()+".internal.instance", "instance of a pre-configured security provider"); + // TODO document + public final static ConfigKey<String> SECURITY_PROVIDER_BUNDLE = ConfigKeys.newStringConfigKey( + SECURITY_PROVIDER_CLASSNAME.getName()+".bundle.symbolicName", "bundle containing the Brooklyn SecurityProvider"); + public final static ConfigKey<String> SECURITY_PROVIDER_BUNDLE_VERSION = ConfigKeys.newStringConfigKey( + SECURITY_PROVIDER_CLASSNAME.getName()+".bundle.version", "bundle containing the Brooklyn SecurityProvider"); /** * Explicitly set the users/passwords, e.g. in brooklyn.properties: diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/BrooklynSecurityProviderFilter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/BrooklynSecurityProviderFilter.java new file mode 100644 index 0000000..264a575 --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/BrooklynSecurityProviderFilter.java @@ -0,0 +1,222 @@ +/* + * 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.brooklyn.rest.filter; + +import java.io.IOException; + +import javax.annotation.Priority; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.Context; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.rest.BrooklynWebConfig; +import org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule; +import org.apache.brooklyn.rest.security.provider.SecurityProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Provides a filter that performs authentication with the {@link SecurityProvider} + * as configured according to {@link BrooklynWebConfig#SECURITY_PROVIDER_CLASSNAME}. + * + * This replaces the JAAS {@link BrooklynLoginModule} because that login module requires + * Basic auth, which is not flexible enough to support redirect-based solutions like Oauth. + */ +@Provider +@Priority(100) +public class BrooklynSecurityProviderFilter implements ContainerRequestFilter, ContainerResponseFilter { + + @Context + HttpServletRequest webRequest; + + HttpServletResponse webResponse; + + @Context + private ContextResolver<ManagementContext> mgmtC; + + private ManagementContext mgmt() { + return mgmtC.getContext(ManagementContext.class); + } + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + SecurityProvider provider = getProvider(); + HttpSession session = webRequest.getSession(false); + + if (provider.isAuthenticated(session)) { + return; + } + + String user=null, pass=null; + webResponse.setHeader("WWW-Authenticate", "Basic realm=\"brooklyn\""); + webResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); + throw new RuntimeException("Authentication required."); +// if (provider.requiresUserPass()) { +// // TODO get basic auth +// +// } +// +// if (provider.authenticate(session, user, pass)) { +// +// } else { +// throw new RuntimeException("Authentication failed."); +// } + } + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { + // nothing needs done on the response + } + + protected SecurityProvider getProvider() { + // TODO + return null; + } + + /** + * The session attribute set for authenticated users; for reference + * (but should not be relied up to confirm authentication, as + * the providers may impose additional criteria such as timeouts, + * or a null user (no login) may be permitted) + */ + public static final String AUTHENTICATED_USER_SESSION_ATTRIBUTE = BrooklynLoginModule.AUTHENTICATED_USER_SESSION_ATTRIBUTE; + + private static final Logger log = LoggerFactory.getLogger(BrooklynSecurityProviderFilter.class); + +// private static ThreadLocal<String> originalRequest = new ThreadLocal<String>(); +// +// @Override +// public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { +// HttpServletRequest httpRequest = (HttpServletRequest) request; +// HttpServletResponse httpResponse = (HttpServletResponse) response; +// String uri = httpRequest.getRequestURI(); +// +// if (provider == null) { +// log.warn("No security provider available: disallowing web access to brooklyn"); +// httpResponse.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE); +// return; +// } +// +// if (originalRequest.get() != null) { +// // clear the entitlement context before setting to avoid warnings +// Entitlements.clearEntitlementContext(); +// } else { +// originalRequest.set(uri); +// } +// +// boolean authenticated = provider.isAuthenticated(httpRequest.getSession()); +// if ("/logout".equals(uri) || "/v1/logout".equals(uri)) { +// httpResponse.setHeader("WWW-Authenticate", "Basic realm=\"brooklyn\""); +// if (authenticated && httpRequest.getSession().getAttributeNames().hasMoreElements()) { +// logout(httpRequest); +// httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); +// } else { +// RequestDispatcher dispatcher = httpRequest.getRequestDispatcher("/"); +// log.debug("Not authenticated, forwarding request for {} to {}", uri, dispatcher); +// dispatcher.forward(httpRequest, httpResponse); +// } +// return; +// } +// +// if (!(httpRequest.getSession().getAttributeNames().hasMoreElements() && provider.isAuthenticated(httpRequest.getSession())) || +// "/logout".equals(originalRequest.get())) { +// authenticated = authenticate(httpRequest); +// } +// +// if (!authenticated) { +// httpResponse.setHeader("WWW-Authenticate", "Basic realm=\"brooklyn\""); +// httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED); +// return; +// } +// +// // Note that the attribute AUTHENTICATED_USER_SESSION_ATTRIBUTE is only set in the call to authenticate(httpRequest), +// // so must not try to get the user until that is done. +// String uid = RequestTaggingFilter.getTag(); +// String user = Strings.toString(httpRequest.getSession().getAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE)); +// try { +// WebEntitlementContext entitlementContext = new WebEntitlementContext(user, httpRequest.getRemoteAddr(), uri, uid); +// Entitlements.setEntitlementContext(entitlementContext); +// +// chain.doFilter(request, response); +// } catch (Throwable e) { +// if (!response.isCommitted()) { +// httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); +// } +// } finally { +// originalRequest.remove(); +// Entitlements.clearEntitlementContext(); +// } +// } +// +// protected boolean authenticate(HttpServletRequest request) { +// HttpSession session = request.getSession(); +// if (provider.isAuthenticated(session)) { +// return true; +// } +// session.setAttribute(BrooklynWebConfig.REMOTE_ADDRESS_SESSION_ATTRIBUTE, request.getRemoteAddr()); +// String user = null, pass = null; +// String authorization = request.getHeader("Authorization"); +// if (authorization != null) { +// String userpass = new String(Base64.decodeBase64(authorization.substring(6))); +// int idxColon = userpass.indexOf(":"); +// if (idxColon >= 0) { +// user = userpass.substring(0, idxColon); +// pass = userpass.substring(idxColon + 1); +// } else { +// return false; +// } +// } +// if (provider.authenticate(session, user, pass)) { +// if (user != null) { +// session.setAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE, user); +// } +// return true; +// } +// +// return false; +// } +// +// @Override +// public void init(FilterConfig config) throws ServletException { +// ManagementContext mgmt = OsgiCompat.getManagementContext(config.getServletContext()); +// provider = new DelegatingSecurityProvider(mgmt); +// } +// +// @Override +// public void destroy() { +// } +// +// protected void logout(HttpServletRequest request) { +// log.info("REST logging {} out of session {}", +// request.getSession().getAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE), request.getSession().getId()); +// provider.logout(request.getSession()); +// request.getSession().removeAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE); +// request.getSession().removeAttribute(BrooklynWebConfig.REMOTE_ADDRESS_SESSION_ATTRIBUTE); +// request.getSession().invalidate(); +// } + +} diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/EntitlementContextFilter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/EntitlementContextFilter.java index a039b57..c7a9a5c 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/EntitlementContextFilter.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/EntitlementContextFilter.java @@ -39,7 +39,7 @@ import org.apache.brooklyn.core.mgmt.entitlement.WebEntitlementContext; public class EntitlementContextFilter implements ContainerRequestFilter, ContainerResponseFilter { @Context private HttpServletRequest request; - + @Override public void filter(ContainerRequestContext requestContext) throws IOException { SecurityContext securityContext = requestContext.getSecurityContext(); diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/GoogleOauthFilter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/GoogleOauthFilter.java index e91733a..ab15f1a 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/GoogleOauthFilter.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/GoogleOauthFilter.java @@ -34,6 +34,10 @@ import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; import javax.ws.rs.ext.Provider; import org.apache.brooklyn.util.exceptions.Exceptions; @@ -56,7 +60,7 @@ import org.slf4j.LoggerFactory; @Provider @Priority(1) -public class GoogleOauthFilter implements Filter { +public class GoogleOauthFilter implements ContainerRequestFilter { private static final Logger log = LoggerFactory.getLogger(GoogleOauthFilter.class); @@ -80,178 +84,183 @@ public class GoogleOauthFilter implements Filter { private String callbackUri = "http://localhost.io:8081/"; private String audience = "audience"; - @Override - public void init(FilterConfig filterConfig) throws ServletException { - initializateParams(filterConfig); - } - - private void initializateParams(FilterConfig filterConfig) { - Enumeration<String> enums = filterConfig.getInitParameterNames(); - - while (enums.hasMoreElements()) { - String paramKey = enums.nextElement(); - String paramValue = filterConfig.getInitParameter(paramKey); - System.out.println(paramKey + ":" + paramValue); - switch (paramKey) { - case PARAM_URI_TOKEN_INFO: - uriTokenInfo = paramValue; - break; - case PARAM_URI_GETTOKEN: - uriGetToken = paramValue; - break; - case PARAM_URI_LOGIN_REDIRECT: - uriTokenRedirect = paramValue; - break; - case PARAM_CLIENT_ID: - clientId = paramValue; - break; - case PARAM_CLIENT_SECRET: - clientSecret = paramValue; - break; - case PARAM_CALLBACK_URI: - callbackUri = paramValue; - break; - case PARAM_AUDIENCE: - audience = paramValue; - break; - default: - System.out.println("Ignored param: " + paramKey + ":" + paramValue); - } - } - } - - @Override - public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) - throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest) req; - // Redirection from the authenticator server - String code = req.getParameter(SESSION_KEY_CODE); - - // Getting token, if exists, from the current session - String token = (String) request.getSession().getAttribute(SESSION_KEY_ACCESS_TOKEN); - - boolean continueFilterProcessing; - if (code != null && !"".equals(code)) { // in brooklyn, have - // Strings.isNonBlank(code) - continueFilterProcessing = getToken(req, resp, chain); - } else if (token == null || "".equals(token)) { // isBlank - continueFilterProcessing = redirectLogin(resp); - } else { - continueFilterProcessing = validateToken(token, resp); - } - if (continueFilterProcessing) { - chain.doFilter(req, resp); - } - } - - private boolean validateToken(String token, ServletResponse resp) throws ClientProtocolException, IOException { - // System.out.println("########################### Validating token - // ###########################"); - HashMap<String, String> params = new HashMap<String, String>(); - params.put(SESSION_KEY_ACCESS_TOKEN, token); - - String body = post(uriTokenInfo, params); - // System.out.println(body); - Map<?,?> jsonObject = null; - - // get the access token from json and request info from Google - try { - jsonObject = (Map<?,?>) Yamls.parseAll(body).iterator().next(); - log.info("Parsed '"+body+"' as "+jsonObject); - } catch (Exception e) { - Exceptions.propagateIfFatal(e); - log.info("Unable to parse: '"+body+"'"); - throw new RuntimeException("Unable to parse json " + body, e); - } - - if (!clientId.equals(jsonObject.get(audience))) { - return redirectLogin(resp); - } - // if (isTokenExpiredOrNearlySo(...) { ... } - return true; - } - - private boolean getToken(ServletRequest req, ServletResponse resp, FilterChain chain) - throws ClientProtocolException, IOException, ServletException { - String code = req.getParameter(SESSION_KEY_CODE); - - // get the access token by post to Google - HashMap<String, String> params = new HashMap<String, String>(); - params.put(SESSION_KEY_CODE, code); - params.put("client_id", clientId); - params.put("client_secret", clientSecret); - params.put("redirect_uri", callbackUri); - params.put("grant_type", "authorization_code"); - - String body = post(uriGetToken, params); - - Map<?,?> jsonObject = null; - - // get the access token from json and request info from Google - try { - jsonObject = (Map<?,?>) Yamls.parseAll(body).iterator().next(); - log.info("Parsed '"+body+"' as "+jsonObject); - } catch (Exception e) { - Exceptions.propagateIfFatal(e); - log.info("Unable to parse: '"+body+"'"); - return redirectLogin(resp); - } - - // Left token and code in session - String accessToken = (String) jsonObject.get(SESSION_KEY_ACCESS_TOKEN); - HttpServletRequest request = (HttpServletRequest) req; - request.getSession().setAttribute(SESSION_KEY_ACCESS_TOKEN, accessToken); - request.getSession().setAttribute(SESSION_KEY_CODE, code); - - // resp.getWriter().println(json); - return true; - } - - // makes a POST request to url with form parameters and returns body as a - // string - public String post(String url, Map<String, String> formParameters) throws ClientProtocolException, IOException { - HttpPost request = new HttpPost(url); - - List<NameValuePair> nvps = new ArrayList<NameValuePair>(); - for (String key : formParameters.keySet()) { - nvps.add(new BasicNameValuePair(key, formParameters.get(key))); - } - request.setEntity(new UrlEncodedFormEntity(nvps)); - - return execute(request); - } - - // makes a GET request to url and returns body as a string - public String get(String url) throws ClientProtocolException, IOException { - return execute(new HttpGet(url)); - } - - // makes request and checks response code for 200 - private String execute(HttpRequestBase request) throws ClientProtocolException, IOException { - HttpClient httpClient = new DefaultHttpClient(); - HttpResponse response = httpClient.execute(request); - - HttpEntity entity = response.getEntity(); - String body = EntityUtils.toString(entity); - - if (response.getStatusLine().getStatusCode() != 200) { - throw new RuntimeException( - "Expected 200 but got " + response.getStatusLine().getStatusCode() + ", with body " + body); - } - - return body; - } - - private boolean redirectLogin(ServletResponse response) throws IOException { - HttpServletResponse res = (HttpServletResponse) response; - res.setContentType(ContentType.APPLICATION_XML.toString()); - res.sendRedirect(uriTokenRedirect); - return false; - } +// @Override +// public void init(FilterConfig filterConfig) throws ServletException { +// log.info("GOOGLE FILTER 1"); +// initializateParams(filterConfig); +// } +// +// private void initializateParams(FilterConfig filterConfig) { +// Enumeration<String> enums = filterConfig.getInitParameterNames(); +// +// while (enums.hasMoreElements()) { +// String paramKey = enums.nextElement(); +// String paramValue = filterConfig.getInitParameter(paramKey); +// System.out.println(paramKey + ":" + paramValue); +// switch (paramKey) { +// case PARAM_URI_TOKEN_INFO: +// uriTokenInfo = paramValue; +// break; +// case PARAM_URI_GETTOKEN: +// uriGetToken = paramValue; +// break; +// case PARAM_URI_LOGIN_REDIRECT: +// uriTokenRedirect = paramValue; +// break; +// case PARAM_CLIENT_ID: +// clientId = paramValue; +// break; +// case PARAM_CLIENT_SECRET: +// clientSecret = paramValue; +// break; +// case PARAM_CALLBACK_URI: +// callbackUri = paramValue; +// break; +// case PARAM_AUDIENCE: +// audience = paramValue; +// break; +// default: +// System.out.println("Ignored param: " + paramKey + ":" + paramValue); +// } +// } +// } +// +// @Override +// public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) +// throws IOException, ServletException { +// log.info("GOOGLE FILTER 2"); +// HttpServletRequest request = (HttpServletRequest) req; +// // Redirection from the authenticator server +// String code = req.getParameter(SESSION_KEY_CODE); +// +// // Getting token, if exists, from the current session +// String token = (String) request.getSession().getAttribute(SESSION_KEY_ACCESS_TOKEN); +// +// boolean continueFilterProcessing; +// if (code != null && !"".equals(code)) { // in brooklyn, have +// // Strings.isNonBlank(code) +// continueFilterProcessing = getToken(req, resp, chain); +// } else if (token == null || "".equals(token)) { // isBlank +// continueFilterProcessing = redirectLogin(resp); +// } else { +// continueFilterProcessing = validateToken(token, resp); +// } +// if (continueFilterProcessing) { +// chain.doFilter(req, resp); +// } +// } +// +// private boolean validateToken(String token, ServletResponse resp) throws ClientProtocolException, IOException { +// log.info("GOOGLE FILTER 3"); +// // System.out.println("########################### Validating token +// // ###########################"); +// HashMap<String, String> params = new HashMap<String, String>(); +// params.put(SESSION_KEY_ACCESS_TOKEN, token); +// +// String body = post(uriTokenInfo, params); +// // System.out.println(body); +// Map<?,?> jsonObject = null; +// +// // get the access token from json and request info from Google +// try { +// jsonObject = (Map<?,?>) Yamls.parseAll(body).iterator().next(); +// log.info("Parsed '"+body+"' as "+jsonObject); +// } catch (Exception e) { +// Exceptions.propagateIfFatal(e); +// log.info("Unable to parse: '"+body+"'"); +// throw new RuntimeException("Unable to parse json " + body, e); +// } +// +// if (!clientId.equals(jsonObject.get(audience))) { +// return redirectLogin(resp); +// } +// // if (isTokenExpiredOrNearlySo(...) { ... } +// return true; +// } +// +// private boolean getToken(ServletRequest req, ServletResponse resp, FilterChain chain) +// throws ClientProtocolException, IOException, ServletException { +// String code = req.getParameter(SESSION_KEY_CODE); +// +// // get the access token by post to Google +// HashMap<String, String> params = new HashMap<String, String>(); +// params.put(SESSION_KEY_CODE, code); +// params.put("client_id", clientId); +// params.put("client_secret", clientSecret); +// params.put("redirect_uri", callbackUri); +// params.put("grant_type", "authorization_code"); +// +// String body = post(uriGetToken, params); +// +// Map<?,?> jsonObject = null; +// +// // get the access token from json and request info from Google +// try { +// jsonObject = (Map<?,?>) Yamls.parseAll(body).iterator().next(); +// log.info("Parsed '"+body+"' as "+jsonObject); +// } catch (Exception e) { +// Exceptions.propagateIfFatal(e); +// log.info("Unable to parse: '"+body+"'"); +// return redirectLogin(resp); +// } +// +// // Left token and code in session +// String accessToken = (String) jsonObject.get(SESSION_KEY_ACCESS_TOKEN); +// HttpServletRequest request = (HttpServletRequest) req; +// request.getSession().setAttribute(SESSION_KEY_ACCESS_TOKEN, accessToken); +// request.getSession().setAttribute(SESSION_KEY_CODE, code); +// +// // resp.getWriter().println(json); +// return true; +// } +// +// // makes a POST request to url with form parameters and returns body as a +// // string +// public String post(String url, Map<String, String> formParameters) throws ClientProtocolException, IOException { +// HttpPost request = new HttpPost(url); +// +// List<NameValuePair> nvps = new ArrayList<NameValuePair>(); +// for (String key : formParameters.keySet()) { +// nvps.add(new BasicNameValuePair(key, formParameters.get(key))); +// } +// request.setEntity(new UrlEncodedFormEntity(nvps)); +// +// return execute(request); +// } +// +// // makes a GET request to url and returns body as a string +// public String get(String url) throws ClientProtocolException, IOException { +// return execute(new HttpGet(url)); +// } +// +// // makes request and checks response code for 200 +// private String execute(HttpRequestBase request) throws ClientProtocolException, IOException { +// HttpClient httpClient = new DefaultHttpClient(); +// HttpResponse response = httpClient.execute(request); +// +// HttpEntity entity = response.getEntity(); +// String body = EntityUtils.toString(entity); +// +// if (response.getStatusLine().getStatusCode() != 200) { +// throw new RuntimeException( +// "Expected 200 but got " + response.getStatusLine().getStatusCode() + ", with body " + body); +// } +// +// return body; +// } +// +// private boolean redirectLogin(ServletResponse response) throws IOException { +// HttpServletResponse res = (HttpServletResponse) response; +// res.setContentType(ContentType.APPLICATION_XML.toString()); +// res.sendRedirect(uriTokenRedirect); +// return false; +// } @Override - public void destroy() { + public void filter(ContainerRequestContext requestContext) throws IOException { + log.info("GOOGLE FILTER 2"); // TODO Auto-generated method stub + } } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/BrooklynLoginModule.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/BrooklynLoginModule.java index 9765349..9374fbe 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/BrooklynLoginModule.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/BrooklynLoginModule.java @@ -60,28 +60,8 @@ import static com.google.common.base.Preconditions.checkNotNull; * JAAS module delegating authentication to the {@link SecurityProvider} implementation * configured in {@literal brooklyn.properties}, key {@literal brooklyn.webconsole.security.provider}. * <p> - * <p> - * If used in an OSGi environment only implementations visible from {@literal brooklyn-rest-server} are usable by default. - * To use a custom security provider add the following configuration to the its bundle in {@literal src/main/resources/OSGI-INF/bundle/security-provider.xml}: - * <p> - * <pre> - * {@code - * <?xml version="1.0" encoding="UTF-8"?> - * <blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0" - * xmlns:jaas="http://karaf.apache.org/xmlns/jaas/v1.1.0" - * xmlns:ext="http://aries.apache.org/blueprint/xmlns/blueprint-ext/v1.0.0"> - * - * <jaas:config name="karaf" rank="1"> - * <jaas:module className="org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule" - * flags="required"> - * brooklyn.webconsole.security.provider.symbolicName=BUNDLE_SYMBOLIC_NAME - * brooklyn.webconsole.security.provider.version=BUNDLE_VERSION - * </jaas:module> - * </jaas:config> - * - * </blueprint> - * } - * </pre> + * We have also supported configuring this as options in OSGi; + * this is now deprecated, but see {@link #initProviderFromOptions(StringConfigMap)} for more info. */ // Needs an explicit "org.apache.karaf.jaas.config" Import-Package in the manifest! public class BrooklynLoginModule implements LoginModule { @@ -171,75 +151,127 @@ public class BrooklynLoginModule implements LoginModule { @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) { - this.subject = subject; - this.callbackHandler = callbackHandler; - this.options = options; - - this.bundleContext = (BundleContext) options.get(BundleContext.class.getName()); - - loginSuccess = false; - commitSuccess = false; - - initProvider(); + try { + this.subject = subject; + this.callbackHandler = callbackHandler; + this.options = options; + + this.bundleContext = (BundleContext) options.get(BundleContext.class.getName()); + + loginSuccess = false; + commitSuccess = false; + + initProvider(); + + } catch (Exception e) { + log.warn("Unable to initialize BrooklynLoginModule: "+e, e); + Exceptions.propagateIfFatal(e); + } } private void initProvider() { + // use existing (shared) provider if there is one, for speed + // (note this login module class gets a different instance on each request; caching the provider is a big efficiency gain) StringConfigMap brooklynProperties = getManagementContext().getConfig(); provider = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE); - log.info("ALEX BR LOGIN - INIT "+provider); + if (provider != null) return; + provider = getManagementContext().getScratchpad().get(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE); + if (provider != null) return; + + initProviderFromOptions(brooklynProperties); + if (provider==null) { + // no osgi options set, so use the standard properties-based one (usual path) + provider = createDefaultSecurityProvider(getManagementContext()); + } + + log.debug("Using security provider " + provider); + } + + /** + * We have since switching to OSGi also allowed the provider to be specified as an option in the + * OSGi blueprint. This has never been used AFAIK in the real world but it was the only way to specify a bundle. + * Note that only implementations visible from {@literal brooklyn-rest-server} are usable by default. + * We now support specifying a bundle for the delegate, but this is being left in as deprecated. + * To use this <b>deprecated</b> configuration with a custom security provider, + * add the following configuration to the its bundle in {@literal src/main/resources/OSGI-INF/bundle/security-provider.xml}: + * <p> + * <pre> + * {@code + * <?xml version="1.0" encoding="UTF-8"?> + * <blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0" + * xmlns:jaas="http://karaf.apache.org/xmlns/jaas/v1.1.0" + * xmlns:ext="http://aries.apache.org/blueprint/xmlns/blueprint-ext/v1.0.0"> + * + * <jaas:config name="karaf" rank="1"> + * <jaas:module className="org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule" flags="required"> + * brooklyn.webconsole.security.provider.symbolicName=BUNDLE_SYMBOLIC_NAME + * brooklyn.webconsole.security.provider.version=BUNDLE_VERSION + * </jaas:module> + * </jaas:config> + * + * </blueprint> + * } + * </pre> + * @deprecated since 2019-01, use the brooklyn system properties + * {@link BrooklynWebConfig#SECURITY_PROVIDER_CLASSNAME}, + * {@link BrooklynWebConfig#SECURITY_PROVIDER_BUNDLE}, and + * {@link BrooklynWebConfig#SECURITY_PROVIDER_BUNDLE_VERSION}, + */ + protected void initProviderFromOptions(StringConfigMap brooklynProperties) { + // this uses *options* to determine the security provider to load + // (not sure this is ever used) String symbolicName = (String) options.get(PROPERTY_BUNDLE_SYMBOLIC_NAME); String version = (String) options.get(PROPERTY_BUNDLE_VERSION); String className = (String) options.get(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME.getName()); if (className != null && symbolicName == null) { throw new IllegalStateException("Missing JAAS module property " + PROPERTY_BUNDLE_SYMBOLIC_NAME + " pointing at the bundle where to load the security provider from."); } - if (provider != null) return; - provider = getManagementContext().getScratchpad().get(BrooklynWebConfig.SECURITY_PROVIDER_INSTANCE); - if (provider != null) return; if (symbolicName != null) { if (className == null) { className = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_CLASSNAME); } if (className != null) { - try { - Collection<Bundle> bundles = getMatchingBundles(symbolicName, version); - if (bundles.isEmpty()) { - throw new IllegalStateException("No bundle " + symbolicName + ":" + version + " found"); - } else if (bundles.size() > 1) { - log.warn("Found multiple bundles matching symbolicName " + symbolicName + " and version " + version + - " while trying to load security provider " + className + ". Will use first one that loads the class successfully."); - } - provider = tryLoadClass(className, bundles); - if (provider == null) { - throw new ClassNotFoundException("Unable to load class " + className + " from bundle " + symbolicName + ":" + version); - } - } catch (Exception e) { - Exceptions.propagateIfFatal(e); - throw new IllegalStateException("Can not load or create security provider " + className + " for bundle " + symbolicName + ":" + version, e); - } + provider = loadProviderFromBundle(getManagementContext(), bundleContext, symbolicName, version, className); } - } else { - log.debug("Delegating security provider loading to Brooklyn."); - provider = createDefaultSecurityProvider(getManagementContext()); } + } - log.debug("Using security provider " + provider); + public static SecurityProvider loadProviderFromBundle( + ManagementContext mgmt, BundleContext bundleContext, + String symbolicName, String version, String className) { + try { + Collection<Bundle> bundles = getMatchingBundles(bundleContext, symbolicName, version); + if (bundles.isEmpty()) { + throw new IllegalStateException("No bundle " + symbolicName + ":" + version + " found"); + } else if (bundles.size() > 1) { + log.warn("Found multiple bundles matching symbolicName " + symbolicName + " and version " + version + + " while trying to load security provider " + className + ". Will use first one that loads the class successfully."); + } + SecurityProvider p = tryLoadClass(mgmt, className, bundles); + if (p == null) { + throw new ClassNotFoundException("Unable to load class " + className + " from bundle " + symbolicName + ":" + version); + } + return p; + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + throw new IllegalStateException("Can not load or create security provider " + className + " for bundle " + symbolicName + ":" + version, e); + } } - private SecurityProvider tryLoadClass(String className, Collection<Bundle> bundles) + private static SecurityProvider tryLoadClass(ManagementContext mgmt, String className, Collection<Bundle> bundles) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException { for (Bundle b : bundles) { try { @SuppressWarnings("unchecked") Class<? extends SecurityProvider> securityProviderType = (Class<? extends SecurityProvider>) b.loadClass(className); - return DelegatingSecurityProvider.createSecurityProviderInstance(getManagementContext(), securityProviderType); + return DelegatingSecurityProvider.createSecurityProviderInstance(mgmt, securityProviderType); } catch (ClassNotFoundException e) { } } return null; } - private Collection<Bundle> getMatchingBundles(final String symbolicName, final String version) { + private static Collection<Bundle> getMatchingBundles(BundleContext bundleContext, final String symbolicName, final String version) { Collection<Bundle> bundles = new ArrayList<>(); for (Bundle b : bundleContext.getBundles()) { if (b.getSymbolicName().equals(symbolicName) && @@ -252,65 +284,84 @@ public class BrooklynLoginModule implements LoginModule { @Override public boolean login() throws LoginException { - if (callbackHandler == null) { - log.info("ALEX BR LOGIN - LOGIN 1"); - loginSuccess = false; - throw new FailedLoginException("Username and password not available"); - } - - NameCallback cbName = new NameCallback("Username: "); - PasswordCallback cbPassword = new PasswordCallback("Password: ", false); - - Callback[] callbacks = {cbName, cbPassword}; - log.info("ALEX BR LOGIN - LOGIN 2 - "+cbName); - try { - callbackHandler.handle(callbacks); - } catch (IOException ioe) { - throw new LoginException(ioe.getMessage()); - } catch (UnsupportedCallbackException uce) { - throw new LoginException(uce.getMessage() + " not available to obtain information from user"); - } - String user = cbName.getName(); - String password = new String(cbPassword.getPassword()); - - providerSession = new SecurityProviderHttpSession(); - - Request req = getJettyRequest(); - if (req != null) { - String remoteAddr = req.getRemoteAddr(); - providerSession.setAttribute(BrooklynWebConfig.REMOTE_ADDRESS_SESSION_ATTRIBUTE, remoteAddr); - } - - if (!provider.authenticate(providerSession, user, password)) { - loginSuccess = false; - throw new FailedLoginException("Incorrect username or password"); - } - - if (user != null) { - providerSession.setAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE, user); - } - - principals = new ArrayList<>(2); - principals.add(new UserPrincipal(user)); - // Could introduce a new interface SecurityRoleAware, implemented by - // the SecurityProviders, returning the roles a user has assigned. - // For now a static role is good enough. - String role = (String) options.get(PROPERTY_ROLE); - if (role == null) { - role = DEFAULT_ROLE; - } - if (Strings.isNonEmpty(role)) { - principals.add(new RolePrincipal(role)); + log.info("ALEX BLM login - "+callbackHandler+" "+this+" "+provider); + String user=null, password=null; + + if (provider.requiresUserPass()) { + if (callbackHandler == null) { + loginSuccess = false; + throw new FailedLoginException("Username and password not available"); + } + + NameCallback cbName = new NameCallback("Username: "); + PasswordCallback cbPassword = new PasswordCallback("Password: ", false); + + Callback[] callbacks = {cbName, cbPassword}; + + try { + callbackHandler.handle(callbacks); + } catch (IOException ioe) { + throw new LoginException(ioe.getMessage()); + } catch (UnsupportedCallbackException uce) { + throw new LoginException(uce.getMessage() + " not available to obtain information from user"); + } + user = cbName.getName(); + password = new String(cbPassword.getPassword()); + } + + + Request req = getJettyRequest(); + if (req != null) { + providerSession = req.getSession(false); + } + log.info("GOT SESSION - "+providerSession); + if (providerSession == null) { + providerSession = new SecurityProviderHttpSession(); + } + if (req != null) { + String remoteAddr = req.getRemoteAddr(); + providerSession.setAttribute(BrooklynWebConfig.REMOTE_ADDRESS_SESSION_ATTRIBUTE, remoteAddr); + } + + if (!provider.authenticate(providerSession, user, password)) { + loginSuccess = false; + throw new FailedLoginException("Incorrect username or password"); + } + + if (user != null) { + providerSession.setAttribute(AUTHENTICATED_USER_SESSION_ATTRIBUTE, user); + + principals = new ArrayList<>(2); + principals.add(new UserPrincipal(user)); + // Could introduce a new interface SecurityRoleAware, implemented by + // the SecurityProviders, returning the roles a user has assigned. + // For now a static role is good enough. + String role = (String) options.get(PROPERTY_ROLE); + if (role == null) { + role = DEFAULT_ROLE; + } + if (Strings.isNonEmpty(role)) { + principals.add(new RolePrincipal(role)); + } + } + + loginSuccess = true; + return true; + } catch (LoginException e) { + throw e; + } catch (Exception e) { + log.warn("Unexpected error during login: "+e, e); + throw e; } - loginSuccess = true; - return true; } @Override public boolean commit() throws LoginException { - log.info("ALEX BR LOGIN - COMMIT"); - if (loginSuccess) { + log.info("ALEX BLM BR LOGIN - COMMIT"); + if (loginSuccess && principals!=null && !principals.isEmpty()) { + // for oauth principals aren't set currently; they don't seem to be needed + if (subject.isReadOnly()) { throw new LoginException("Can't commit read-only subject"); } @@ -323,6 +374,7 @@ public class BrooklynLoginModule implements LoginModule { @Override public boolean abort() throws LoginException { + log.info("ALEX BLM abort"); if (loginSuccess && commitSuccess) { removePrincipal(); } @@ -348,6 +400,7 @@ public class BrooklynLoginModule implements LoginModule { } private void removePrincipal() throws LoginException { + if (principals==null || principals.isEmpty()) return; if (subject.isReadOnly()) { throw new LoginException("Read-only subject"); } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/GoogleOauthLoginModule.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/GoogleOauthLoginModule.java index 7a770cb..bd1493f 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/GoogleOauthLoginModule.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/jaas/GoogleOauthLoginModule.java @@ -95,9 +95,9 @@ public class GoogleOauthLoginModule implements LoginModule { private String callbackUri = "http://localhost.io:8081/"; private String audience = "audience"; - private static final String OAUTH2_TOKEN = "org.apache.activemq.jaas.oauth2.token"; - private static final String OAUTH2_ROLE = "org.apache.activemq.jaas.oauth2.role"; - private static final String OAUTH2_URL = "org.apache.activemq.jaas.oauth2.oauth2url"; +// private static final String OAUTH2_TOKEN = "org.apache.activemq.jaas.oauth2.token"; +// private static final String OAUTH2_ROLE = "org.apache.activemq.jaas.oauth2.role"; +// private static final String OAUTH2_URL = "org.apache.activemq.jaas.oauth2.oauth2url"; private Set<Principal> principals = new HashSet<>(); private Subject subject; private CallbackHandler callbackHandler; diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/AnyoneSecurityProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/AnyoneSecurityProvider.java index 97b4fe1..7aa9ab3 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/AnyoneSecurityProvider.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/AnyoneSecurityProvider.java @@ -20,7 +20,7 @@ package org.apache.brooklyn.rest.security.provider; import javax.servlet.http.HttpSession; -/** provider who allows everyone */ +/** provider who allows everyone, but does require a user/pass to be provided */ public class AnyoneSecurityProvider implements SecurityProvider { @Override @@ -29,6 +29,11 @@ public class AnyoneSecurityProvider implements SecurityProvider { } @Override + public boolean requiresUserPass() { + return true; + } + + @Override public boolean authenticate(HttpSession session, String user, String password) { return true; } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/BlackholeSecurityProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/BlackholeSecurityProvider.java index a976975..5418924 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/BlackholeSecurityProvider.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/BlackholeSecurityProvider.java @@ -20,7 +20,7 @@ package org.apache.brooklyn.rest.security.provider; import javax.servlet.http.HttpSession; -/** provider who disallows everyone */ +/** provider who disallows everyone, though it does require a user/pass */ public class BlackholeSecurityProvider implements SecurityProvider { @Override @@ -37,4 +37,10 @@ public class BlackholeSecurityProvider implements SecurityProvider { public boolean logout(HttpSession session) { return true; } + + @Override + public boolean requiresUserPass() { + return true; + } + } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/BrooklynUserWithRandomPasswordSecurityProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/BrooklynUserWithRandomPasswordSecurityProvider.java index 7b8e4a5..3400d0a 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/BrooklynUserWithRandomPasswordSecurityProvider.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/BrooklynUserWithRandomPasswordSecurityProvider.java @@ -70,4 +70,9 @@ public class BrooklynUserWithRandomPasswordSecurityProvider extends AbstractSecu public String toString() { return JavaClassNames.cleanSimpleClassName(this); } + + @Override + public boolean requiresUserPass() { + return true; + } } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java index 94b7894..d5bb50b 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/DelegatingSecurityProvider.java @@ -27,9 +27,11 @@ import javax.servlet.http.HttpSession; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.config.StringConfigMap; import org.apache.brooklyn.core.internal.BrooklynProperties; +import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.rest.BrooklynWebConfig; +import org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule; import org.apache.brooklyn.util.core.ClassLoaderUtils; -import org.apache.brooklyn.util.text.Strings; +import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,24 +85,20 @@ public class DelegatingSecurityProvider implements SecurityProvider { this, delegate); return delegate; } - log.info("REST using security provider " + className); try { - ClassLoaderUtils clu = new ClassLoaderUtils(this, mgmt); - Class<? extends SecurityProvider> clazz; - try { - clazz = (Class<? extends SecurityProvider>) clu.loadClass(className); - } catch (Exception e) { - String oldPackage = "brooklyn.web.console.security."; - if (className.startsWith(oldPackage)) { - className = Strings.removeFromStart(className, oldPackage); - className = DelegatingSecurityProvider.class.getPackage().getName() + "." + className; - clazz = (Class<? extends SecurityProvider>) clu.loadClass(className); - log.warn("Deprecated package " + oldPackage + " detected; please update security provider to point to " + className); - } else throw e; + String bundle = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_BUNDLE); + if (bundle!=null) { + String bundleVersion = brooklynProperties.getConfig(BrooklynWebConfig.SECURITY_PROVIDER_BUNDLE_VERSION); + log.info("REST using security provider " + className + " from " + bundle+":"+bundleVersion); + BundleContext bundleContext = ((ManagementContextInternal)mgmt).getOsgiManager().get().getFramework().getBundleContext(); + delegate = BrooklynLoginModule.loadProviderFromBundle(mgmt, bundleContext, bundle, bundleVersion, className); + } else { + log.info("REST using security provider " + className); + ClassLoaderUtils clu = new ClassLoaderUtils(this, mgmt); + Class<? extends SecurityProvider> clazz = (Class<? extends SecurityProvider>) clu.loadClass(className); + delegate = createSecurityProviderInstance(mgmt, clazz); } - - delegate = createSecurityProviderInstance(mgmt, clazz); } catch (Exception e) { log.warn("REST unable to instantiate security provider " + className + "; all logins are being disallowed", e); delegate = new BlackholeSecurityProvider(); @@ -173,4 +171,15 @@ public class DelegatingSecurityProvider implements SecurityProvider { private String getModificationCountKey() { return getClass().getName() + ".ModCount"; } + + @Override + public boolean requiresUserPass() { + return getDelegate().requiresUserPass(); + } + + @Override + public String toString() { + return super.toString()+"["+getDelegate()+"]"; + } + } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java index a0795cb..2d9be55 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/ExplicitUsersSecurityProvider.java @@ -114,4 +114,9 @@ public class ExplicitUsersSecurityProvider extends AbstractSecurityProvider impl return false; } + + @Override + public boolean requiresUserPass() { + return true; + } } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/GoogleOauthSecurityProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/GoogleOauthSecurityProvider.java index 21f022e..89e4844 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/GoogleOauthSecurityProvider.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/GoogleOauthSecurityProvider.java @@ -18,29 +18,128 @@ */ package org.apache.brooklyn.rest.security.provider; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpSession; + import org.apache.brooklyn.rest.filter.GoogleOauthFilter; +import org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule; +import org.apache.brooklyn.util.exceptions.Exceptions; +import org.apache.brooklyn.util.text.Identifiers; +import org.apache.brooklyn.util.text.Strings; +import org.apache.brooklyn.util.yaml.Yamls; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.jetty.server.HttpConnection; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.servlet.http.HttpSession; - /** provider who allows everyone */ public class GoogleOauthSecurityProvider implements SecurityProvider { public static final Logger LOG = LoggerFactory.getLogger(GoogleOauthSecurityProvider.class); + private static final Logger logger = LoggerFactory.getLogger(BrooklynLoginModule.class); + private static final String SESSION_KEY_ACCESS_TOKEN = "access_token"; + private static final String SESSION_KEY_CODE = "code"; + private static final String FAKE_TOKEN_FOR_DEBUG = "fake_token"; +// public static final String PARAM_URI_TOKEN_INFO = "uriTokenInfo"; +// public static final String PARAM_URI_GETTOKEN = "uriGetToken"; +// public static final String PARAM_URI_LOGIN_REDIRECT = "uriLoginRedirect"; +// public static final String PARAM_CLIENT_ID = "clientId"; +// public static final String PARAM_CLIENT_SECRET = "clientSecret"; +// public static final String PARAM_CALLBACK_URI = "callbackUri"; +// public static final String PARAM_AUDIENCE = "audience"; + + private String uriGetToken = "https://accounts.google.com/o/oauth2/token"; + private String uriAuthorize = "https://accounts.google.com/o/oauth2/auth"; + private String uriTokenInfo = "https://www.googleapis.com/oauth2/v1/tokeninfo"; + + // or github: +// private String uriGetToken = "https://github.com/login/oauth/authorize"; +// private String uriAuthorize = "https://github.com/login/oauth/authorize"; +// private String uriTokenInfo = "https://github.com/login/oauth/access_token"; + +// private String apiURLBase = "https://api.github.com/"; + +// private String uriTokenRedirect = "/"; + // google + // TODO parameterise + private String clientId = "789182012565-burd24h3bc0im74g2qemi7lnihvfqd02.apps.googleusercontent.com"; + private String clientSecret = "X00v-LfU34U4SfsHqPKMWfQl"; + // github +// private String clientId = "7f76b9970d8ac15b30b0"; +// private String clientSecret = "9e15f8dd651f0b1896a3a582f17fa82f049fc910"; + + // TODO + private String audience = "audience"; + +// private static final String OAUTH2_TOKEN = "org.apache.activemq.jaas.oauth2.token"; +// private static final String OAUTH2_ROLE = "org.apache.activemq.jaas.oauth2.role"; +// private static final String OAUTH2_URL = "org.apache.activemq.jaas.oauth2.oauth2url"; +// private Set<Principal> principals = new HashSet<>(); +// private Subject subject; +// private CallbackHandler callbackHandler; +// private boolean debug; +// private String roleName = "webconsole"; +// private String oauth2URL = uriTokenInfo; +// private boolean loginSucceeded; +// private String userName; +// private boolean commitSuccess; + @Override public boolean isAuthenticated(HttpSession session) { - LOG.info("isAuthenticated"); + LOG.info("isAuthenticated 1 "+session+" ... "+this); Object token = session.getAttribute(GoogleOauthFilter.SESSION_KEY_ACCESS_TOKEN); - + // TODO is it valid? return token!=null; } @Override public boolean authenticate(HttpSession session, String user, String password) { - LOG.info("authenticate"); - return true; + LOG.info("authenticate "+session+" "+user); + + if (isAuthenticated(session)) { + return true; + } + + Request request = getJettyRequest(); + // Redirection from the authenticator server + String code = request.getParameter(SESSION_KEY_CODE); + // Getting token, if exists, from the current session + String token = (String) request.getSession().getAttribute(SESSION_KEY_ACCESS_TOKEN); + + try { + if (Strings.isNonBlank(code)) { + return getToken(); + } else if (Strings.isEmpty(token)) { + return redirectLogin(); + } else { + return validateToken(token); + } + } catch (Exception e) { + LOG.warn("Error performing OAuth: "+e, e); + throw Exceptions.propagate(e); + } } @Override @@ -49,4 +148,162 @@ public class GoogleOauthSecurityProvider implements SecurityProvider { session.removeAttribute(GoogleOauthFilter.SESSION_KEY_ACCESS_TOKEN); return true; } + + @Override + public boolean requiresUserPass() { + return false; + } + + private boolean getToken() throws ClientProtocolException, IOException, ServletException { + Request request = getJettyRequest(); + String code = request.getParameter(SESSION_KEY_CODE); + String callbackUri = request.getRequestURL().toString(); + + // get the access token by post to Google + HashMap<String, String> params = new HashMap<String, String>(); + params.put(SESSION_KEY_CODE, code); + params.put("client_id", clientId); + params.put("client_secret", clientSecret); + params.put("redirect_uri", callbackUri); + params.put("grant_type", "authorization_code"); + + String body = post(uriGetToken, params); + + Map<?,?> jsonObject = null; + + // get the access token from json and request info from Google + try { + jsonObject = (Map<?,?>) Yamls.parseAll(body).iterator().next(); + logger.info("Parsed '"+body+"' as "+jsonObject); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + logger.info("Unable to parse: '"+body+"'"); + // throw new RuntimeException("Unable to parse json " + body); + return redirectLogin(); + } + + // Left token and code in session + String accessToken = (String) jsonObject.get(SESSION_KEY_ACCESS_TOKEN); + request.getSession().setAttribute(SESSION_KEY_ACCESS_TOKEN, accessToken); + request.getSession().setAttribute(SESSION_KEY_CODE, code); + + // TODO is it valid? + LOG.debug("Got token/code "+accessToken+"/"+code+" from "+jsonObject); + // resp.getWriter().println(json); + return true; + } + + private boolean validateToken(String token) throws ClientProtocolException, IOException { + // TODO for debug + if(token.equals(FAKE_TOKEN_FOR_DEBUG)){ + return true; + } + HashMap<String, String> params = new HashMap<String, String>(); + params.put(SESSION_KEY_ACCESS_TOKEN, token); + + String body = post(uriTokenInfo, params); + // System.out.println(body); + Map<?,?> jsonObject = null; + + // get the access token from json and request info from Google + try { + jsonObject = (Map<?,?>) Yamls.parseAll(body).iterator().next(); + logger.info("Parsed '"+body+"' as "+jsonObject); + } catch (Exception e) { + Exceptions.propagateIfFatal(e); + logger.info("Unable to parse: '"+body+"'"); + throw new RuntimeException("Unable to parse json " + body, e); + } + + // TODO what's this for? + if (!clientId.equals(jsonObject.get(audience))) { + return redirectLogin(); + } + // if (isTokenExpiredOrNearlySo(...) { ... } + return true; + } + + // TODO these http methods need tidying + + // makes a GET request to url and returns body as a string + public String get(String url) throws ClientProtocolException, IOException { + return execute(new HttpGet(url)); + } + + // makes a POST request to url with form parameters and returns body as a + // string + public String post(String url, Map<String, String> formParameters) throws ClientProtocolException, IOException { + HttpPost request = new HttpPost(url); + + List<NameValuePair> nvps = new ArrayList<NameValuePair>(); + for (String key : formParameters.keySet()) { + nvps.add(new BasicNameValuePair(key, formParameters.get(key))); + } + request.setEntity(new UrlEncodedFormEntity(nvps)); + + return execute(request); + } + + // makes request and checks response code for 200 + private String execute(HttpRequestBase request) throws ClientProtocolException, IOException { + // TODO tidy + HttpClient httpClient = new DefaultHttpClient(); + HttpResponse response = httpClient.execute(request); + + HttpEntity entity = response.getEntity(); + String body = EntityUtils.toString(entity); + + if (response.getStatusLine().getStatusCode() != 200) { + throw new RuntimeException( + "Expected 200 but got " + response.getStatusLine().getStatusCode() + ", with body " + body); + } + + return body; + } + + private boolean redirectLogin() throws IOException { + String state=Identifiers.makeRandomId(12); //should be stored in session + String callbackUri = getJettyRequest().getRequestURL().toString(); + StringBuilder oauthUrl = new StringBuilder().append(uriAuthorize) + .append("?response_type=").append("code") + .append("&client_id=").append(clientId) // the client id from the api console registration + .append("&redirect_uri=").append(callbackUri) // the servlet that github redirects to after + // authorization +// .append("&scope=").append("user public_repo") + .append("&scope=openid%20email") // scope is the api permissions we + .append("&state=").append(state) + .append("&access_type=offline") // here we are asking to access to user's data while they are not + // signed in + .append("&approval_prompt=force"); // this requires them to verify which account to use, if they are + // already signed in + + // just for look inside +// Collection<String> originalHeaders = response.getHeaderNames(); + + Response response = getJettyResponse(); + response.reset(); +// response.addHeader("Origin", "http://localhost.io:8081"); +// response.addHeader("Access-Control-Allow-Origin", "*"); +//// response.addHeader("Access-Control-Request-Method", "GET, POST"); +//// response.addHeader("Access-Control-Request-Headers", "origin, x-requested-with"); + LOG.info("OAUTH url redirect is: "+oauthUrl.toString()); + + response.sendRedirect(oauthUrl.toString()); + + return false; + } + + private Request getJettyRequest() { + return Optional.ofNullable(HttpConnection.getCurrentConnection()) + .map(HttpConnection::getHttpChannel) + .map(HttpChannel::getRequest) + .orElse(null); + } + + private Response getJettyResponse() { + return Optional.ofNullable(HttpConnection.getCurrentConnection()) + .map(HttpConnection::getHttpChannel) + .map(HttpChannel::getResponse) + .orElse(null); + } } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/LdapSecurityProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/LdapSecurityProvider.java index d3636e9..cc9b013 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/LdapSecurityProvider.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/LdapSecurityProvider.java @@ -129,4 +129,9 @@ public class LdapSecurityProvider extends AbstractSecurityProvider implements Se throw Exceptions.propagate(new ClassNotFoundException("Unable to load LDAP classes ("+LDAP_CONTEXT_FACTORY+") required for Brooklyn LDAP security provider")); } } + + @Override + public boolean requiresUserPass() { + return true; + } } diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/SecurityProvider.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/SecurityProvider.java index 57d1400..42575ff 100644 --- a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/SecurityProvider.java +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/security/provider/SecurityProvider.java @@ -28,8 +28,13 @@ import javax.servlet.http.HttpSession; public interface SecurityProvider { public boolean isAuthenticated(HttpSession session); - - public boolean authenticate(HttpSession session, String user, String password); - + /** whether this provider requires a user/pass; if this returns false, the framework can + * send null/null as the user/pass to {@link #authenticate(HttpSession, String, String)}, + * and should do that if user/pass info is not immediately available + * (ie for things like oauth, the framework should not require basic auth if this method returns false) + */ + public boolean requiresUserPass(); + public boolean authenticate(HttpSession session, String user, String pass); public boolean logout(HttpSession session); + } diff --git a/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml b/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml index e0b6c9f..dc40b44 100644 --- a/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml +++ b/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml @@ -59,11 +59,6 @@ limitations under the License. <reference id="localManagementContext" interface="org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal"/> - <jaas:config name="webconsole"> - <!-- <jaas:module className="org.apache.brooklyn.rest.security.jaas.GoogleOauthLoginModule" flags="required"/> --> - <jaas:module className="org.apache.brooklyn.rest.security.jaas.BrooklynLoginModule" flags="required"/> - </jaas:config> - <reference id="shutdownHandler" interface="org.apache.brooklyn.core.mgmt.ShutdownHandler"/> <bean class="org.apache.brooklyn.rest.security.jaas.ManagementContextHolder"> @@ -117,10 +112,7 @@ limitations under the License. <bean class="org.apache.brooklyn.rest.util.DefaultExceptionMapper"/> <bean class="org.apache.brooklyn.rest.util.json.BrooklynJacksonJsonProvider"/> <bean class="org.apache.brooklyn.rest.util.FormMapProvider"/> - <bean class="org.apache.cxf.jaxrs.security.JAASAuthenticationFilter"> - <property name="contextName" value="webconsole"/> - <property name="realmName" value="webconsole"/> - </bean> + <bean class="org.apache.brooklyn.rest.filter.BrooklynSecurityProviderFilter"/> <bean class="org.apache.brooklyn.rest.util.ManagementContextProvider"> <argument ref="localManagementContext"/> </bean> diff --git a/rest/rest-resources/src/main/resources/jaas.conf b/rest/rest-resources/src/main/resources/jaas.conf deleted file mode 100644 index 45dc90a..0000000 --- a/rest/rest-resources/src/main/resources/jaas.conf +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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. - */ -webconsole { - org.apache.brooklyn.rest.security.jaas.GoogleOauthLoginModule required; -}; diff --git a/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java b/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java index f5089a1..fcdea9d 100644 --- a/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java +++ b/rest/rest-server/src/main/java/org/apache/brooklyn/rest/filter/BrooklynPropertiesSecurityFilter.java @@ -60,7 +60,7 @@ public class BrooklynPropertiesSecurityFilter implements Filter { */ public static final String AUTHENTICATED_USER_SESSION_ATTRIBUTE = BrooklynLoginModule.AUTHENTICATED_USER_SESSION_ATTRIBUTE; - private static final Logger log = LoggerFactory.getLogger(BrooklynPropertiesSecurityFilter.class); + private static final Logger log = LoggerFactory.getLogger(BrooklynSecurityProviderFilter.class); protected DelegatingSecurityProvider provider; diff --git a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/entitlement/AuthenticateAnyoneSecurityProvider.java b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/entitlement/AuthenticateAnyoneSecurityProvider.java index b7264b2..d231886 100644 --- a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/entitlement/AuthenticateAnyoneSecurityProvider.java +++ b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/entitlement/AuthenticateAnyoneSecurityProvider.java @@ -38,4 +38,9 @@ public class AuthenticateAnyoneSecurityProvider implements SecurityProvider { public boolean logout(HttpSession session) { return false; } + + @Override + public boolean requiresUserPass() { + return true; + } } \ No newline at end of file diff --git a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/security/provider/TestSecurityProvider.java b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/security/provider/TestSecurityProvider.java index cad251f..d6350d0 100644 --- a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/security/provider/TestSecurityProvider.java +++ b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/security/provider/TestSecurityProvider.java @@ -43,4 +43,9 @@ public class TestSecurityProvider implements SecurityProvider { public boolean logout(HttpSession session) { return false; } + + @Override + public boolean requiresUserPass() { + return true; + } }
