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 1b74e2faa9a1078df4dc5ceae2f1806af52bfe8c..d2f839d5a876efda6046f1a48015db8703f93fff 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 f26e745a96c4550c22d5fe0ae11c40f5f2aa9bc9..4c9aa1efc4a1d359dd05065880d073267cd57368 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 68b90f6529397f49d6cef304eb6a9a02683a272c..6d4530e3fcc1abfbd209909a3f0c7d874cea89ff 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 2144dae6ecb6583b0a74eea5418cb7b39bb89127..fa23664f8c7d7ae5ee2551c519714e8fdad4762c 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 d485f0f5d70daaafd53b01df64e999bcae73193a..6369a1b1acf968cdcf998899d85aa0e5ca8b1fa9 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 363ac008e9bbd3ff559b850fc3faf987a6850a1c..cf3290213e78a6f5c4272811fedfd77c763b4c27 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 f86e9aa027318ae3019d35b5443d56cd1dda967f..8431e00e0c58e8a3c1ca2369332160e089099870 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/**