-
-
-
-
- {this.renderSecondaryNavBar()}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ {this.renderSecondaryNavBar()}
+
-
+
);
}
}
diff --git a/frontend/styleguide.config.js b/frontend/styleguide.config.js
new file mode 100644
index 0000000..ab4d853
--- /dev/null
+++ b/frontend/styleguide.config.js
@@ -0,0 +1,99 @@
+const path = require('path');
+const ExtractTextPlugin = require('extract-text-webpack-plugin'); // eslint-disable-line import/no-extraneous-dependencies
+const merge = require('webpack-merge'); // eslint-disable-line import/no-extraneous-dependencies
+const appWebpackConfig = require('./webpack.config');
+// const MiniHtmlWebpackPlugin = require('mini-html-webpack-plugin');
+
+// const config = {
+// plugins: [
+// new MiniHtmlWebpackPlugin({
+// context: {
+// title: 'Webpack demo'
+// },
+// filename: 'demo.html' // Optional, defaults to `index.html`
+// })
+// ]
+// };
+
+const ourWebpackConfig = {
+ node: {
+ fs: 'empty',
+ },
+ // module: {
+ // rules: [
+ // {
+ // test: /\.less$/,
+ // use: ExtractTextPlugin.extract({
+ // fallback: 'style-loader',
+ // use: ['css-loader', 'less-loader'],
+ // }),
+ // },
+ // {
+ // test: /\.css$/,
+ // use: ExtractTextPlugin.extract({
+ // fallback: 'style-loader',
+ // use: ['css-loader'],
+ // }),
+ // },
+
+ // ],
+ // },
+};
+
+const mergedWebpackConfig = merge(appWebpackConfig, ourWebpackConfig);
+
+module.exports = {
+ title: 'Attivio Search UI Component Reference',
+ verbose: true,
+ assetsDir: 'docs/static',
+ // template: () => {
+ // return `
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // `;
+ // },
+ ignore: [], // Add any componets we want to exclude here
+ defaultExample: false,
+ usageMode: 'expand',
+ styleguideDir: 'styleguide',
+ editorConfig: {
+ theme: 'ambiance', // see http://codemirror.net/demo/theme.html
+ },
+ styles: {},
+
+ sections: [
+ {
+ name: 'Components',
+ components: () => {
+ return [
+ 'src/components/DummyComp.js',
+ ];
+ },
+ },
+ ],
+ require: [
+ path.join(__dirname, 'src/style/main.less'),
+ ],
+ getComponentPathLine(componentPath) {
+ const name = path.basename(componentPath, '.js');
+ // const dir = path.dirname(componentPath);
+ return `import ${name} from '../components/${name}.js';`;
+ },
+ getExampleFilename(componentPath) {
+ const name = path.basename(componentPath, '.js');
+ const mdName = `${name}.md`;
+ const dir = path.dirname(componentPath);
+ const fullMdPath = path.resolve(dir, '../../docs/components', mdName);
+ return fullMdPath;
+ },
+ webpackConfig: mergedWebpackConfig,
+};
diff --git a/servlet/application.properties b/servlet/application.properties
index 376fcd2..8af87f7 100644
--- a/servlet/application.properties
+++ b/servlet/application.properties
@@ -60,12 +60,30 @@ suit.attivio.password=attivio
# ignored and this is appended as a parameter to the URI used for forwarding REST requests.
#suit.attivio.authToken=
+# If you will be accessing the REST API endpoints from a different server (e.g., a custom
+# web application), you will need to specify that application's server's URI here (may
+# be a comma-separated list if there are multiple origins). This allows cross-origin (CORS)
+# access to the servlet.
+#suit.attivio.corsOrigins=*
+
+# If you are setting the CORS origin, you can choose to also specify only certain methods
+# for CORS access. By default, the servlet will allow access to all HTTP methods ("*") so
+# you can generally leave this property commented-out.
+#suit.attivio.corsMethods=
+
+
##################################################################################
# Search UI Application Configuration
# Use these properties to tell the servlet about your Search UI application.
##################################################################################
+# This property allows you to disable serving of the full Search UI application that
+# is included in the servlet. By default, users can access it at the servlet's
+# context path (e.g. /searchui/) but if you set this property to false, accessing that
+# URI will produce a 404 ("not found") error.
+#suit.attivio.serveSearchUI=false
+
# These are the routes to allow for your application. Trying to access any other
# URLs will result in the servlet trying to serve static resources, such as images
# or the main HTML file. Generally, the list of routes here should match the
diff --git a/servlet/pom.xml b/servlet/pom.xml
index c43aef8..899a67b 100755
--- a/servlet/pom.xml
+++ b/servlet/pom.xml
@@ -57,6 +57,12 @@
spring-boot-starter-tomcat
provided
+
+ junit
+ junit
+ 4.10
+ test
+
org.apache.httpcomponents
httpclient
diff --git a/servlet/src/main/java/com/attivio/suitback/config/MvcConfig.java b/servlet/src/main/java/com/attivio/suitback/config/MvcConfig.java
deleted file mode 100755
index 81faa1c..0000000
--- a/servlet/src/main/java/com/attivio/suitback/config/MvcConfig.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
-* Copyright 2018 Attivio Inc., All rights reserved.
-*/
-package com.attivio.suitback.config;
-
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.servlet.config.annotation.CorsRegistry;
-import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
-
-@Configuration
-public class MvcConfig extends WebMvcConfigurerAdapter {
- @Override
- public void addCorsMappings(CorsRegistry registry) {
- registry.addMapping("/**")
- .allowedOrigins("*")
- .allowedMethods("GET", "POST", "PUT", "DELETE", "HEAD")
- .allowedHeaders("*")
- .allowCredentials(true);
- }
-}
diff --git a/servlet/src/main/java/com/attivio/suitback/config/SecurityConfigBasic.java b/servlet/src/main/java/com/attivio/suitback/config/SecurityConfigBasic.java
index 4aa1eb0..df2fa81 100644
--- a/servlet/src/main/java/com/attivio/suitback/config/SecurityConfigBasic.java
+++ b/servlet/src/main/java/com/attivio/suitback/config/SecurityConfigBasic.java
@@ -5,11 +5,13 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.web.access.channel.ChannelProcessingFilter;
@Configuration
@Profile("!saml")
@@ -36,6 +38,13 @@ public class SecurityConfigBasic extends WebSecurityConfigurerAdapter {
"/**/*.ico"
};
+ @Value("${suit.attivio.corsOrigins:*}")
+ String corsOrigins;
+
+ @Value("${suit.attivio.corsMethods:*}")
+ String corsMethods;
+
+
@Override
public void configure(WebSecurity web) throws Exception {
// The REST API, the special sockjs-node URLs, and any static files are NOT to be authenticated
@@ -54,6 +63,7 @@ protected void configure(HttpSecurity http) throws Exception {
.and()
.httpBasic()
.disable()
+ .addFilterBefore(new WebSecurityCorsFilter(corsOrigins, corsMethods), ChannelProcessingFilter.class)
.csrf()
.disable()
.authorizeRequests()
diff --git a/servlet/src/main/java/com/attivio/suitback/config/SecurityConfigSAML.java b/servlet/src/main/java/com/attivio/suitback/config/SecurityConfigSAML.java
index 37124eb..95324ef 100644
--- a/servlet/src/main/java/com/attivio/suitback/config/SecurityConfigSAML.java
+++ b/servlet/src/main/java/com/attivio/suitback/config/SecurityConfigSAML.java
@@ -13,6 +13,7 @@
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.saml.storage.EmptyStorageFactory;
import org.springframework.security.saml.websso.WebSSOProfileConsumerImpl;
+import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import com.github.ulisesbocchio.spring.boot.security.saml.bean.SAMLConfigurerBean;
import com.github.ulisesbocchio.spring.boot.security.saml.configurer.ServiceProviderBuilder;
@@ -67,6 +68,12 @@ public class SecurityConfigSAML extends ServiceProviderConfigurerAdapter {
@Value("${saml.sso.maxAssertionTime:3000}")
int maxAssertionTime;
+ @Value("${suit.attivio.corsOrigins:*}")
+ String corsOrigins;
+
+ @Value("${suit.attivio.corsMethods:*}")
+ String corsMethods;
+
@Autowired
SAMLConfigurerBean samlConfigurer;
@@ -175,6 +182,7 @@ public void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.disable()
+ .addFilterBefore(new WebSecurityCorsFilter(corsOrigins, corsMethods), ChannelProcessingFilter.class)
.csrf()
.disable()
.anonymous()
diff --git a/servlet/src/main/java/com/attivio/suitback/config/WebSecurityCorsFilter.java b/servlet/src/main/java/com/attivio/suitback/config/WebSecurityCorsFilter.java
new file mode 100644
index 0000000..9fea775
--- /dev/null
+++ b/servlet/src/main/java/com/attivio/suitback/config/WebSecurityCorsFilter.java
@@ -0,0 +1,103 @@
+/**
+* Copyright 2018 Attivio Inc., All rights reserved.
+*/
+package com.attivio.suitback.config;
+
+import java.io.IOException;
+import java.util.Enumeration;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WebSecurityCorsFilter implements Filter {
+ static final Logger LOG = LoggerFactory.getLogger(WebSecurityCorsFilter.class);
+
+ private String corsOrigins;
+ private String corsMethods;
+
+ public WebSecurityCorsFilter(String corsOrigins, String corsMethods) {
+ this.corsOrigins = corsOrigins;
+ this.corsMethods = corsMethods;
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ /**
+ * Based on the configuration and the incoming request, decide what to return
+ * for the value of the Access-Control-Allow-Origin header, if anything.
+ *
+ * @param request the incoming request
+ * @return the origin to return or null
if not configured or nothing matches
+ */
+ String getAllowedCorsOrigin(String comingFrom) {
+ String originToReturn = null;
+ // Special case of "*"
+ if ("*".equals(corsOrigins)) {
+ originToReturn = "*";
+ } else {
+ String[] origins = corsOrigins.split(",");
+ for (String testOrigin : origins) {
+ if (testOrigin.trim().equals(comingFrom)) {
+ // The actual origin matches one of the items in the corsOrigins list
+ originToReturn = comingFrom;
+ break;
+ }
+ }
+ }
+
+ LOG.trace("Incoming origin is " + comingFrom + ". (Allowed origins are: " + corsOrigins + ".)");
+ if (originToReturn != null) {
+ LOG.trace("Returning Access-Control-Allow-Origin header with value " + originToReturn + ".");
+ } else {
+ LOG.trace("Returning NO Access-Control-Allow-Origin header.");
+ }
+ return originToReturn;
+ }
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest req = (HttpServletRequest)request;
+
+ Enumeration origins = req.getHeaders("Origin");
+ String originToReturn = null;
+ if (origins != null) {
+ if (origins.hasMoreElements()) {
+ String origin = origins.nextElement();
+ if (origins.hasMoreElements()) {
+ String queryString = (req.getQueryString() != null) ? ("?" + req.getQueryString()) : "";
+ String requestedURL = req.getRequestURL().append(queryString).toString();
+ LOG.warn("Being called with multiple Origin headers set. URI: " + requestedURL);
+ } else {
+ originToReturn = getAllowedCorsOrigin(origin);
+ if (originToReturn != null) {
+ }
+ }
+ }
+ }
+
+ HttpServletResponse res = (HttpServletResponse)response;
+ if (originToReturn != null) {
+ res.setHeader("Access-Control-Allow-Origin", originToReturn);
+ }
+ res.setHeader("Access-Control-Allow-Methods", corsMethods);
+ res.setHeader("Access-Control-Max-Age", "3600");
+ res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, x-requested-with, Cache-Control");
+ res.setHeader("Access-Control-Allow-Credentials", "true");
+ chain.doFilter(request, res);
+ }
+
+ @Override
+ public void destroy() {
+ }
+}
diff --git a/servlet/src/main/java/com/attivio/suitback/controllers/ConfigController.java b/servlet/src/main/java/com/attivio/suitback/controllers/ConfigController.java
index e12f564..b41c0b6 100644
--- a/servlet/src/main/java/com/attivio/suitback/controllers/ConfigController.java
+++ b/servlet/src/main/java/com/attivio/suitback/controllers/ConfigController.java
@@ -13,11 +13,9 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
-@CrossOrigin
@Controller
public class ConfigController {
// Add the property logging.level.com.attivio.suitback.controllers.ConfigController
diff --git a/servlet/src/main/java/com/attivio/suitback/controllers/HomeController.java b/servlet/src/main/java/com/attivio/suitback/controllers/HomeController.java
index 3b0261e..9ea2019 100755
--- a/servlet/src/main/java/com/attivio/suitback/controllers/HomeController.java
+++ b/servlet/src/main/java/com/attivio/suitback/controllers/HomeController.java
@@ -6,20 +6,33 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.httpclient.HttpStatus;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
-import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
-@CrossOrigin
@Component
public class HomeController extends AbstractController {
+
+ /**
+ * If this is set to false
, then the Search UI application won't
+ * be served by the servlet. The default value of true
means
+ * that it will be.
+ */
+ @Value("${suit.attivio.serveSearchUI:true}")
+ boolean serveSearchUI;
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
- // Don't cache the HTML file because it is used to force the SAML log in
- response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
- response.addHeader("Pragma", "no-cache");
- return new ModelAndView("index.html");
+ if (serveSearchUI) {
+ // Don't cache the HTML file because it is used to force the SAML log in
+ response.addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.addHeader("Pragma", "no-cache");
+ return new ModelAndView("index.html");
+ } else {
+ response.sendError(HttpStatus.SC_NOT_FOUND, "Serving the Search UI application has been disabled.");
+ return null;
+ }
}
}
diff --git a/servlet/src/main/java/com/attivio/suitback/controllers/LogController.java b/servlet/src/main/java/com/attivio/suitback/controllers/LogController.java
index 3e8100f..cc985ef 100644
--- a/servlet/src/main/java/com/attivio/suitback/controllers/LogController.java
+++ b/servlet/src/main/java/com/attivio/suitback/controllers/LogController.java
@@ -6,11 +6,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
-@CrossOrigin
@Controller
public class LogController {
static final Logger LOG = LoggerFactory.getLogger("SUIT Servlet");
diff --git a/servlet/src/main/java/com/attivio/suitback/controllers/LoginController.java b/servlet/src/main/java/com/attivio/suitback/controllers/LoginController.java
new file mode 100644
index 0000000..55fa8a3
--- /dev/null
+++ b/servlet/src/main/java/com/attivio/suitback/controllers/LoginController.java
@@ -0,0 +1,38 @@
+/**
+* Copyright 2018 Attivio Inc., All rights reserved.
+*/
+package com.attivio.suitback.controllers;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+@Controller
+public class LoginController {
+
+ /**
+ * Callers can access this method to ensure that they are logged
+ * into the configured security, if any (e.g., for SAML authentication).
+ * If the user is already logged in or succeeds in logging in now via
+ * redirects from the SAML identity provider, then the user is
+ * redirected to the target URI. Essentially, this method is a no-op
+ * for the user if already logged in. Generally, a client application
+ * that is hosted outside this servlet should be able to redirect to
+ * this method if an Ajax call fails to execute properly—doing so
+ * should have the effect of either re-upping the session or re-logging
+ * the user in
+ *
+ * @param response
+ * @param targetUri
+ * @throws IOException
+ */
+ @RequestMapping("/rest/login")
+ public void login(HttpServletResponse response,
+ @RequestParam(value = "uri", required = true) String targetUri) throws IOException {
+ response.sendRedirect(targetUri);
+ }
+}
diff --git a/servlet/src/main/java/com/attivio/suitback/controllers/RestProxy.java b/servlet/src/main/java/com/attivio/suitback/controllers/RestProxy.java
index bec9434..d17cc79 100755
--- a/servlet/src/main/java/com/attivio/suitback/controllers/RestProxy.java
+++ b/servlet/src/main/java/com/attivio/suitback/controllers/RestProxy.java
@@ -40,7 +40,6 @@
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@@ -52,7 +51,6 @@
import com.attivio.suitback.controllers.UserController.UserDetails;
import com.google.gson.Gson;
-@CrossOrigin
@Controller
public class RestProxy {
static final String API_KEY_PARAM = "apikey";
@@ -193,7 +191,7 @@ public ResponseEntity mirrorRest(@RequestBody(required=false) String bod
ResponseEntity responseEntity = null;
- LOG.trace("Proxying REST API call from '" + request.getRequestURL().toString() +
+ LOG.trace("Proxying REST API call (" + method.toString() + ") from '" + request.getRequestURL().toString() +
(request.getQueryString() != null ? ("?" + request.getQueryString()) : "") + "' to '" + uri.toString() + "'");
try {
diff --git a/servlet/src/main/java/com/attivio/suitback/controllers/UserController.java b/servlet/src/main/java/com/attivio/suitback/controllers/UserController.java
index ef47fa9..cb1a10d 100755
--- a/servlet/src/main/java/com/attivio/suitback/controllers/UserController.java
+++ b/servlet/src/main/java/com/attivio/suitback/controllers/UserController.java
@@ -11,11 +11,9 @@
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
-@CrossOrigin
@Controller
public class UserController {
// Add the property logging.level.com.attivio.suitback.controllers.UserController
diff --git a/servlet/src/test/java/com/attivio/suitback/config/WebSecurityCorsFilterTest.java b/servlet/src/test/java/com/attivio/suitback/config/WebSecurityCorsFilterTest.java
new file mode 100644
index 0000000..871bedf
--- /dev/null
+++ b/servlet/src/test/java/com/attivio/suitback/config/WebSecurityCorsFilterTest.java
@@ -0,0 +1,38 @@
+/**
+* Copyright 2018 Attivio Inc., All rights reserved.
+*/
+package com.attivio.suitback.config;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class WebSecurityCorsFilterTest {
+
+ @Test
+ public void testGetAllowedCorsOrigin() {
+ WebSecurityCorsFilter filter;
+
+ // Test the asterisk case
+ filter = new WebSecurityCorsFilter("*", "");
+ assertEquals(filter.getAllowedCorsOrigin("*"), "*");
+ assertEquals(filter.getAllowedCorsOrigin("http://myhost:17000"), "*");
+
+ // Test with a single value
+ filter = new WebSecurityCorsFilter("http://myhost:17000", "");
+ assertEquals(filter.getAllowedCorsOrigin("http://myhost:17000"), "http://myhost:17000");
+ assertEquals(filter.getAllowedCorsOrigin("http://shadyhost:666"), null);
+
+ // Test with multiple values with commas and spaces
+ filter = new WebSecurityCorsFilter("http://myhost:17000, http://yourhost:8080", "");
+ assertEquals(filter.getAllowedCorsOrigin("http://myhost:17000"), "http://myhost:17000");
+ assertEquals(filter.getAllowedCorsOrigin("http://yourhost:8080"), "http://yourhost:8080");
+ assertEquals(filter.getAllowedCorsOrigin("http://shadyhost:666"), null);
+
+ // Test with multiple values with commas and no spaces
+ filter = new WebSecurityCorsFilter("http://myhost:17000, http://yourhost:8080", "");
+ assertEquals(filter.getAllowedCorsOrigin("http://myhost:17000"), "http://myhost:17000");
+ assertEquals(filter.getAllowedCorsOrigin("http://yourhost:8080"), "http://yourhost:8080");
+ assertEquals(filter.getAllowedCorsOrigin("http://shadyhost:666"), null);
+ }
+}