/* (c) 2016 Open Source Geospatial Foundation - all rights reserved * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.security; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import org.geoserver.security.config.SecurityNamedServiceConfig; import org.geoserver.security.event.RoleLoadedListener; import org.geoserver.security.impl.AbstractGeoServerSecurityService; import org.geoserver.security.impl.GeoServerRole; import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.PathNotFoundException; /** * @author Alessio Fabiani, GeoSolutions S.A.S. * */ public class GeoServerRestRoleService extends AbstractGeoServerSecurityService implements GeoServerRoleService { static final SortedSet<String> emptyStringSet = Collections .unmodifiableSortedSet(new TreeSet<String>()); static final Map<String, String> emptyMap = Collections.emptyMap(); /** * Sets a specified timeout value, in milliseconds, to be used when opening a * communications link to the resource referenced by this URLConnection. * If the timeout expires before the connection can be established, * a {@link java.net.SocketTimeoutException} is raised. * * A timeout of zero is interpreted as an infinite timeout. * * Some non-standard implementation of this method may ignore the specified timeout. * To see the connect timeout set, please call getConnectTimeout(). * * @param timeout an int that specifies the connect timeout value in milliseconds * @throws {@link IllegalArgumentException} - if the timeout parameter is negative */ private static final int CONN_TIMEOUT = 30000; /** * Sets the read timeout to a specified timeout, in milliseconds. * A non-zero value specifies the timeout when reading from Input stream when a * connection is established to a resource. * If the timeout expires before there is data available for read, * a {@link java.net.SocketTimeoutException} is raised. * * A timeout of zero is interpreted as an infinite timeout. * * Some non-standard implementation of this method may ignore the specified timeout. * To see the read timeout set, please call getReadTimeout(). * * @param timeout an int that specifies the timeout value to be used in milliseconds * @throws {@link IllegalArgumentException} - if the timeout parameter is negative */ private static final int READ_TIMEOUT = 30000; private RestTemplate restTemplate; private static String rolePrefix = "ROLE_"; static boolean isEmpty(String property) { return property == null || property.isEmpty(); } GeoServerRestRoleServiceConfig restRoleServiceConfig; private boolean convertToUpperCase = true; private String adminGroup; private String groupAdminGroup; protected Set<RoleLoadedListener> listeners = Collections .synchronizedSet(new HashSet<RoleLoadedListener>()); /** * Default Constructor */ public GeoServerRestRoleService(SecurityNamedServiceConfig config) throws IOException { initializeFromConfig(config); } @Override public void initializeFromConfig(SecurityNamedServiceConfig config) throws IOException { super.initializeFromConfig(config); restRoleServiceConfig = (GeoServerRestRoleServiceConfig) config; if (!isEmpty(restRoleServiceConfig.getAdminRoleName())) { this.adminGroup = restRoleServiceConfig.getAdminRoleName(); } if (!isEmpty(restRoleServiceConfig.getGroupAdminRoleName())) { this.groupAdminGroup = restRoleServiceConfig.getGroupAdminRoleName(); } } /** * Read only store. */ @Override public boolean canCreateStore() { return false; } /** * Read only store. */ @Override public GeoServerRoleStore createStore() throws IOException { return null; } /** * @see org.geoserver.security.GeoServerRoleService#registerRoleLoadedListener(RoleLoadedListener) */ public void registerRoleLoadedListener(RoleLoadedListener listener) { listeners.add(listener); } /** * @see org.geoserver.security.GeoServerRoleService#unregisterRoleLoadedListener(RoleLoadedListener) */ public void unregisterRoleLoadedListener(RoleLoadedListener listener) { listeners.remove(listener); } /** * Roles to group association is not supported */ @Override public SortedSet<String> getGroupNamesForRole(GeoServerRole role) throws IOException { return emptyStringSet; } @Override public SortedSet<String> getUserNamesForRole(GeoServerRole role) throws IOException { final SortedSet<String> users = new TreeSet<String>(); return Collections.unmodifiableSortedSet(users); } @SuppressWarnings("unchecked") @Override public SortedSet<GeoServerRole> getRolesForUser(String username) throws IOException { final SortedSet<GeoServerRole> roles = new TreeSet<GeoServerRole>(); try { return (SortedSet<GeoServerRole>) connectToRESTEndpoint( restRoleServiceConfig.getBaseUrl(), restRoleServiceConfig.getUsersRESTEndpoint() + "/" + username, restRoleServiceConfig.getUsersJSONPath(), new RestEndpointConnectionCallback() { @Override public Object executeWithContext(String json) throws Exception { try { List<String> rolesString = JsonPath.read(json, restRoleServiceConfig.getUsersJSONPath()); for (String role : rolesString) { if (role.startsWith(rolePrefix)) { // remove standard role prefix role = role.substring(rolePrefix.length()); } roles.add(createRoleObject(role)); } } catch (PathNotFoundException ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); roles.clear(); roles.add(GeoServerRole.AUTHENTICATED_ROLE); } SortedSet<GeoServerRole> finalRoles = Collections.unmodifiableSortedSet(fixGeoServerRoles(roles)); if(LOGGER.isLoggable(Level.FINE)) { LOGGER.fine("Setting ROLES for User [" + username + "] to " + finalRoles); } return finalRoles; } }); } catch (Exception ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); } return Collections.unmodifiableSortedSet(roles); } protected SortedSet<GeoServerRole> fixGeoServerRoles(SortedSet<GeoServerRole> roles) { // Check if is an ADMIN GeoServerRole adminRole = getAdminRole(); if (roles.contains(GeoServerRole.ADMIN_ROLE) || roles.contains(adminRole)) { roles.clear(); roles.add(GeoServerRole.ADMIN_ROLE); } // Check if Role Anonymous is present other than other roles if (roles.size() > 1 && roles.contains(GeoServerRole.ANONYMOUS_ROLE)) { roles.remove(GeoServerRole.ANONYMOUS_ROLE); } return roles; } @Override public SortedSet<GeoServerRole> getRolesForGroup(String groupname) throws IOException { SortedSet<GeoServerRole> set = new TreeSet<GeoServerRole>(); GeoServerRole role = getRoleByName(groupname); if (role != null) { set.add(role); } return Collections.unmodifiableSortedSet(set); } @SuppressWarnings("unchecked") @Override public SortedSet<GeoServerRole> getRoles() throws IOException { final SortedSet<GeoServerRole> roles = new TreeSet<GeoServerRole>(); try { return (SortedSet<GeoServerRole>) connectToRESTEndpoint( restRoleServiceConfig.getBaseUrl(), restRoleServiceConfig.getRolesRESTEndpoint(), restRoleServiceConfig.getRolesJSONPath(), new RestEndpointConnectionCallback() { @Override public Object executeWithContext(String json) throws Exception { try { List<String> rolesString = JsonPath.read(json, restRoleServiceConfig.getRolesJSONPath()); for (String role : rolesString) { if (role.startsWith(rolePrefix)) { // remove standard role prefix role = role.substring(rolePrefix.length()); } roles.add(createRoleObject(role)); } } catch (PathNotFoundException ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); } return Collections.unmodifiableSortedSet(roles); } }); } catch (Exception ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); } return Collections.unmodifiableSortedSet(roles); } @Override public Map<String, String> getParentMappings() throws IOException { return emptyMap; } @Override public GeoServerRole createRoleObject(String role) throws IOException { return new GeoServerRole(rolePrefix + (convertToUpperCase ? role.toUpperCase() : role)); } @Override public GeoServerRole getParentRole(GeoServerRole role) throws IOException { return null; } @Override public GeoServerRole getRoleByName(String role) throws IOException { if (role.startsWith(rolePrefix)) { // remove standard role prefix role = role.substring(rolePrefix.length()); } final String roleName = role; try { return (GeoServerRole) connectToRESTEndpoint( restRoleServiceConfig.getBaseUrl(), restRoleServiceConfig.getRolesRESTEndpoint(), restRoleServiceConfig.getRolesJSONPath(), new RestEndpointConnectionCallback() { @Override public Object executeWithContext(String json) throws Exception { try { List<String> rolesString = JsonPath.read(json, restRoleServiceConfig.getRolesJSONPath()); for (String targetRole : rolesString) { if (targetRole.startsWith(rolePrefix)) { // remove standard role prefix targetRole = targetRole.substring(rolePrefix.length()); } if (roleName.equalsIgnoreCase(targetRole)) { return createRoleObject(roleName); } } } catch (PathNotFoundException ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); } return null; } }); } catch (Exception ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); } return null; } @Override public void load() throws IOException { // Not needed } @Override public Properties personalizeRoleParams(String roleName, Properties roleParams, String userName, Properties userProps) throws IOException { return null; } @Override public GeoServerRole getAdminRole() { if (adminGroup == null) { try { return (GeoServerRole) connectToRESTEndpoint( restRoleServiceConfig.getBaseUrl(), restRoleServiceConfig.getAdminRoleRESTEndpoint(), restRoleServiceConfig.getAdminRoleJSONPath(), new RestEndpointConnectionCallback() { @Override public Object executeWithContext(String json) throws Exception { try { String targetRole = JsonPath.read(json, restRoleServiceConfig.getAdminRoleJSONPath()); if (targetRole.startsWith(rolePrefix)) { // remove standard role prefix targetRole = targetRole.substring(rolePrefix.length()); } return createRoleObject(targetRole); } catch (PathNotFoundException ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); } return null; } }); } catch (Exception ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); } } try { return getRoleByName(adminGroup); } catch (IOException e) { throw new RuntimeException(e); } } @Override public GeoServerRole getGroupAdminRole() { if (groupAdminGroup == null) { return getAdminRole(); } try { return getRoleByName(groupAdminGroup); } catch (IOException e) { throw new RuntimeException(e); } } @Override public int getRoleCount() throws IOException { return getRoles().size(); } /** * @return the restTemplate */ public RestTemplate getRestTemplate() { if (restTemplate == null) { restTemplate = restTemplate(); } return restTemplate; } /** * @param restTemplate the restTemplate to set */ public void setRestTemplate(RestTemplate restTemplate) { this.restTemplate = restTemplate; } private RestTemplate restTemplate() { return new RestTemplate(clientHttpRequestFactory()); } private ClientHttpRequestFactory clientHttpRequestFactory() { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); factory.setReadTimeout(READ_TIMEOUT); factory.setConnectTimeout(CONN_TIMEOUT); return factory; } /** * Execute REST CALL, and then call the given callback on HTTP JSON Response. * * @param callback * @throws Exception */ protected Object connectToRESTEndpoint( final String roleRESTBaseURL, final String roleRESTEndpoint, final String roleJSONPath, RestEndpointConnectionCallback callback) throws Exception { // HttpURLConnection conn = null; ClientHttpRequest clientRequest = null; ClientHttpResponse clientResponse = null; try { final URI baseURI = new URI(roleRESTBaseURL); URL url = baseURI.resolve(roleRESTEndpoint).toURL(); /*conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Accept", "application/json"); conn.setRequestProperty("Content-length", "0"); conn.setUseCaches(false); conn.setAllowUserInteraction(false); conn.setConnectTimeout(CONN_TIMEOUT); conn.setReadTimeout(READ_TIMEOUT); conn.connect(); int status = conn.getResponseCode();*/ clientRequest = getRestTemplate().getRequestFactory().createRequest(url.toURI(), HttpMethod.GET); clientResponse = clientRequest.execute(); int status = clientResponse.getRawStatusCode(); switch (status) { case 200: case 201: BufferedReader br = new BufferedReader( new InputStreamReader(clientResponse.getBody())); StringBuilder sb = new StringBuilder(); String line; while ((line = br.readLine()) != null) { sb.append(line + "\n"); } br.close(); String json = sb.toString(); return callback.executeWithContext(json); } } catch (MalformedURLException ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); } catch (IOException ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); } catch (URISyntaxException ex) { Logger.getLogger(getClass().getName()).log(Level.WARNING, null, ex); } finally { if (clientResponse != null) { try { clientResponse.close(); } catch (Exception ex) { Logger.getLogger(getClass().getName()).log(Level.SEVERE, null, ex); } } } return null; } /** * Callback interface to be used in the REST call methods for performing operations on individually HTTP JSON responses. * * @author Alessio Fabiani, GeoSolutions S.A.S. * */ interface RestEndpointConnectionCallback { /** * Perform specific operations accordingly to the caller needs. * * @param json the <code>JSON</code> string to perform an operation on. * @throws Exception */ Object executeWithContext(final String json) throws Exception; } }