From 464a6287f7d42de16e8ca1a4e8d6602974d3d276 Mon Sep 17 00:00:00 2001 From: Rick Herrick <jrherrick@wustl.edu> Date: Wed, 7 Sep 2016 11:56:20 -0500 Subject: [PATCH] XNAT-4325 XNAT-4432 Added /xapi/siteConfig/siteWideAlert* to open URLs. Jiggered the app info bean to carry path info instead of injecting directly into filter bean. Used this to create open URL matchers to test if users are permitted to access particular site config values. Added test for missing properties to return 404 when values are requested for non-existent properties. --- .../nrg/xapi/rest/settings/SiteConfigApi.java | 69 ++++-- .../nrg/xnat/initialization/RootConfig.java | 4 +- .../xnat/initialization/SecurityConfig.java | 49 +---- ...rSecurityInterceptorBeanPostProcessor.java | 40 ++-- .../xnat/security/XnatInitCheckFilter.java | 71 +----- .../org/nrg/xnat/services/XnatAppInfo.java | 207 +++++++++++++++--- .../xnat/security/configured-urls.yaml | 1 + 7 files changed, 256 insertions(+), 185 deletions(-) diff --git a/src/main/java/org/nrg/xapi/rest/settings/SiteConfigApi.java b/src/main/java/org/nrg/xapi/rest/settings/SiteConfigApi.java index 1b74e2fa..d2f839d5 100644 --- a/src/main/java/org/nrg/xapi/rest/settings/SiteConfigApi.java +++ b/src/main/java/org/nrg/xapi/rest/settings/SiteConfigApi.java @@ -46,7 +46,7 @@ public class SiteConfigApi extends AbstractXapiRestController { public void checkForFoundPreferences() { if (!_appInfo.isInitialized()) { Map<String, String> tempPrefs = _appInfo.getFoundPreferences(); - if(tempPrefs!=null){ + if (tempPrefs != null) { _found.putAll(tempPrefs); } if (_found.size() > 0) { @@ -56,12 +56,17 @@ public class SiteConfigApi extends AbstractXapiRestController { } @ApiOperation(value = "Returns the full map of site configuration properties.", notes = "Complex objects may be returned as encapsulated JSON strings.", response = String.class, responseContainer = "Map") - @ApiResponses({@ApiResponse(code = 200, message = "Site configuration properties successfully retrieved."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to set site configuration properties."), @ApiResponse(code = 500, message = "Unexpected error")}) + @ApiResponses({@ApiResponse(code = 200, message = "Site configuration properties successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized to set site configuration properties."), + @ApiResponse(code = 500, message = "Unexpected error")}) @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) public ResponseEntity<Map<String, Object>> getSiteConfigProperties(final HttpServletRequest request) { - final HttpStatus status = isPermitted(); - if (status != null) { - return new ResponseEntity<>(status); + if (!_appInfo.isOpenUrlRequest(request)) { + final HttpStatus status = isPermitted(request, _appInfo.getOpenUrls().values()); + if (status != null) { + return new ResponseEntity<>(status); + } } final String username = getSessionUser().getUsername(); if (_log.isDebugEnabled()) { @@ -83,7 +88,10 @@ public class SiteConfigApi extends AbstractXapiRestController { } @ApiOperation(value = "Sets a map of site configuration properties.", notes = "Sets the site configuration properties specified in the map.", response = Void.class) - @ApiResponses({@ApiResponse(code = 200, message = "Site configuration properties successfully set."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to set site configuration properties."), @ApiResponse(code = 500, message = "Unexpected error")}) + @ApiResponses({@ApiResponse(code = 200, message = "Site configuration properties successfully set."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized to set site configuration properties."), + @ApiResponse(code = 500, message = "Unexpected error")}) @RequestMapping(consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.APPLICATION_JSON_VALUE}, method = RequestMethod.POST) public ResponseEntity<Void> setSiteConfigProperties(@ApiParam(value = "The map of site configuration properties to be set.", required = true) @RequestBody final Map<String, String> properties) throws InitializationException { final HttpStatus status = isPermitted(); @@ -121,7 +129,10 @@ public class SiteConfigApi extends AbstractXapiRestController { } @ApiOperation(value = "Returns a map of the selected site configuration properties.", notes = "Complex objects may be returned as encapsulated JSON strings.", response = String.class, responseContainer = "Map") - @ApiResponses({@ApiResponse(code = 200, message = "Site configuration properties successfully retrieved."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to set site configuration properties."), @ApiResponse(code = 500, message = "Unexpected error")}) + @ApiResponses({@ApiResponse(code = 200, message = "Site configuration properties successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized to set site configuration properties."), + @ApiResponse(code = 500, message = "Unexpected error")}) @RequestMapping(value = "values/{preferences}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) public ResponseEntity<Map<String, Object>> getSpecifiedSiteConfigProperties(@PathVariable final List<String> preferences) { final HttpStatus status = isPermitted(); @@ -135,21 +146,28 @@ public class SiteConfigApi extends AbstractXapiRestController { final Map<String, Object> values = new HashMap<>(); for (final String preference : preferences) { - final Object value = getPreferences().get(preference); - if (value != null) { - values.put(preference, value); + if (getPreferences().containsKey(preference)) { + values.put(preference, getPreferences().get(preference)); } } return new ResponseEntity<>(values, HttpStatus.OK); } @ApiOperation(value = "Returns the value of the selected site configuration property.", notes = "Complex objects may be returned as encapsulated JSON strings.", response = Object.class) - @ApiResponses({@ApiResponse(code = 200, message = "Site configuration property successfully retrieved."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to access site configuration properties."), @ApiResponse(code = 500, message = "Unexpected error")}) + @ApiResponses({@ApiResponse(code = 200, message = "Site configuration property successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized to access site configuration properties."), + @ApiResponse(code = 500, message = "Unexpected error")}) @RequestMapping(value = "{property}", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) - public ResponseEntity<Object> getSpecifiedSiteConfigProperty(@ApiParam(value = "The site configuration property to retrieve.", required = true) @PathVariable final String property) { - final HttpStatus status = isPermitted(); - if (status != null) { - return new ResponseEntity<>(status); + public ResponseEntity<Object> getSpecifiedSiteConfigProperty(final HttpServletRequest request, @ApiParam(value = "The site configuration property to retrieve.", required = true) @PathVariable final String property) { + if (!_appInfo.isOpenUrlRequest(request)) { + final HttpStatus status = isPermitted(); + if (status != null) { + return new ResponseEntity<>(status); + } + } + if (!getPreferences().containsKey(property)) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); } final Object value = getPreferences().get(property); if (_log.isDebugEnabled()) { @@ -159,7 +177,10 @@ public class SiteConfigApi extends AbstractXapiRestController { } @ApiOperation(value = "Sets a single site configuration property.", notes = "Sets the site configuration property specified in the URL to the value set in the body.", response = Void.class) - @ApiResponses({@ApiResponse(code = 200, message = "Site configuration properties successfully set."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 403, message = "Not authorized to set site configuration properties."), @ApiResponse(code = 500, message = "Unexpected error")}) + @ApiResponses({@ApiResponse(code = 200, message = "Site configuration properties successfully set."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 403, message = "Not authorized to set site configuration properties."), + @ApiResponse(code = 500, message = "Unexpected error")}) @RequestMapping(value = "{property}", consumes = {MediaType.TEXT_PLAIN_VALUE, MediaType.APPLICATION_JSON_VALUE}, produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.POST) public ResponseEntity<Void> setSiteConfigProperty(@ApiParam(value = "The property to be set.", required = true) @PathVariable("property") final String property, @ApiParam("The value to be set for the property.") @RequestBody final String value) throws InitializationException { final HttpStatus status = isPermitted(); @@ -187,7 +208,9 @@ public class SiteConfigApi extends AbstractXapiRestController { } @ApiOperation(value = "Returns a map of application build properties.", notes = "This includes the implementation version, Git commit hash, and build number and number.", response = Properties.class) - @ApiResponses({@ApiResponse(code = 200, message = "Application build properties successfully retrieved."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 500, message = "Unexpected error")}) + @ApiResponses({@ApiResponse(code = 200, message = "Application build properties successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 500, message = "Unexpected error")}) @RequestMapping(value = "buildInfo", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) public ResponseEntity<Properties> getBuildInfo() { if (_log.isDebugEnabled()) { @@ -198,7 +221,9 @@ public class SiteConfigApi extends AbstractXapiRestController { } @ApiOperation(value = "Returns a map of extended build attributes.", notes = "The values are dependent on what attributes are set for the build. It is not unexpected that there are no extended build attributes.", response = String.class, responseContainer = "Map") - @ApiResponses({@ApiResponse(code = 200, message = "Extended build attributes successfully retrieved."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 500, message = "Unexpected error")}) + @ApiResponses({@ApiResponse(code = 200, message = "Extended build attributes successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 500, message = "Unexpected error")}) @RequestMapping(value = "buildInfo/attributes", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) public ResponseEntity<Map<String, Map<String, String>>> getBuildAttributeInfo() { if (_log.isDebugEnabled()) { @@ -209,7 +234,9 @@ public class SiteConfigApi extends AbstractXapiRestController { } @ApiOperation(value = "Returns the system uptime.", notes = "This returns the uptime as a map of time units: days, hours, minutes, and seconds.", response = String.class, responseContainer = "Map") - @ApiResponses({@ApiResponse(code = 200, message = "System uptime successfully retrieved."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 500, message = "Unexpected error")}) + @ApiResponses({@ApiResponse(code = 200, message = "System uptime successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 500, message = "Unexpected error")}) @RequestMapping(value = "uptime", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) public ResponseEntity<Map<String, String>> getSystemUptime() { if (_log.isDebugEnabled()) { @@ -220,7 +247,9 @@ public class SiteConfigApi extends AbstractXapiRestController { } @ApiOperation(value = "Returns the system uptime.", notes = "This returns the uptime as a formatted string.", response = String.class) - @ApiResponses({@ApiResponse(code = 200, message = "System uptime successfully retrieved."), @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), @ApiResponse(code = 500, message = "Unexpected error")}) + @ApiResponses({@ApiResponse(code = 200, message = "System uptime successfully retrieved."), + @ApiResponse(code = 401, message = "Must be authenticated to access the XNAT REST API."), + @ApiResponse(code = 500, message = "Unexpected error")}) @RequestMapping(value = "uptime/display", produces = MediaType.APPLICATION_JSON_VALUE, method = RequestMethod.GET) public ResponseEntity<String> getFormattedSystemUptime() { if (_log.isDebugEnabled()) { diff --git a/src/main/java/org/nrg/xnat/initialization/RootConfig.java b/src/main/java/org/nrg/xnat/initialization/RootConfig.java index f26e745a..4c9aa1ef 100644 --- a/src/main/java/org/nrg/xnat/initialization/RootConfig.java +++ b/src/main/java/org/nrg/xnat/initialization/RootConfig.java @@ -51,8 +51,8 @@ import java.util.Properties; @Import({PropertiesConfig.class, DatabaseConfig.class, SecurityConfig.class, ApplicationConfig.class}) public class RootConfig { @Bean - public XnatAppInfo appInfo(final ServletContext context, final JdbcTemplate template) throws IOException { - return new XnatAppInfo(context, template); + public XnatAppInfo appInfo(final ServletContext context, final SerializerService serializerService, final JdbcTemplate template) throws IOException { + return new XnatAppInfo(context, serializerService, template); } @Bean diff --git a/src/main/java/org/nrg/xnat/initialization/SecurityConfig.java b/src/main/java/org/nrg/xnat/initialization/SecurityConfig.java index 68b90f65..6d4530e3 100644 --- a/src/main/java/org/nrg/xnat/initialization/SecurityConfig.java +++ b/src/main/java/org/nrg/xnat/initialization/SecurityConfig.java @@ -1,10 +1,6 @@ package org.nrg.xnat.initialization; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; import org.nrg.config.exceptions.SiteConfigurationException; -import org.nrg.framework.services.SerializerService; import org.nrg.xdat.preferences.SiteConfigPreferences; import org.nrg.xdat.services.AliasTokenService; import org.nrg.xdat.services.XdatUserAuthService; @@ -20,9 +16,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; import org.springframework.context.annotation.Primary; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.vote.AuthenticatedVoter; @@ -46,7 +39,6 @@ import org.springframework.security.web.session.ConcurrentSessionFilter; import javax.sql.DataSource; import java.io.IOException; -import java.io.InputStream; import java.util.*; @Configuration @@ -124,15 +116,8 @@ public class SecurityConfig { } @Bean - public FilterSecurityInterceptorBeanPostProcessor filterSecurityInterceptorBeanPostProcessor(final SerializerService serializer, final SiteConfigPreferences preferences) throws IOException { - final Resource resource = RESOURCE_LOADER.getResource("classpath:META-INF/xnat/security/configured-urls.yaml"); - try (final InputStream inputStream = resource.getInputStream()) { - final HashMap<String, ArrayList<String>> urlMap = serializer.deserializeYaml(inputStream, SerializerService.TYPE_REF_MAP_STRING_LIST_STRING); - final FilterSecurityInterceptorBeanPostProcessor postProcessor = new FilterSecurityInterceptorBeanPostProcessor(preferences); - postProcessor.setOpenUrls(urlMap.get("openUrls")); - postProcessor.setAdminUrls(urlMap.get("adminUrls")); - return postProcessor; - } + public FilterSecurityInterceptorBeanPostProcessor filterSecurityInterceptorBeanPostProcessor(final SiteConfigPreferences preferences, final XnatAppInfo appInfo) throws IOException { + return new FilterSecurityInterceptorBeanPostProcessor(preferences, appInfo); } @Bean @@ -203,38 +188,12 @@ public class SecurityConfig { } @Bean - public XnatInitCheckFilter xnatInitCheckFilter(final SerializerService serializer, final XnatAppInfo appInfo) throws IOException { - final Resource resource = RESOURCE_LOADER.getResource("classpath:META-INF/xnat/security/initialization-urls.yaml"); - try (final InputStream inputStream = resource.getInputStream()) { - final XnatInitCheckFilter filter = new XnatInitCheckFilter(appInfo); - final JsonNode paths = serializer.deserializeYaml(inputStream); - filter.setConfigurationPath(paths.get("configPath").asText()); - filter.setNonAdminErrorPath(paths.get("nonAdminErrorPath").asText()); - filter.setInitializationPaths(nodeToList(paths.get("initPaths"))); - filter.setExemptedPaths(nodeToList(paths.get("exemptedPaths"))); - return filter; - } + public XnatInitCheckFilter xnatInitCheckFilter(final XnatAppInfo appInfo) throws IOException { + return new XnatInitCheckFilter(appInfo); } @Bean public XnatDatabaseUserDetailsService customDatabaseService(final XdatUserAuthService userAuthService, final DataSource dataSource) { return new XnatDatabaseUserDetailsService(userAuthService, dataSource); } - - protected List<String> nodeToList(final JsonNode node) { - final List<String> list = new ArrayList<>(); - if (node.isArray()) { - final ArrayNode arrayNode = (ArrayNode) node; - for (final JsonNode item : arrayNode) { - list.add(item.asText()); - } - } else if (node.isTextual()) { - list.add(node.asText()); - } else { - list.add(node.toString()); - } - return list; - } - - private static final ResourceLoader RESOURCE_LOADER = new DefaultResourceLoader(); } diff --git a/src/main/java/org/nrg/xnat/security/FilterSecurityInterceptorBeanPostProcessor.java b/src/main/java/org/nrg/xnat/security/FilterSecurityInterceptorBeanPostProcessor.java index 2144dae6..fa23664f 100644 --- a/src/main/java/org/nrg/xnat/security/FilterSecurityInterceptorBeanPostProcessor.java +++ b/src/main/java/org/nrg/xnat/security/FilterSecurityInterceptorBeanPostProcessor.java @@ -13,6 +13,7 @@ package org.nrg.xnat.security; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nrg.xdat.preferences.SiteConfigPreferences; +import org.nrg.xnat.services.XnatAppInfo; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; @@ -25,25 +26,14 @@ import org.springframework.security.web.access.intercept.FilterSecurityIntercept import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; -import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; -import java.util.List; public class FilterSecurityInterceptorBeanPostProcessor implements BeanPostProcessor { @Autowired - public FilterSecurityInterceptorBeanPostProcessor(final SiteConfigPreferences preferences) { + public FilterSecurityInterceptorBeanPostProcessor(final SiteConfigPreferences preferences, final XnatAppInfo appInfo) { _preferences = preferences; - } - - public void setOpenUrls(List<String> openUrls) { - _openUrls.clear(); - _openUrls.addAll(openUrls); - } - - public void setAdminUrls(List<String> adminUrls) { - _adminUrls.clear(); - _adminUrls.addAll(adminUrls); + _appInfo = appInfo; } @Override @@ -70,27 +60,25 @@ public class FilterSecurityInterceptorBeanPostProcessor implements BeanPostProce public ExpressionBasedFilterInvocationSecurityMetadataSource getMetadataSource(boolean requiredLogin) { final LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> map = new LinkedHashMap<>(); - for (final String openUrl : _openUrls) { + for (final AntPathRequestMatcher matcher : _appInfo.getOpenUrls().values()) { if (_log.isDebugEnabled()) { - _log.debug("Setting permitAll on the open URL: " + openUrl); + _log.debug("Setting permitAll on the open URL: " + matcher.getPattern()); } - - map.put(new AntPathRequestMatcher(openUrl), SecurityConfig.createList(PERMIT_ALL)); + map.put(matcher, SecurityConfig.createList(PERMIT_ALL)); } - for (String adminUrl : _adminUrls) { + for (final AntPathRequestMatcher matcher : _appInfo.getAdminUrls().values()) { if (_log.isDebugEnabled()) { - _log.debug("Setting permissions on the admin URL: " + adminUrl); + _log.debug("Setting permissions on the admin URL: " + matcher.getPattern()); } - - map.put(new AntPathRequestMatcher(adminUrl), SecurityConfig.createList(ADMIN_EXPRESSION)); + map.put(matcher, SecurityConfig.createList(ADMIN_EXPRESSION)); } - final String nonopen = requiredLogin ? DEFAULT_EXPRESSION : PERMIT_ALL; + final String secure = requiredLogin ? DEFAULT_EXPRESSION : PERMIT_ALL; if (_log.isDebugEnabled()) { - _log.debug("Setting " + nonopen + " on the default pattern: " + DEFAULT_PATTERN); + _log.debug("Setting " + secure + " on the default pattern: " + DEFAULT_PATTERN); } - map.put(new AntPathRequestMatcher(DEFAULT_PATTERN), SecurityConfig.createList(nonopen)); + map.put(new AntPathRequestMatcher(DEFAULT_PATTERN), SecurityConfig.createList(secure)); return new ExpressionBasedFilterInvocationSecurityMetadataSource(map, new DefaultWebSecurityExpressionHandler()); } @@ -115,7 +103,5 @@ public class FilterSecurityInterceptorBeanPostProcessor implements BeanPostProce private static final String DEFAULT_EXPRESSION = "hasRole('ROLE_USER')"; private final SiteConfigPreferences _preferences; - - private final List<String> _openUrls = new ArrayList<>(); - private final List<String> _adminUrls = new ArrayList<>(); + private final XnatAppInfo _appInfo; } diff --git a/src/main/java/org/nrg/xnat/security/XnatInitCheckFilter.java b/src/main/java/org/nrg/xnat/security/XnatInitCheckFilter.java index d485f0f5..6369a1b1 100644 --- a/src/main/java/org/nrg/xnat/security/XnatInitCheckFilter.java +++ b/src/main/java/org/nrg/xnat/security/XnatInitCheckFilter.java @@ -27,9 +27,6 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; public class XnatInitCheckFilter extends GenericFilterBean { @Autowired @@ -55,7 +52,7 @@ public class XnatInitCheckFilter extends GenericFilterBean { if (isAnonymous) { String header = request.getHeader("Authorization"); - if (header != null && header.startsWith("Basic ") && !isInitializerPath(uri)) { + if (header != null && header.startsWith("Basic ") && !_appInfo.isInitPathRequest(request)) { // Users that authenticated using basic authentication receive an error message informing // them that the system is not yet initialized. response.sendError(HttpServletResponse.SC_FORBIDDEN, "Site has not yet been configured."); @@ -65,13 +62,13 @@ public class XnatInitCheckFilter extends GenericFilterBean { final String referer = request.getHeader("Referer"); - if (isInitializerPath(uri) || - _configurationPathPattern.matcher(uri).matches() || - _nonAdminErrorPathPattern.matcher(uri).matches() || - isExemptedPath(uri)) { + if (_appInfo.isInitPathRequest(request) || + _appInfo.isConfigPathRequest(request) || + _appInfo.isNonAdminErrorPathRequest(request) || + _appInfo.isExemptedPathRequest(request)) { //If you're already on the configuration page, error page, or expired password page, continue on without redirect. chain.doFilter(req, res); - } else if (referer != null && (_configurationPathPattern.matcher(referer).matches() || _nonAdminErrorPathPattern.matcher(referer).matches() || isExemptedPath(referer)) && !uri.contains("/app/template") && !uri.contains("/app/screen") && !uri.endsWith(".vm") && !uri.equals("/")) { + } else if (referer != null && (_appInfo.isConfigPathRequest(referer) || _appInfo.isNonAdminErrorPathRequest(referer) || _appInfo.isExemptedPathRequest(referer)) && !uri.contains("/app/template") && !uri.contains("/app/screen") && !uri.endsWith(".vm") && !uri.equals("/")) { //If you're on a request within the configuration page (or error page or expired password page), continue on without redirect. This checks that the referer is the configuration page and that // the request is not for another page (preventing the user from navigating away from the Configuration page via the menu bar). chain.doFilter(req, res); @@ -85,71 +82,23 @@ public class XnatInitCheckFilter extends GenericFilterBean { final String serverPath = XnatHttpUtils.getServerRoot(request); if (Roles.isSiteAdmin(user)) { if (_log.isWarnEnabled()) { - _log.warn("Admin user {} has logged into the uninitialized server and is being redirected to {}", user.getUsername(), serverPath + _configurationPath); + _log.warn("Admin user {} has logged into the uninitialized server and is being redirected to {}", user.getUsername(), serverPath + _appInfo.getConfigPath()); } //Otherwise, if the user has administrative permissions, direct the user to the configuration page. - response.sendRedirect(serverPath + _configurationPath); + response.sendRedirect(serverPath + _appInfo.getConfigPath()); } else { if (_log.isWarnEnabled()) { - _log.warn("Non-admin user {} has logged into the uninitialized server and is being redirected to {}", user.getUsername(), serverPath + _nonAdminErrorPath); + _log.warn("Non-admin user {} has logged into the uninitialized server and is being redirected to {}", user.getUsername(), serverPath + _appInfo.getNonAdminErrorPath()); } //The system is not initialized but the user does not have administrative permissions. Direct the user to an error page. - response.sendRedirect(serverPath + _nonAdminErrorPath); + response.sendRedirect(serverPath + _appInfo.getNonAdminErrorPath()); } } } } } - public void setInitializationPaths(final List<String> initializationPaths) { - for (final String initializationPath : initializationPaths) { - _initializationPathPatterns.add(Pattern.compile("^(https*://.*)?" + initializationPath + ".*$")); - } - } - - public void setConfigurationPath(String configurationPath) { - _configurationPath = configurationPath; - _configurationPathPattern = Pattern.compile("^(https*://.*)?" + configurationPath + "/*"); - } - - public void setNonAdminErrorPath(String nonAdminErrorPath) { - _nonAdminErrorPath = nonAdminErrorPath; - _nonAdminErrorPathPattern = Pattern.compile("^(https*://.*)?" + nonAdminErrorPath + "/*"); - } - - public void setExemptedPaths(List<String> exemptedPaths) { - _exemptedPaths.clear(); - _exemptedPaths.addAll(exemptedPaths); - } - - private boolean isExemptedPath(final String path) { - for (final String exemptedPath : _exemptedPaths) { - if (path.split("\\?")[0].endsWith(exemptedPath)) { - return true; - } - } - return false; - } - - private boolean isInitializerPath(final String uri) { - for (final Pattern initializationPathPattern : _initializationPathPatterns) { - if (initializationPathPattern.matcher(uri).matches()) { - return true; - } - } - return false; - } - private static Logger _log = LoggerFactory.getLogger(XnatInitCheckFilter.class); private final XnatAppInfo _appInfo; - - private String _configurationPath; - private String _nonAdminErrorPath; - private Pattern _configurationPathPattern; - private Pattern _nonAdminErrorPathPattern; - - private List<Pattern> _initializationPathPatterns = new ArrayList<>(); - - private final List<String> _exemptedPaths = new ArrayList<>(); } diff --git a/src/main/java/org/nrg/xnat/services/XnatAppInfo.java b/src/main/java/org/nrg/xnat/services/XnatAppInfo.java index 363ac008..cf329021 100644 --- a/src/main/java/org/nrg/xnat/services/XnatAppInfo.java +++ b/src/main/java/org/nrg/xnat/services/XnatAppInfo.java @@ -1,19 +1,26 @@ package org.nrg.xnat.services; - +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.nrg.framework.services.SerializerService; import org.nrg.prefs.exceptions.InvalidPreferenceName; import org.nrg.xdat.XDAT; import org.python.google.common.collect.ImmutableMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Component; import javax.inject.Inject; import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.sql.ResultSet; @@ -23,10 +30,13 @@ import java.text.DecimalFormat; import java.util.*; import java.util.jar.Attributes; import java.util.jar.Manifest; +import java.util.regex.Pattern; @Component public class XnatAppInfo { + public static final String NON_RELEASE_VERSION_REGEX = "(?i:^.*(SNAPSHOT|BETA|RC).*$)"; + private static final int MILLISECONDS_IN_A_DAY = (24 * 60 * 60 * 1000); private static final int MILLISECONDS_IN_AN_HOUR = (60 * 60 * 1000); private static final int MILLISECONDS_IN_A_MINUTE = (60 * 1000); @@ -37,10 +47,33 @@ public class XnatAppInfo { private static final String SECONDS = "seconds"; @Inject - public XnatAppInfo(final ServletContext context, final JdbcTemplate template) throws IOException { + public XnatAppInfo(final ServletContext context, final SerializerService serializerService, final JdbcTemplate template) throws IOException { _template = template; + + final Resource configuredUrls = RESOURCE_LOADER.getResource("classpath:META-INF/xnat/security/configured-urls.yaml"); + try (final InputStream inputStream = configuredUrls.getInputStream()) { + final HashMap<String, ArrayList<String>> urlMap = serializerService.deserializeYaml(inputStream, SerializerService.TYPE_REF_MAP_STRING_LIST_STRING); + populate(_openUrls, urlMap.get("openUrls")); + populate(_adminUrls, urlMap.get("adminUrls")); + } + + final Resource initializationUrls = RESOURCE_LOADER.getResource("classpath:META-INF/xnat/security/initialization-urls.yaml"); + try (final InputStream inputStream = initializationUrls.getInputStream()) { + final JsonNode paths = serializerService.deserializeYaml(inputStream); + _configPath = paths.get("configPath").asText(); + _configPathPattern = getPathPattern(_configPath); + _configPathMatcher = new AntPathRequestMatcher(_configPath); + + _nonAdminErrorPath = paths.get("nonAdminErrorPath").asText(); + _nonAdminErrorPathPattern = getPathPattern(_nonAdminErrorPath); + _nonAdminErrorPathMatcher = new AntPathRequestMatcher(_nonAdminErrorPath); + + populate(_initPaths, nodeToList(paths.get("initPaths"))); + populate(_exemptedPaths, nodeToList(paths.get("exemptedPaths"))); + } + try (final InputStream input = context.getResourceAsStream("/META-INF/MANIFEST.MF")) { - final Manifest manifest = new Manifest(input); + final Manifest manifest = new Manifest(input); final Attributes attributes = manifest.getMainAttributes(); _properties.setProperty("buildNumber", attributes.getValue("Build-Number")); _properties.setProperty("buildDate", attributes.getValue("Build-Date")); @@ -99,7 +132,6 @@ public class XnatAppInfo { } } } - } public Map<String, String> getFoundPreferences() { @@ -136,24 +168,21 @@ public class XnatAppInfo { if (_log.isInfoEnabled()) { _log.info("The site was not flagged as initialized and initialized preference set to false. Setting system for initialization."); } - for(String pref: _foundPreferences.keySet()){ + for (String pref : _foundPreferences.keySet()) { _template.update( "UPDATE xhbm_preference SET value = ? WHERE name = ?", new Object[]{_foundPreferences.get(pref), pref}, new int[]{Types.VARCHAR, Types.VARCHAR} - ); + ); try { XDAT.getSiteConfigPreferences().set(_foundPreferences.get(pref), pref); - } - catch(InvalidPreferenceName e){ - _log.error("",e); - } - catch(NullPointerException e){ - _log.error("Error getting site config preferences.",e); + } catch (InvalidPreferenceName e) { + _log.error("", e); + } catch (NullPointerException e) { + _log.error("Error getting site config preferences.", e); } } } - } - catch(EmptyResultDataAccessException e){ + } catch (EmptyResultDataAccessException e) { //Could not find the initialized preference. Site is still not initialized. } @@ -187,7 +216,8 @@ public class XnatAppInfo { * @return The version of the application. */ public String getVersion() { - return _properties.getProperty("version"); + final String version = _properties.getProperty("version"); + return version.matches(NON_RELEASE_VERSION_REGEX) ? version + " (build " + getBuildNumber() + " on " + getBuildDate() + ")" : version; } /** @@ -244,12 +274,12 @@ public class XnatAppInfo { * @return A map of values indicating the system uptime. */ public Map<String, String> getUptime() { - final long diff = new Date().getTime() - _startTime.getTime(); - final int days = (int) (diff / MILLISECONDS_IN_A_DAY); - final long daysRemainder = diff % MILLISECONDS_IN_A_DAY; - final int hours = (int) (daysRemainder / MILLISECONDS_IN_AN_HOUR); - final long hoursRemainder = daysRemainder % MILLISECONDS_IN_AN_HOUR; - final int minutes = (int) (hoursRemainder / MILLISECONDS_IN_A_MINUTE); + final long diff = new Date().getTime() - _startTime.getTime(); + final int days = (int) (diff / MILLISECONDS_IN_A_DAY); + final long daysRemainder = diff % MILLISECONDS_IN_A_DAY; + final int hours = (int) (daysRemainder / MILLISECONDS_IN_AN_HOUR); + final long hoursRemainder = daysRemainder % MILLISECONDS_IN_AN_HOUR; + final int minutes = (int) (hoursRemainder / MILLISECONDS_IN_A_MINUTE); final long minutesRemainder = hoursRemainder % MILLISECONDS_IN_A_MINUTE; final Map<String, String> uptime = new HashMap<>(); @@ -274,7 +304,7 @@ public class XnatAppInfo { */ public String getFormattedUptime() { final Map<String, String> uptime = getUptime(); - final StringBuilder buffer = new StringBuilder(); + final StringBuilder buffer = new StringBuilder(); if (uptime.containsKey(DAYS)) { buffer.append(uptime.get(DAYS)).append(" days, "); } @@ -297,18 +327,135 @@ public class XnatAppInfo { return ImmutableMap.copyOf(_plugins); } + /** + * Gets the path where XNAT found its primary configuration file. + * + * @return The path where XNAT found its primary configuration file. + */ + public String getConfigPath() { + return _configPath; + } + + /** + * Gets the path where non-admin users should be sent when errors occur that require administrator intervention. + * + * @return Non-admin users error path. + */ + public String getNonAdminErrorPath() { + return _nonAdminErrorPath; + } + + /** + * Gets the URLs available to all users, including anonymous users. + * + * @return A set of the system's open URLs. + */ + public HashMap<String, AntPathRequestMatcher> getOpenUrls() { + return new HashMap<>(_openUrls); + } + + /** + * Gets the URLs available only to administrators. + * + * @return A set of administrator-only URLs. + */ + public HashMap<String, AntPathRequestMatcher> getAdminUrls() { + return new HashMap<>(_adminUrls); + } + + public boolean isOpenUrlRequest(final HttpServletRequest request) { + return checkUrls(request, _openUrls.values()); + } + + public boolean isInitPathRequest(final HttpServletRequest request) { + return checkUrls(request, _initPaths.values()); + } + + public boolean isExemptedPathRequest(final HttpServletRequest request) { + return checkUrls(request, _exemptedPaths.values()); + } + + public boolean isExemptedPathRequest(final String path) { + for (final String exemptedPath : _exemptedPaths.keySet()) { + if (path.split("\\?")[0].endsWith(exemptedPath)) { + return true; + } + } + return false; + } + + public boolean isConfigPathRequest(final HttpServletRequest request) { + return _configPathMatcher.matches(request); + } + + public boolean isConfigPathRequest(final String path) { + return _configPathPattern.matcher(path).matches(); + } + + public boolean isNonAdminErrorPathRequest(final HttpServletRequest request) { + return _nonAdminErrorPathMatcher.matches(request); + } + + public boolean isNonAdminErrorPathRequest(final String path) { + return _nonAdminErrorPathPattern.matcher(path).matches(); + } + + private List<String> nodeToList(final JsonNode node) { + final List<String> list = new ArrayList<>(); + if (node.isArray()) { + final ArrayNode arrayNode = (ArrayNode) node; + for (final JsonNode item : arrayNode) { + list.add(item.asText()); + } + } else if (node.isTextual()) { + list.add(node.asText()); + } else { + list.add(node.toString()); + } + return list; + } + + private void populate(final Map<String, AntPathRequestMatcher> matchers, final List<String> urls) { + for (final String url : urls) { + matchers.put(url, new AntPathRequestMatcher(url)); + } + } + + private boolean checkUrls(final HttpServletRequest request, final Collection<AntPathRequestMatcher> matchers) { + for (final AntPathRequestMatcher matcher : matchers) { + if (matcher.matches(request)) { + return true; + } + } + return false; + } + + private Pattern getPathPattern(final String path) { + return Pattern.compile("^(https*://.*)?" + path + "/*"); + } + private static final Logger _log = LoggerFactory.getLogger(XnatAppInfo.class); - private static final List<String> PRIMARY_MANIFEST_ATTRIBUTES = Arrays.asList("Build-Number", "Build-Date", "Implementation-Version", "Implementation-Sha"); + private static final List<String> PRIMARY_MANIFEST_ATTRIBUTES = Arrays.asList("Build-Number", "Build-Date", "Implementation-Version", "Implementation-Sha"); + private static final ResourceLoader RESOURCE_LOADER = new DefaultResourceLoader(); private final JdbcTemplate _template; - private final Map<String, String> _foundPreferences = new HashMap<>(); + private final String _configPath; + private final Pattern _configPathPattern; + private final AntPathRequestMatcher _configPathMatcher; + private final String _nonAdminErrorPath; + private final Pattern _nonAdminErrorPathPattern; + private final AntPathRequestMatcher _nonAdminErrorPathMatcher; - private final Date _startTime = new Date(); - private final Properties _properties = new Properties(); - private final Map<String, Map<String, String>> _attributes = new HashMap<>(); - private boolean _initialized = false; - private final Map<String, Properties> _plugins = new HashMap<>(); + private final Map<String, AntPathRequestMatcher> _openUrls = new HashMap<>(); + private final Map<String, AntPathRequestMatcher> _adminUrls = new HashMap<>(); + private final Map<String, AntPathRequestMatcher> _initPaths = new HashMap<>(); + private final Map<String, AntPathRequestMatcher> _exemptedPaths = new HashMap<>(); + private final Map<String, String> _foundPreferences = new HashMap<>(); + private final Date _startTime = new Date(); + private final Properties _properties = new Properties(); + private final Map<String, Map<String, String>> _attributes = new HashMap<>(); + private boolean _initialized = false; + private final Map<String, Properties> _plugins = new HashMap<>(); } - diff --git a/src/main/resources/META-INF/xnat/security/configured-urls.yaml b/src/main/resources/META-INF/xnat/security/configured-urls.yaml index f86e9aa0..8431e00e 100644 --- a/src/main/resources/META-INF/xnat/security/configured-urls.yaml +++ b/src/main/resources/META-INF/xnat/security/configured-urls.yaml @@ -23,6 +23,7 @@ openUrls: - /data/services/sendemailverification* - /REST/services/sendemailverification* - /xapi/siteConfig/buildInfo + - /xapi/siteConfig/siteWideAlert* - /images/** - /scripts/** - /style/** -- GitLab