From 9687134531094e3c65e7734d4c1620cdc4b24bf8 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Thu, 8 Sep 2016 12:18:59 +0200 Subject: [PATCH 01/23] Fix for the unwanted merging of demo and custom configurations --- .../java/eu/openanalytics/ShinyProxyApplication.java | 9 ++++++++- .../resources/{application.yml => application-demo.yml} | 6 +----- .../eu/openanalytics/ShinyProxyApplicationTests.java | 3 ++- 3 files changed, 11 insertions(+), 7 deletions(-) rename src/main/resources/{application.yml => application-demo.yml} (91%) diff --git a/src/main/java/eu/openanalytics/ShinyProxyApplication.java b/src/main/java/eu/openanalytics/ShinyProxyApplication.java index 9002c0ed..ea552aa2 100644 --- a/src/main/java/eu/openanalytics/ShinyProxyApplication.java +++ b/src/main/java/eu/openanalytics/ShinyProxyApplication.java @@ -16,6 +16,8 @@ package eu.openanalytics; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; import javax.inject.Inject; @@ -60,7 +62,12 @@ public class ShinyProxyApplication { Environment environment; public static void main(String[] args) { - SpringApplication.run(new Class[] { ShinyProxyApplication.class }, args); + SpringApplication app = new SpringApplication(ShinyProxyApplication.class); + + boolean hasExternalConfig = Files.exists(Paths.get("application.yml")); + if (!hasExternalConfig) app.setAdditionalProfiles("demo"); + + app.run(args); } @Bean diff --git a/src/main/resources/application.yml b/src/main/resources/application-demo.yml similarity index 91% rename from src/main/resources/application.yml rename to src/main/resources/application-demo.yml index 53567424..2cd02fa4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application-demo.yml @@ -33,11 +33,7 @@ shiny: docker-cmd: ["R", "-e shinyproxy::run_06_tabsets()"] docker-image: openanalytics/shinyproxy-demo ldap-groups: scientists -# useful when debugging; set to 'true' when running in production -spring: - thymeleaf: - cache: true -# logging + logging: file: shinyproxy.log \ No newline at end of file diff --git a/src/test/java/eu/openanalytics/ShinyProxyApplicationTests.java b/src/test/java/eu/openanalytics/ShinyProxyApplicationTests.java index 236eaace..bad7f0c9 100644 --- a/src/test/java/eu/openanalytics/ShinyProxyApplicationTests.java +++ b/src/test/java/eu/openanalytics/ShinyProxyApplicationTests.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @@ -34,9 +35,9 @@ @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = ShinyProxyApplication.class) @WebAppConfiguration +@ActiveProfiles("demo") public class ShinyProxyApplicationTests { - @Inject public DockerService dockerService; From e1843064f16f9507ba5706839bdd0dab5443986a Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Thu, 8 Sep 2016 13:00:59 +0200 Subject: [PATCH 02/23] Fixed issue with heartbeatService cleaning up active containers --- .../openanalytics/components/LogoutHandler.java | 16 ++++++---------- .../openanalytics/services/HeartbeatService.java | 4 ++++ .../eu/openanalytics/services/UserService.java | 11 +++++++++++ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/main/java/eu/openanalytics/components/LogoutHandler.java b/src/main/java/eu/openanalytics/components/LogoutHandler.java index 0b6de36c..8ef7778d 100644 --- a/src/main/java/eu/openanalytics/components/LogoutHandler.java +++ b/src/main/java/eu/openanalytics/components/LogoutHandler.java @@ -27,7 +27,7 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; -import eu.openanalytics.services.DockerService; +import eu.openanalytics.services.UserService; /** * @author Torkild U. Resheim, Itema AS @@ -36,17 +36,13 @@ public class LogoutHandler implements LogoutSuccessHandler { @Inject - DockerService dockerService; + UserService userService; @Override - public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) - throws IOException, ServletException { - if (authentication != null) { - Object principal = authentication.getPrincipal(); - if (principal instanceof LdapUserDetails) { - String username = ((LdapUserDetails)principal).getUsername(); - dockerService.releaseProxy(username); - } else throw new UnsupportedOperationException("Unknown principal type "+principal.getClass().toString()); + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + if (authentication != null && authentication.getPrincipal() instanceof LdapUserDetails) { + String userName = ((LdapUserDetails) authentication.getPrincipal()).getUsername(); + userService.onLogout(userName); } response.sendRedirect("/"); } diff --git a/src/main/java/eu/openanalytics/services/HeartbeatService.java b/src/main/java/eu/openanalytics/services/HeartbeatService.java index caeaca09..80db4df9 100644 --- a/src/main/java/eu/openanalytics/services/HeartbeatService.java +++ b/src/main/java/eu/openanalytics/services/HeartbeatService.java @@ -35,6 +35,10 @@ public void heartbeatReceived(String user, String app) { heartbeatTimestamps.put(user, System.currentTimeMillis()); } + public void clearHeartbeat(String user) { + heartbeatTimestamps.remove(user); + } + private class AppCleaner implements Runnable { @Override public void run() { diff --git a/src/main/java/eu/openanalytics/services/UserService.java b/src/main/java/eu/openanalytics/services/UserService.java index 2a271dbf..e5b61eed 100644 --- a/src/main/java/eu/openanalytics/services/UserService.java +++ b/src/main/java/eu/openanalytics/services/UserService.java @@ -19,6 +19,12 @@ public class UserService implements ApplicationListener Date: Thu, 8 Sep 2016 14:50:15 +0200 Subject: [PATCH 03/23] Added an env var named SHINYPROXY_USERNAME to the docker environment --- src/main/java/eu/openanalytics/services/DockerService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/eu/openanalytics/services/DockerService.java b/src/main/java/eu/openanalytics/services/DockerService.java index cba7c41e..9d2941a2 100644 --- a/src/main/java/eu/openanalytics/services/DockerService.java +++ b/src/main/java/eu/openanalytics/services/DockerService.java @@ -225,6 +225,7 @@ private Proxy startProxy(String userName, String appName) { .image(app.getDockerImage()) .exposedPorts("3838") .cmd(app.getDockerCmd()) + .env(String.format("SHINYPROXY_USERNAME=%s", userName)) .build(); ContainerCreation container = dockerClient.createContainer(containerConfig); From 6fa89e4724af715bb9e222887235cc59b4b53750 Mon Sep 17 00:00:00 2001 From: tverbeke Date: Fri, 9 Sep 2016 18:39:22 +0200 Subject: [PATCH 04/23] 0.5.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6d04ce98..e550e12b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ eu.openanalytics shinyproxy - 0.4.0 + 0.5.0 jar shinyproxy From 4ec6486d24e277fdac3fee481c69b695295fc396 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Fri, 7 Oct 2016 16:13:54 +0200 Subject: [PATCH 05/23] Adding event model to track and save usage statistics --- .../openanalytics/ShinyProxyApplication.java | 4 -- .../components/LogoutHandler.java | 2 +- .../components/UsageStatsCollector.java | 70 +++++++++++++++++++ .../controllers/HeartbeatController.java | 6 +- .../openanalytics/services/DockerService.java | 7 ++ .../openanalytics/services/EventService.java | 57 +++++++++++++++ .../services/HeartbeatService.java | 70 ------------------- .../openanalytics/services/UserService.java | 60 ++++++++++++++-- 8 files changed, 193 insertions(+), 83 deletions(-) create mode 100644 src/main/java/eu/openanalytics/components/UsageStatsCollector.java create mode 100644 src/main/java/eu/openanalytics/services/EventService.java delete mode 100644 src/main/java/eu/openanalytics/services/HeartbeatService.java diff --git a/src/main/java/eu/openanalytics/ShinyProxyApplication.java b/src/main/java/eu/openanalytics/ShinyProxyApplication.java index ea552aa2..7be2a7bd 100644 --- a/src/main/java/eu/openanalytics/ShinyProxyApplication.java +++ b/src/main/java/eu/openanalytics/ShinyProxyApplication.java @@ -31,7 +31,6 @@ import org.springframework.core.env.Environment; import org.springframework.scheduling.annotation.EnableAsync; -import eu.openanalytics.services.AppService; import eu.openanalytics.services.DockerService; import eu.openanalytics.services.DockerService.MappingListener; import io.undertow.Handlers; @@ -55,9 +54,6 @@ public class ShinyProxyApplication { @Inject DockerService dockerService; - @Inject - AppService appService; - @Inject Environment environment; diff --git a/src/main/java/eu/openanalytics/components/LogoutHandler.java b/src/main/java/eu/openanalytics/components/LogoutHandler.java index 8ef7778d..9c5f5536 100644 --- a/src/main/java/eu/openanalytics/components/LogoutHandler.java +++ b/src/main/java/eu/openanalytics/components/LogoutHandler.java @@ -42,7 +42,7 @@ public class LogoutHandler implements LogoutSuccessHandler { public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { if (authentication != null && authentication.getPrincipal() instanceof LdapUserDetails) { String userName = ((LdapUserDetails) authentication.getPrincipal()).getUsername(); - userService.onLogout(userName); + userService.logout(userName); } response.sendRedirect("/"); } diff --git a/src/main/java/eu/openanalytics/components/UsageStatsCollector.java b/src/main/java/eu/openanalytics/components/UsageStatsCollector.java new file mode 100644 index 00000000..1d6380e2 --- /dev/null +++ b/src/main/java/eu/openanalytics/components/UsageStatsCollector.java @@ -0,0 +1,70 @@ +package eu.openanalytics.components; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Optional; +import java.util.function.Consumer; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; + +import org.apache.log4j.Logger; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import eu.openanalytics.services.EventService; +import eu.openanalytics.services.EventService.Event; + +@Component +public class UsageStatsCollector implements Consumer { + + private Logger log = Logger.getLogger(UsageStatsCollector.class); + + @Inject + Environment environment; + + @Inject + EventService eventService; + + private String baseURL; + private String dbName; + + @PostConstruct + public void init() { + baseURL = environment.getProperty("shiny.proxy.usage-stats-url"); + dbName = environment.getProperty("shiny.proxy.usage-stats-db"); + if (baseURL == null || dbName == null) { + log.info("Disabled. Usage statistics will not be posted."); + } else { + eventService.addListener(this); + log.info(String.format("Enabled. Posting usage statistics to db %s at %s", dbName, baseURL)); + } + } + + @Override + public void accept(Event event) { + String destination = String.format("%s/write?db=%s", baseURL, dbName); + String data = Optional.ofNullable(event.data).orElse(""); + String body = String.format("event,username=%s,type=%s data=\"%s\"", event.user, event.type, data); + try { + doPost(destination, body); + } catch (IOException e) { + log.error("Failed to submit user statistic event", e); + } + } + + private void doPost(String url, String body) throws IOException { + HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + try (DataOutputStream dos = new DataOutputStream(conn.getOutputStream())) { + dos.writeBytes(body); + dos.flush(); + } + //TODO Handle response +// int responseCode = conn.getResponseCode(); +// System.out.println(responseCode); + } +} diff --git a/src/main/java/eu/openanalytics/controllers/HeartbeatController.java b/src/main/java/eu/openanalytics/controllers/HeartbeatController.java index 85a3d7a7..7c074969 100644 --- a/src/main/java/eu/openanalytics/controllers/HeartbeatController.java +++ b/src/main/java/eu/openanalytics/controllers/HeartbeatController.java @@ -12,20 +12,20 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; -import eu.openanalytics.services.HeartbeatService; +import eu.openanalytics.services.UserService; @Controller public class HeartbeatController { @Inject - HeartbeatService heartbeatService; + UserService userService; @RequestMapping("/heartbeat/**") void heartbeat(Principal principal, HttpServletRequest request, HttpServletResponse response) { String userName = (principal == null) ? request.getSession().getId() : principal.getName(); Matcher matcher = Pattern.compile(".*/app/(.*)").matcher(request.getRequestURI()); String appName = matcher.matches() ? matcher.group(1) : null; - heartbeatService.heartbeatReceived(userName, appName); + userService.heartbeatReceived(userName, appName); try { response.setStatus(200); response.getWriter().write("Ok"); diff --git a/src/main/java/eu/openanalytics/services/DockerService.java b/src/main/java/eu/openanalytics/services/DockerService.java index 9d2941a2..e5e28a62 100644 --- a/src/main/java/eu/openanalytics/services/DockerService.java +++ b/src/main/java/eu/openanalytics/services/DockerService.java @@ -52,6 +52,7 @@ import eu.openanalytics.ShinyProxyException; import eu.openanalytics.services.AppService.ShinyApp; +import eu.openanalytics.services.EventService.EventType; @Service public class DockerService { @@ -70,6 +71,9 @@ public class DockerService { @Inject AppService appService; + @Inject + EventService eventService; + @Inject DockerClient dockerClient; @@ -177,6 +181,7 @@ public void run() { dockerClient.removeContainer(proxy.containerId); releasePort(proxy.port); log.info(String.format("Proxy released [user: %s] [app: %s] [port: %d]", proxy.userName, proxy.appName, proxy.port)); + eventService.post(EventType.AppStop.toString(), proxy.userName, proxy.appName); } catch (Exception e){ log.error("Failed to stop container " + proxy.name, e); } @@ -256,6 +261,8 @@ private Proxy startProxy(String userName, String appName) { activeProxies.add(proxy); log.info(String.format("Proxy activated [user: %s] [app: %s] [port: %d]", userName, appName, proxy.port)); + eventService.post(EventType.AppStart.toString(), userName, appName); + return proxy; } diff --git a/src/main/java/eu/openanalytics/services/EventService.java b/src/main/java/eu/openanalytics/services/EventService.java new file mode 100644 index 00000000..d3a2a9a0 --- /dev/null +++ b/src/main/java/eu/openanalytics/services/EventService.java @@ -0,0 +1,57 @@ +package eu.openanalytics.services; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +import org.springframework.stereotype.Service; + +@Service +public class EventService { + + private List> listeners = new CopyOnWriteArrayList<>(); + + public void post(String type, String user, String data) { + post(new Event(type, user, System.currentTimeMillis(), data)); + } + + public void post(Event event) { + for (Consumer listener: listeners) { + listener.accept(event); + } + } + + public void addListener(Consumer listener) { + listeners.add(listener); + } + + public void removeListener(Consumer listener) { + listeners.remove(listener); + } + + public static class Event { + + public String type; + public String user; + public long timestamp; + public String data; + + public Event() { + // Default constructor. + } + + public Event(String type, String user, long timestamp, String data) { + this.type = type; + this.user = user; + this.timestamp = timestamp; + this.data = data; + } + } + + public enum EventType { + Login, + Logout, + AppStart, + AppStop + } +} diff --git a/src/main/java/eu/openanalytics/services/HeartbeatService.java b/src/main/java/eu/openanalytics/services/HeartbeatService.java deleted file mode 100644 index 80db4df9..00000000 --- a/src/main/java/eu/openanalytics/services/HeartbeatService.java +++ /dev/null @@ -1,70 +0,0 @@ -package eu.openanalytics.services; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; - -import org.apache.log4j.Logger; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Service; - -import eu.openanalytics.services.DockerService.Proxy; - -@Service -public class HeartbeatService { - - @Inject - Environment environment; - - @Inject - DockerService dockerService; - - private Logger log = Logger.getLogger(HeartbeatService.class); - - private Map heartbeatTimestamps; - - @PostConstruct - public void init() { - heartbeatTimestamps = new ConcurrentHashMap<>(); - new Thread(new AppCleaner(), "HeartbeatThread").start(); - } - - public void heartbeatReceived(String user, String app) { - heartbeatTimestamps.put(user, System.currentTimeMillis()); - } - - public void clearHeartbeat(String user) { - heartbeatTimestamps.remove(user); - } - - private class AppCleaner implements Runnable { - @Override - public void run() { - long cleanupInterval = 2 * Long.parseLong(environment.getProperty("shiny.proxy.heartbeat-rate", "10000")); - long heartbeatTimeout = Long.parseLong(environment.getProperty("shiny.proxy.heartbeat-timeout", "60000")); - - while (true) { - try { - long currentTimestamp = System.currentTimeMillis(); - for (Proxy proxy: dockerService.listProxies()) { - Long lastHeartbeat = heartbeatTimestamps.get(proxy.userName); - if (lastHeartbeat == null) lastHeartbeat = proxy.startupTimestamp; - long proxySilence = currentTimestamp - lastHeartbeat; - if (proxySilence > heartbeatTimeout) { - log.info(String.format("Releasing inactive proxy [user: %s] [app: %s] [silence: %dms]", proxy.userName, proxy.appName, proxySilence)); - dockerService.releaseProxy(proxy.userName); - heartbeatTimestamps.remove(proxy.userName); - } - } - } catch (Throwable t) { - log.error("Error in HeartbeatThread", t); - } - try { - Thread.sleep(cleanupInterval); - } catch (InterruptedException e) {} - } - } - } -} diff --git a/src/main/java/eu/openanalytics/services/UserService.java b/src/main/java/eu/openanalytics/services/UserService.java index e5b61eed..a2c79d9c 100644 --- a/src/main/java/eu/openanalytics/services/UserService.java +++ b/src/main/java/eu/openanalytics/services/UserService.java @@ -1,5 +1,9 @@ package eu.openanalytics.services; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.PostConstruct; import javax.inject.Inject; import org.apache.log4j.Logger; @@ -11,19 +15,29 @@ import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; +import eu.openanalytics.services.DockerService.Proxy; +import eu.openanalytics.services.EventService.EventType; + @Service public class UserService implements ApplicationListener { private Logger log = Logger.getLogger(UserService.class); + private Map heartbeatTimestamps = new ConcurrentHashMap<>(); + @Inject Environment environment; @Inject DockerService dockerService; - + @Inject - HeartbeatService heartbeatService; + EventService eventService; + + @PostConstruct + public void init() { + new Thread(new AppCleaner(), "HeartbeatThread").start(); + } public String[] getAdminRoles() { String[] adminGroups = environment.getProperty("shiny.proxy.ldap.admin-groups", String[].class); @@ -41,13 +55,49 @@ public void onApplicationEvent(AbstractAuthenticationEvent event) { Exception e = ((AbstractAuthenticationFailureEvent) event).getException(); log.info(String.format("Authentication failure [user: %s] [error: %s]", source.getName(), e.getMessage())); } else if (event instanceof AuthenticationSuccessEvent) { - log.info(String.format("User logged in [user: %s]", source.getName())); + String userName = source.getName(); + log.info(String.format("User logged in [user: %s]", userName)); + eventService.post(EventType.Login.toString(), userName, null); } } - public void onLogout(String userName) { - heartbeatService.clearHeartbeat(userName); + public void logout(String userName) { + heartbeatTimestamps.remove(userName); dockerService.releaseProxy(userName); log.info(String.format("User logged out [user: %s]", userName)); + eventService.post(EventType.Logout.toString(), userName, null); + } + + public void heartbeatReceived(String user, String app) { + heartbeatTimestamps.put(user, System.currentTimeMillis()); + } + + private class AppCleaner implements Runnable { + @Override + public void run() { + long cleanupInterval = 2 * Long.parseLong(environment.getProperty("shiny.proxy.heartbeat-rate", "10000")); + long heartbeatTimeout = Long.parseLong(environment.getProperty("shiny.proxy.heartbeat-timeout", "60000")); + + while (true) { + try { + long currentTimestamp = System.currentTimeMillis(); + for (Proxy proxy: dockerService.listProxies()) { + Long lastHeartbeat = heartbeatTimestamps.get(proxy.userName); + if (lastHeartbeat == null) lastHeartbeat = proxy.startupTimestamp; + long proxySilence = currentTimestamp - lastHeartbeat; + if (proxySilence > heartbeatTimeout) { + log.info(String.format("Releasing inactive proxy [user: %s] [app: %s] [silence: %dms]", proxy.userName, proxy.appName, proxySilence)); + dockerService.releaseProxy(proxy.userName); + heartbeatTimestamps.remove(proxy.userName); + } + } + } catch (Throwable t) { + log.error("Error in HeartbeatThread", t); + } + try { + Thread.sleep(cleanupInterval); + } catch (InterruptedException e) {} + } + } } } From c4a2c940f88ab9cfa2f3890e58de648b9e2fa6c1 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Fri, 7 Oct 2016 16:50:01 +0200 Subject: [PATCH 06/23] Proper error handling when usage statistic cannot be posted --- .../components/UsageStatsCollector.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/openanalytics/components/UsageStatsCollector.java b/src/main/java/eu/openanalytics/components/UsageStatsCollector.java index 1d6380e2..299ab2da 100644 --- a/src/main/java/eu/openanalytics/components/UsageStatsCollector.java +++ b/src/main/java/eu/openanalytics/components/UsageStatsCollector.java @@ -1,5 +1,6 @@ package eu.openanalytics.components; +import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.HttpURLConnection; @@ -10,6 +11,7 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import org.springframework.core.env.Environment; import org.springframework.stereotype.Component; @@ -39,7 +41,7 @@ public void init() { log.info("Disabled. Usage statistics will not be posted."); } else { eventService.addListener(this); - log.info(String.format("Enabled. Posting usage statistics to db %s at %s", dbName, baseURL)); + log.info(String.format("Enabled. Posting usage statistics to %s at %s", dbName, baseURL)); } } @@ -51,7 +53,7 @@ public void accept(Event event) { try { doPost(destination, body); } catch (IOException e) { - log.error("Failed to submit user statistic event", e); + log.error("Failed to submit usage statistic event", e); } } @@ -63,8 +65,13 @@ private void doPost(String url, String body) throws IOException { dos.writeBytes(body); dos.flush(); } - //TODO Handle response -// int responseCode = conn.getResponseCode(); -// System.out.println(responseCode); + int responseCode = conn.getResponseCode(); + if (responseCode == 204) { + // All is well. + } else { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + IOUtils.copy(conn.getErrorStream(), bos); + throw new IOException(new String(bos.toByteArray())); + } } } From 9d0168f8fc1817f8d9f86a2cbe3f0a8aec030224 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Wed, 19 Oct 2016 11:57:57 +0200 Subject: [PATCH 07/23] Added more logging to detect container responsiveness issues --- .../eu/openanalytics/services/DockerService.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/openanalytics/services/DockerService.java b/src/main/java/eu/openanalytics/services/DockerService.java index 9d2941a2..0961c6c8 100644 --- a/src/main/java/eu/openanalytics/services/DockerService.java +++ b/src/main/java/eu/openanalytics/services/DockerService.java @@ -240,7 +240,7 @@ private Proxy startProxy(String userName, String appName) { throw new ShinyProxyException("Failed to start container: " + e.getMessage(), e); } - if (!testContainer(proxy, 20, 500)) { + if (!testContainer(proxy, 20, 500, 5000)) { releaseProxy(proxy, true); throw new ShinyProxyException("Container did not respond in time"); } @@ -268,13 +268,17 @@ private Proxy findProxy(String userName) { return null; } - private boolean testContainer(Proxy proxy, int maxTries, int waitMs) { + private boolean testContainer(Proxy proxy, int maxTries, int waitMs, int timeoutMs) { + String urlString = String.format("http://%s:%d", environment.getProperty("shiny.proxy.docker.host"), proxy.port); for (int currentTry = 1; currentTry <= maxTries; currentTry++) { try { - URL testURL = new URL("http://" + environment.getProperty("shiny.proxy.docker.host") + ":" + proxy.port); - int responseCode = ((HttpURLConnection) testURL.openConnection()).getResponseCode(); + URL testURL = new URL(urlString); + HttpURLConnection connection = ((HttpURLConnection) testURL.openConnection()); + connection.setConnectTimeout(timeoutMs); + int responseCode = connection.getResponseCode(); if (responseCode == 200) return true; } catch (Exception e) { + log.warn(String.format("Container unresponsive, trying again (%d/%d): %s", currentTry, maxTries, urlString)); try { Thread.sleep(waitMs); } catch (InterruptedException ignore) {} } } From 40351625c7d818b32bc83493842b8aa445891fc6 Mon Sep 17 00:00:00 2001 From: tverbeke Date: Tue, 25 Oct 2016 21:47:11 +0200 Subject: [PATCH 08/23] 0.6.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e550e12b..0fbcf80e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ eu.openanalytics shinyproxy - 0.5.0 + 0.6.0 jar shinyproxy From dcade71c842d39f39d259a05e207599f5db277e9 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Thu, 3 Nov 2016 11:23:03 +0100 Subject: [PATCH 09/23] Moved to undertow 1.4.4 --- pom.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pom.xml b/pom.xml index e550e12b..80088776 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,21 @@ org.springframework.boot spring-boot-starter-undertow + + io.undertow + undertow-core + 1.4.4.Final + + + io.undertow + undertow-servlet + 1.4.4.Final + + + io.undertow + undertow-websockets-jsr + 1.4.4.Final + From 8ff67c93efcc7f7763d8cff6ac2972174bb35a58 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Thu, 3 Nov 2016 12:01:06 +0100 Subject: [PATCH 10/23] Added option to specify a logo per app. Modified layout appropriately. --- .../eu/openanalytics/services/AppService.java | 8 ++++++ src/main/resources/templates/index.html | 25 ++++++++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/openanalytics/services/AppService.java b/src/main/java/eu/openanalytics/services/AppService.java index 5933beef..74de7382 100644 --- a/src/main/java/eu/openanalytics/services/AppService.java +++ b/src/main/java/eu/openanalytics/services/AppService.java @@ -80,6 +80,7 @@ public static class ShinyApp { private String name; private String displayName; private String description; + private String logoUrl; private String[] dockerCmd; private String dockerImage; private String[] dockerDns; @@ -106,6 +107,13 @@ public void setDescription(String description) { this.description = description; } + public String getLogoUrl() { + return logoUrl; + } + public void setLogoUrl(String logoUrl) { + this.logoUrl = logoUrl; + } + public String[] getDockerCmd() { return dockerCmd; } diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 759ff374..c8313cd1 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -16,14 +16,21 @@
-
-
    -
  • - -
    - -
  • -
-
+
+
+
+
+
+ +
+
+ +
+ +
+
+
+
+
\ No newline at end of file From 5cd3cb1e16b1449fb6efcfc7910b4a4e752151d9 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Thu, 3 Nov 2016 13:06:15 +0100 Subject: [PATCH 11/23] Added support for multiple apps per user --- .../openanalytics/services/DockerService.java | 34 +++++---- .../openanalytics/services/UserService.java | 70 +++++++++++++++++-- 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/src/main/java/eu/openanalytics/services/DockerService.java b/src/main/java/eu/openanalytics/services/DockerService.java index b5909190..b777cdda 100644 --- a/src/main/java/eu/openanalytics/services/DockerService.java +++ b/src/main/java/eu/openanalytics/services/DockerService.java @@ -152,23 +152,31 @@ public void shutdown() { } public String getMapping(String userName, String appName) { - Proxy proxy = findProxy(userName); + Proxy proxy = findProxy(userName, appName); if (proxy == null) { // The user has no proxy yet. proxy = startProxy(userName, appName); - } else if (appName.equals(proxy.appName)) { - // The user's proxy is good to go. - } else { - // The user's proxy is running the wrong app. - releaseProxy(proxy, true); - proxy = startProxy(userName, appName); } return (proxy == null) ? null : proxy.name; } - public void releaseProxy(String userName) { - Proxy proxy = findProxy(userName); - if (proxy != null) releaseProxy(proxy, true); + public void releaseProxies(String userName) { + List proxiesToRelease = new ArrayList<>(); + synchronized (activeProxies) { + for (Proxy proxy: activeProxies) { + if (userName.equals(proxy.userName)) proxiesToRelease.add(proxy); + } + } + for (Proxy proxy: proxiesToRelease) { + releaseProxy(proxy, true); + } + } + + public void releaseProxy(String userName, String appName) { + Proxy proxy = findProxy(userName, appName); + if (proxy != null) { + releaseProxy(proxy, true); + } } private void releaseProxy(Proxy proxy, boolean async) { @@ -205,7 +213,7 @@ private Proxy startProxy(String userName, String appName) { throw new ShinyProxyException("Cannot start container: unknown application: " + appName); } - Proxy proxy = findProxy(userName); + Proxy proxy = findProxy(userName, appName); if (proxy != null) { throw new ShinyProxyException("Cannot start container: user " + userName + " already has a running proxy"); } @@ -266,10 +274,10 @@ private Proxy startProxy(String userName, String appName) { return proxy; } - private Proxy findProxy(String userName) { + private Proxy findProxy(String userName, String appName) { synchronized (activeProxies) { for (Proxy proxy: activeProxies) { - if (userName.equals(proxy.userName)) return proxy; + if (userName.equals(proxy.userName) && appName.equals(proxy.appName)) return proxy; } } return null; diff --git a/src/main/java/eu/openanalytics/services/UserService.java b/src/main/java/eu/openanalytics/services/UserService.java index a2c79d9c..4524394d 100644 --- a/src/main/java/eu/openanalytics/services/UserService.java +++ b/src/main/java/eu/openanalytics/services/UserService.java @@ -1,5 +1,7 @@ package eu.openanalytics.services; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -23,7 +25,7 @@ public class UserService implements ApplicationListener heartbeatTimestamps = new ConcurrentHashMap<>(); + private Map heartbeatTimestamps = new ConcurrentHashMap<>(); @Inject Environment environment; @@ -62,14 +64,20 @@ public void onApplicationEvent(AbstractAuthenticationEvent event) { } public void logout(String userName) { - heartbeatTimestamps.remove(userName); - dockerService.releaseProxy(userName); + List keysToRemove = new ArrayList<>(); + for (HeartbeatKey key: heartbeatTimestamps.keySet()) { + if (key.userName.equals(userName)) keysToRemove.add(key); + } + for (HeartbeatKey key: keysToRemove) { + heartbeatTimestamps.remove(key); + } + dockerService.releaseProxies(userName); log.info(String.format("User logged out [user: %s]", userName)); eventService.post(EventType.Logout.toString(), userName, null); } public void heartbeatReceived(String user, String app) { - heartbeatTimestamps.put(user, System.currentTimeMillis()); + heartbeatTimestamps.put(getKey(user, app), System.currentTimeMillis()); } private class AppCleaner implements Runnable { @@ -82,13 +90,14 @@ public void run() { try { long currentTimestamp = System.currentTimeMillis(); for (Proxy proxy: dockerService.listProxies()) { - Long lastHeartbeat = heartbeatTimestamps.get(proxy.userName); + HeartbeatKey key = getKey(proxy.userName, proxy.appName); + Long lastHeartbeat = heartbeatTimestamps.get(key); if (lastHeartbeat == null) lastHeartbeat = proxy.startupTimestamp; long proxySilence = currentTimestamp - lastHeartbeat; if (proxySilence > heartbeatTimeout) { log.info(String.format("Releasing inactive proxy [user: %s] [app: %s] [silence: %dms]", proxy.userName, proxy.appName, proxySilence)); - dockerService.releaseProxy(proxy.userName); - heartbeatTimestamps.remove(proxy.userName); + dockerService.releaseProxy(proxy.userName, proxy.appName); + heartbeatTimestamps.remove(key); } } } catch (Throwable t) { @@ -100,4 +109,51 @@ public void run() { } } } + + private HeartbeatKey getKey(String userName, String appName) { + return new HeartbeatKey(userName, appName); + } + + + private static class HeartbeatKey { + + private String userName; + private String appName; + + public HeartbeatKey(String userName, String appName) { + this.userName = userName; + this.appName = appName; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((appName == null) ? 0 : appName.hashCode()); + result = prime * result + ((userName == null) ? 0 : userName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + HeartbeatKey other = (HeartbeatKey) obj; + if (appName == null) { + if (other.appName != null) + return false; + } else if (!appName.equals(other.appName)) + return false; + if (userName == null) { + if (other.userName != null) + return false; + } else if (!userName.equals(other.userName)) + return false; + return true; + } + } } From a73b0593e2524d4ebe244b7de230fe0be1671f00 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Mon, 7 Nov 2016 10:17:28 +0100 Subject: [PATCH 12/23] Switch between old bulletpoint layout and new logo layout --- .../openanalytics/controllers/IndexController.java | 10 +++++++++- src/main/resources/templates/index.html | 12 +++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/openanalytics/controllers/IndexController.java b/src/main/java/eu/openanalytics/controllers/IndexController.java index db207622..39225a7d 100644 --- a/src/main/java/eu/openanalytics/controllers/IndexController.java +++ b/src/main/java/eu/openanalytics/controllers/IndexController.java @@ -16,6 +16,7 @@ package eu.openanalytics.controllers; import java.security.Principal; +import java.util.List; import javax.inject.Inject; @@ -26,6 +27,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import eu.openanalytics.services.AppService; +import eu.openanalytics.services.AppService.ShinyApp; import eu.openanalytics.services.UserService; /** @@ -45,9 +47,15 @@ public class IndexController { @RequestMapping("/") String index(ModelMap map, Principal principal) { + List apps = appService.getApps((Authentication) principal); + boolean displayAppLogos = false; + for (ShinyApp app: apps) { + if (app.getLogoUrl() != null) displayAppLogos = true; + } map.put("title", environment.getProperty("shiny.proxy.title")); map.put("logo", environment.getProperty("shiny.proxy.logo-url")); - map.put("apps", appService.getApps((Authentication) principal).toArray()); + map.put("apps", apps.toArray()); + map.put("displayAppLogos", displayAppLogos); map.put("adminGroups", userService.getAdminRoles()); return "index"; } diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index c8313cd1..30ca6b16 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -16,7 +16,17 @@
-
+
+
    +
  • + +
    + +
  • +
+
+ +
From 9597bf5b45991c3f062614413230fff273c6b62a Mon Sep 17 00:00:00 2001 From: tverbeke Date: Tue, 8 Nov 2016 23:01:37 +0100 Subject: [PATCH 13/23] prepare for testing --- README.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5226ce20..951aaf3d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The build will result in a single `.jar` file that is made available in the `tar ## Running the application ``` -java -jar shinyproxy-0.4.0.jar +java -jar shinyproxy-0.6.0.jar ``` Navigate to http://localhost:8080 to access the application. If the default configuration is used, authentication will be done against the LDAP server at *ldap.forumsys.com*; to log in one can use the user name "tesla" and password "password". diff --git a/pom.xml b/pom.xml index 676b3f10..17e68778 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ eu.openanalytics shinyproxy - 0.6.0 + 0.7.0-SNAPSHOT jar shinyproxy From e1a39c2b841184214138b2e92a0487c5fea0d361 Mon Sep 17 00:00:00 2001 From: tverbeke Date: Thu, 10 Nov 2016 23:24:44 +0100 Subject: [PATCH 14/23] 0.7.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 17e68778..8eb3b3b7 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ eu.openanalytics shinyproxy - 0.7.0-SNAPSHOT + 0.7.0 jar shinyproxy From e29974267c9326dd465df775560545a983a78fa5 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Thu, 17 Nov 2016 12:43:11 +0100 Subject: [PATCH 15/23] Added support for forwarding URL query strings to shiny --- .../java/eu/openanalytics/controllers/AppController.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/openanalytics/controllers/AppController.java b/src/main/java/eu/openanalytics/controllers/AppController.java index eda4dbbb..577cfc76 100644 --- a/src/main/java/eu/openanalytics/controllers/AppController.java +++ b/src/main/java/eu/openanalytics/controllers/AppController.java @@ -53,9 +53,13 @@ String app(ModelMap map, Principal principal, HttpServletRequest request) { String appName = matcher.matches() ? matcher.group(1) : null; String mapping = dockerService.getMapping(userName, appName); + String queryString = request.getQueryString(); + if (queryString == null) queryString = ""; + else queryString = "?" + queryString; + map.put("title", environment.getProperty("shiny.proxy.title")); map.put("logo", environment.getProperty("shiny.proxy.logo-url")); - map.put("container", "/" + mapping + environment.getProperty("shiny.proxy.landing-page")); + map.put("container", "/" + mapping + environment.getProperty("shiny.proxy.landing-page") + queryString); map.put("heartbeatRate", environment.getProperty("shiny.proxy.heartbeat-rate", "10000")); map.put("adminGroups", userService.getAdminRoles()); From e30a196477cf8db78082697c04de7b8d97ea026a Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Thu, 17 Nov 2016 17:15:54 +0100 Subject: [PATCH 16/23] Adding a 'simple' authentication mechanism --- .../eu/openanalytics/WebSecurityConfig.java | 117 +----------- .../AuthenticationConfigurationFactory.java | 168 ++++++++++++++++++ .../components/LogoutHandler.java | 9 +- .../controllers/IndexController.java | 6 +- .../eu/openanalytics/services/AppService.java | 41 +---- .../openanalytics/services/UserService.java | 31 +++- src/main/resources/application-demo.yml | 14 +- 7 files changed, 225 insertions(+), 161 deletions(-) create mode 100644 src/main/java/eu/openanalytics/auth/AuthenticationConfigurationFactory.java diff --git a/src/main/java/eu/openanalytics/WebSecurityConfig.java b/src/main/java/eu/openanalytics/WebSecurityConfig.java index 855348e2..bbef17f5 100644 --- a/src/main/java/eu/openanalytics/WebSecurityConfig.java +++ b/src/main/java/eu/openanalytics/WebSecurityConfig.java @@ -15,39 +15,26 @@ */ package eu.openanalytics; -import java.util.HashSet; -import java.util.Set; +import java.util.Arrays; import javax.inject.Inject; -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; -import org.springframework.ldap.core.ContextSource; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter; 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.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.ldap.DefaultSpringSecurityContextSource; -import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import eu.openanalytics.auth.AuthenticationConfigurationFactory; import eu.openanalytics.components.LogoutHandler; import eu.openanalytics.services.AppService; import eu.openanalytics.services.AppService.ShinyApp; import eu.openanalytics.services.UserService; -/** - * @author Torkild U. Resheim, Itema AS - */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @@ -82,12 +69,13 @@ protected void configure(HttpSecurity http) throws Exception { .frameOptions() .sameOrigin(); - if (hasAuth(environment)) { + if (AuthenticationConfigurationFactory.hasAuth(environment)) { // Limit access to the app pages http.authorizeRequests().antMatchers("/login").permitAll(); for (ShinyApp app: appService.getApps()) { - String[] appRoles = appService.getAppRoles(app.getName()); - if (appRoles != null && appRoles.length > 0) http.authorizeRequests().antMatchers("/app/" + app.getName()).hasAnyRole(appRoles); + if (app.getGroups() == null || app.getGroups().length == 0) continue; + String[] appRoles = Arrays.stream(app.getGroups()).map(s -> s.toUpperCase()).toArray(i -> new String[i]); + http.authorizeRequests().antMatchers("/app/" + app.getName()).hasAnyRole(appRoles); } // Limit access to the admin pages @@ -107,11 +95,6 @@ protected void configure(HttpSecurity http) throws Exception { } } - private static boolean hasAuth(Environment env) { - String auth = env.getProperty("shiny.proxy.authentication", "").toLowerCase(); - return (!auth.isEmpty() && !auth.equals("none")); - } - @Configuration protected static class AuthenticationConfiguration extends GlobalAuthenticationConfigurerAdapter { @@ -120,93 +103,7 @@ protected static class AuthenticationConfiguration extends GlobalAuthenticationC @Override public void init(AuthenticationManagerBuilder auth) throws Exception { - if (!hasAuth(environment)) return; - - String[] userDnPatterns = { environment.getProperty("shiny.proxy.ldap.user-dn-pattern") }; - if (userDnPatterns[0] == null || userDnPatterns[0].isEmpty()) userDnPatterns = new String[0]; - - String managerDn = environment.getProperty("shiny.proxy.ldap.manager-dn"); - if (managerDn != null && managerDn.isEmpty()) managerDn = null; - - // Manually instantiate contextSource so it can be passed into authoritiesPopulator below. - String ldapUrl = environment.getProperty("shiny.proxy.ldap.url"); - DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(ldapUrl); - if (managerDn != null) { - contextSource.setUserDn(managerDn); - contextSource.setPassword(environment.getProperty("shiny.proxy.ldap.manager-password")); - } - contextSource.afterPropertiesSet(); - - // Manually instantiate authoritiesPopulator because it uses a customized class. - CNLdapAuthoritiesPopulator authoritiesPopulator = new CNLdapAuthoritiesPopulator( - contextSource, - environment.getProperty("shiny.proxy.ldap.group-search-base", "")); - authoritiesPopulator.setGroupRoleAttribute("cn"); - authoritiesPopulator.setGroupSearchFilter(environment.getProperty("shiny.proxy.ldap.group-search-filter", "(uniqueMember={0})")); - - auth - .ldapAuthentication() - .userDnPatterns(userDnPatterns) - .userSearchBase(environment.getProperty("shiny.proxy.ldap.user-search-base", "")) - .userSearchFilter(environment.getProperty("shiny.proxy.ldap.user-search-filter")) - .ldapAuthoritiesPopulator(authoritiesPopulator) - .contextSource(contextSource); - } - } - - private static class CNLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopulator { - - private static final Log logger = LogFactory.getLog(DefaultLdapAuthoritiesPopulator.class); - - public CNLdapAuthoritiesPopulator(ContextSource contextSource, String groupSearchBase) { - super(contextSource, groupSearchBase); - } - - @Override - public Set getGroupMembershipRoles(String userDn, String username) { - if (getGroupSearchBase() == null) { - return new HashSet(); - } - - Set authorities = new HashSet(); - - if (logger.isDebugEnabled()) { - logger.debug("Searching for roles for user '" + username + "', DN = " + "'" - + userDn + "', with filter " + getGroupSearchFilter() - + " in search base '" + getGroupSearchBase() + "'"); - } - - // Here's the modification: added {2}, which refers to the user cn if available. - Set userRoles = getLdapTemplate().searchForSingleAttributeValues( - getGroupSearchBase(), getGroupSearchFilter(), - new String[] { userDn, username, getCn(userDn) }, getGroupRoleAttribute()); - - if (logger.isDebugEnabled()) { - logger.debug("Roles from search: " + userRoles); - } - - for (String role : userRoles) { - - if (isConvertToUpperCase()) { - role = role.toUpperCase(); - } - - authorities.add(new SimpleGrantedAuthority(getRolePrefix() + role)); - } - - return authorities; - } - - private String getCn(String dn) { - try { - LdapName ln = new LdapName(dn); - for (Rdn rdn : ln.getRdns()) { - if (rdn.getType().equalsIgnoreCase("CN")) { - return rdn.getValue().toString(); - } - } - } catch (InvalidNameException e) {} - return ""; + AuthenticationConfigurationFactory.configure(auth, environment); } } } \ No newline at end of file diff --git a/src/main/java/eu/openanalytics/auth/AuthenticationConfigurationFactory.java b/src/main/java/eu/openanalytics/auth/AuthenticationConfigurationFactory.java new file mode 100644 index 00000000..c3f93e44 --- /dev/null +++ b/src/main/java/eu/openanalytics/auth/AuthenticationConfigurationFactory.java @@ -0,0 +1,168 @@ +package eu.openanalytics.auth; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.env.Environment; +import org.springframework.ldap.core.ContextSource; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.ldap.DefaultSpringSecurityContextSource; +import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator; + +public class AuthenticationConfigurationFactory { + + private enum AuthType { + none, + simple, + ldap; + + public static AuthType get(String name) { + for (AuthType value: values()) { + if (value.toString().equalsIgnoreCase(name)) return value; + } + return none; + } + } + + public static boolean hasAuth(Environment environment) { + AuthType authType = AuthType.get(environment.getProperty("shiny.proxy.authentication", "")); + return authType != AuthType.none; + } + + public static void configure(AuthenticationManagerBuilder auth, Environment environment) throws Exception { + AuthType authType = AuthType.get(environment.getProperty("shiny.proxy.authentication", "")); + if (authType == AuthType.simple) configureSimple(auth, environment); + if (authType == AuthType.ldap) configureLDAP(auth, environment); + } + + private static void configureSimple(AuthenticationManagerBuilder auth, Environment environment) throws Exception { + InMemoryUserDetailsManagerConfigurer userDetails = auth.inMemoryAuthentication(); + int i=0; + SimpleUser user = loadUser(i++, environment); + while (user != null) { + userDetails.withUser(user.name).password(user.password).roles(user.roles); + user = loadUser(i++, environment); + } + } + + private static SimpleUser loadUser(int index, Environment environment) { + String userName = environment.getProperty(String.format("shiny.proxy.users[%d].name", index)); + if (userName == null) return null; + String password = environment.getProperty(String.format("shiny.proxy.users[%d].password", index)); + String[] roles = environment.getProperty(String.format("shiny.proxy.users[%d].groups", index), String[].class); + roles = Arrays.stream(roles).map(s -> s.toUpperCase()).toArray(i -> new String[i]); + return new SimpleUser(userName, password, roles); + } + + private static class SimpleUser { + + public String name; + public String password; + public String[] roles; + + public SimpleUser(String name, String password, String[] roles) { + this.name = name; + this.password = password; + this.roles = roles; + } + + } + + private static void configureLDAP(AuthenticationManagerBuilder auth, Environment environment) throws Exception { + String[] userDnPatterns = { environment.getProperty("shiny.proxy.ldap.user-dn-pattern") }; + if (userDnPatterns[0] == null || userDnPatterns[0].isEmpty()) userDnPatterns = new String[0]; + + String managerDn = environment.getProperty("shiny.proxy.ldap.manager-dn"); + if (managerDn != null && managerDn.isEmpty()) managerDn = null; + + // Manually instantiate contextSource so it can be passed into authoritiesPopulator below. + String ldapUrl = environment.getProperty("shiny.proxy.ldap.url"); + DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(ldapUrl); + if (managerDn != null) { + contextSource.setUserDn(managerDn); + contextSource.setPassword(environment.getProperty("shiny.proxy.ldap.manager-password")); + } + contextSource.afterPropertiesSet(); + + // Manually instantiate authoritiesPopulator because it uses a customized class. + CNLdapAuthoritiesPopulator authoritiesPopulator = new CNLdapAuthoritiesPopulator( + contextSource, + environment.getProperty("shiny.proxy.ldap.group-search-base", "")); + authoritiesPopulator.setGroupRoleAttribute("cn"); + authoritiesPopulator.setGroupSearchFilter(environment.getProperty("shiny.proxy.ldap.group-search-filter", "(uniqueMember={0})")); + + auth + .ldapAuthentication() + .userDnPatterns(userDnPatterns) + .userSearchBase(environment.getProperty("shiny.proxy.ldap.user-search-base", "")) + .userSearchFilter(environment.getProperty("shiny.proxy.ldap.user-search-filter")) + .ldapAuthoritiesPopulator(authoritiesPopulator) + .contextSource(contextSource); + } + + private static class CNLdapAuthoritiesPopulator extends DefaultLdapAuthoritiesPopulator { + + private static final Log logger = LogFactory.getLog(DefaultLdapAuthoritiesPopulator.class); + + public CNLdapAuthoritiesPopulator(ContextSource contextSource, String groupSearchBase) { + super(contextSource, groupSearchBase); + } + + @Override + public Set getGroupMembershipRoles(String userDn, String username) { + if (getGroupSearchBase() == null) { + return new HashSet(); + } + + Set authorities = new HashSet(); + + if (logger.isDebugEnabled()) { + logger.debug("Searching for roles for user '" + username + "', DN = " + "'" + + userDn + "', with filter " + getGroupSearchFilter() + + " in search base '" + getGroupSearchBase() + "'"); + } + + // Here's the modification: added {2}, which refers to the user cn if available. + Set userRoles = getLdapTemplate().searchForSingleAttributeValues( + getGroupSearchBase(), getGroupSearchFilter(), + new String[] { userDn, username, getCn(userDn) }, getGroupRoleAttribute()); + + if (logger.isDebugEnabled()) { + logger.debug("Roles from search: " + userRoles); + } + + for (String role : userRoles) { + + if (isConvertToUpperCase()) { + role = role.toUpperCase(); + } + + authorities.add(new SimpleGrantedAuthority(getRolePrefix() + role)); + } + + return authorities; + } + + private String getCn(String dn) { + try { + LdapName ln = new LdapName(dn); + for (Rdn rdn : ln.getRdns()) { + if (rdn.getType().equalsIgnoreCase("CN")) { + return rdn.getValue().toString(); + } + } + } catch (InvalidNameException e) {} + return ""; + } + } +} diff --git a/src/main/java/eu/openanalytics/components/LogoutHandler.java b/src/main/java/eu/openanalytics/components/LogoutHandler.java index 9c5f5536..ec9cb9b5 100644 --- a/src/main/java/eu/openanalytics/components/LogoutHandler.java +++ b/src/main/java/eu/openanalytics/components/LogoutHandler.java @@ -23,15 +23,12 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; -import org.springframework.security.ldap.userdetails.LdapUserDetails; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import eu.openanalytics.services.UserService; -/** - * @author Torkild U. Resheim, Itema AS - */ @Component public class LogoutHandler implements LogoutSuccessHandler { @@ -40,8 +37,8 @@ public class LogoutHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { - if (authentication != null && authentication.getPrincipal() instanceof LdapUserDetails) { - String userName = ((LdapUserDetails) authentication.getPrincipal()).getUsername(); + if (authentication != null && authentication.getPrincipal() instanceof UserDetails) { + String userName = ((UserDetails) authentication.getPrincipal()).getUsername(); userService.logout(userName); } response.sendRedirect("/"); diff --git a/src/main/java/eu/openanalytics/controllers/IndexController.java b/src/main/java/eu/openanalytics/controllers/IndexController.java index 39225a7d..b81200f5 100644 --- a/src/main/java/eu/openanalytics/controllers/IndexController.java +++ b/src/main/java/eu/openanalytics/controllers/IndexController.java @@ -26,7 +26,6 @@ import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; -import eu.openanalytics.services.AppService; import eu.openanalytics.services.AppService.ShinyApp; import eu.openanalytics.services.UserService; @@ -36,9 +35,6 @@ @Controller public class IndexController { - @Inject - AppService appService; - @Inject UserService userService; @@ -47,7 +43,7 @@ public class IndexController { @RequestMapping("/") String index(ModelMap map, Principal principal) { - List apps = appService.getApps((Authentication) principal); + List apps = userService.getAccessibleApps((Authentication) principal); boolean displayAppLogos = false; for (ShinyApp app: apps) { if (app.getLogoUrl() != null) displayAppLogos = true; diff --git a/src/main/java/eu/openanalytics/services/AppService.java b/src/main/java/eu/openanalytics/services/AppService.java index 74de7382..8188aaa5 100644 --- a/src/main/java/eu/openanalytics/services/AppService.java +++ b/src/main/java/eu/openanalytics/services/AppService.java @@ -16,15 +16,12 @@ package eu.openanalytics.services; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import javax.inject.Inject; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.env.Environment; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Service; @ConfigurationProperties(prefix = "shiny") @@ -47,34 +44,6 @@ public List getApps() { return apps; } - public List getApps(Authentication principalAuth) { - List accessibleApps = new ArrayList<>(); - for (ShinyApp app: apps) { - if (canAccess(principalAuth, app.getName())) accessibleApps.add(app); - } - return accessibleApps; - } - - public boolean canAccess(Authentication principalAuth, String appName) { - String[] appRoles = getAppRoles(appName); - if (appRoles.length == 0 || principalAuth == null) return true; - Arrays.sort(appRoles); - for (GrantedAuthority auth: principalAuth.getAuthorities()) { - String role = auth.getAuthority().toUpperCase(); - if (role.startsWith("ROLE_")) role = role.substring(5); - if (Arrays.binarySearch(appRoles, role) >= 0) return true; - } - return false; - } - - public String[] getAppRoles(String appName) { - ShinyApp app = getApp(appName); - if (app == null || app.getLdapGroups() == null) return new String[0]; - String[] roles = new String[app.getLdapGroups().length]; - for (int i = 0; i < roles.length; i++) roles[i] = app.getLdapGroups()[i].toUpperCase(); - return roles; - } - public static class ShinyApp { private String name; @@ -84,7 +53,7 @@ public static class ShinyApp { private String[] dockerCmd; private String dockerImage; private String[] dockerDns; - private String[] ldapGroups; + private String[] groups; public String getName() { return name; @@ -135,11 +104,11 @@ public void setDockerDns(String[] dockerDns) { this.dockerDns = dockerDns; } - public String[] getLdapGroups() { - return ldapGroups; + public String[] getGroups() { + return groups; } - public void setLdapGroups(String[] ldapGroups) { - this.ldapGroups = ldapGroups; + public void setGroups(String[] groups) { + this.groups = groups; } } } \ No newline at end of file diff --git a/src/main/java/eu/openanalytics/services/UserService.java b/src/main/java/eu/openanalytics/services/UserService.java index 4524394d..ff7f3db5 100644 --- a/src/main/java/eu/openanalytics/services/UserService.java +++ b/src/main/java/eu/openanalytics/services/UserService.java @@ -15,8 +15,10 @@ import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent; import org.springframework.security.authentication.event.AuthenticationSuccessEvent; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Service; +import eu.openanalytics.services.AppService.ShinyApp; import eu.openanalytics.services.DockerService.Proxy; import eu.openanalytics.services.EventService.EventType; @@ -36,19 +38,46 @@ public class UserService implements ApplicationListener getAccessibleApps(Authentication principalAuth) { + List accessibleApps = new ArrayList<>(); + for (ShinyApp app: appService.getApps()) { + if (canAccess(principalAuth, app.getName())) accessibleApps.add(app); + } + return accessibleApps; + } + + private boolean canAccess(Authentication principalAuth, String appName) { + ShinyApp app = appService.getApp(appName); + if (app == null) return false; + if (app.getGroups() == null || app.getGroups().length == 0) return true; + if (principalAuth == null) return true; + + for (GrantedAuthority auth: principalAuth.getAuthorities()) { + String role = auth.getAuthority().toUpperCase(); + if (role.startsWith("ROLE_")) role = role.substring(5); + for (String group: app.getGroups()) { + if (group.equalsIgnoreCase(role)) return true; + } + } + return false; + } @Override public void onApplicationEvent(AbstractAuthenticationEvent event) { diff --git a/src/main/resources/application-demo.yml b/src/main/resources/application-demo.yml index 2cd02fa4..50c20dae 100644 --- a/src/main/resources/application-demo.yml +++ b/src/main/resources/application-demo.yml @@ -7,6 +7,15 @@ shiny: heartbeat-timeout: 60000 port: 8080 authentication: ldap + admin-groups: scientists + # Simple auth configuration + users: + - name: jack + password: password + groups: scientists + - name: jeff + password: password + groups: mathematicians # LDAP configuration ldap: url: ldap://ldap.forumsys.com:389/dc=example,dc=com @@ -15,7 +24,6 @@ shiny: group-search-filter: (uniqueMember={0}) manager-dn: cn=read-only-admin,dc=example,dc=com manager-password: password - admin-groups: scientists # Docker configuration docker: cert-path: /home/none @@ -28,11 +36,11 @@ shiny: description: Application which demonstrates the basics of a Shiny app docker-cmd: ["R", "-e shinyproxy::run_01_hello()"] docker-image: openanalytics/shinyproxy-demo - ldap-groups: scientists, mathematicians + groups: scientists, mathematicians - name: 06_tabsets docker-cmd: ["R", "-e shinyproxy::run_06_tabsets()"] docker-image: openanalytics/shinyproxy-demo - ldap-groups: scientists + groups: scientists logging: file: From e9175a76fad7f4aa635f145338a51e8ec1144c72 Mon Sep 17 00:00:00 2001 From: tverbeke Date: Thu, 17 Nov 2016 22:23:45 +0100 Subject: [PATCH 17/23] 0.7.5 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8eb3b3b7..7810aedd 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ eu.openanalytics shinyproxy - 0.7.0 + 0.7.5 jar shinyproxy From 65ba0b2a2beabcaa129f7644e8e8c7f1c487b7b4 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Wed, 7 Dec 2016 15:48:02 +0100 Subject: [PATCH 18/23] Disabled the X-Frame-Options header to allow iframe embedding --- src/main/java/eu/openanalytics/WebSecurityConfig.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/eu/openanalytics/WebSecurityConfig.java b/src/main/java/eu/openanalytics/WebSecurityConfig.java index bbef17f5..d3daf85f 100644 --- a/src/main/java/eu/openanalytics/WebSecurityConfig.java +++ b/src/main/java/eu/openanalytics/WebSecurityConfig.java @@ -62,12 +62,9 @@ public void configure(WebSecurity web) throws Exception { protected void configure(HttpSecurity http) throws Exception { http // must disable or handle in proxy - .csrf() - .disable() + .csrf().disable() // disable X-Frame-Options - .headers() - .frameOptions() - .sameOrigin(); + .headers().frameOptions().disable(); if (AuthenticationConfigurationFactory.hasAuth(environment)) { // Limit access to the app pages From 3e2fb90079b8db45e514de83e95d29ead9a3bbd9 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Wed, 7 Dec 2016 16:55:15 +0100 Subject: [PATCH 19/23] Added option to specify host memory limit --- .../eu/openanalytics/services/AppService.java | 8 +++++ .../openanalytics/services/DockerService.java | 31 ++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/openanalytics/services/AppService.java b/src/main/java/eu/openanalytics/services/AppService.java index 8188aaa5..893b2d4d 100644 --- a/src/main/java/eu/openanalytics/services/AppService.java +++ b/src/main/java/eu/openanalytics/services/AppService.java @@ -53,6 +53,7 @@ public static class ShinyApp { private String[] dockerCmd; private String dockerImage; private String[] dockerDns; + private String dockerMemory; private String[] groups; public String getName() { @@ -104,6 +105,13 @@ public void setDockerDns(String[] dockerDns) { this.dockerDns = dockerDns; } + public String getDockerMemory() { + return dockerMemory; + } + public void setDockerMemory(String dockerMemory) { + this.dockerMemory = dockerMemory; + } + public String[] getGroups() { return groups; } diff --git a/src/main/java/eu/openanalytics/services/DockerService.java b/src/main/java/eu/openanalytics/services/DockerService.java index b777cdda..8c6000c0 100644 --- a/src/main/java/eu/openanalytics/services/DockerService.java +++ b/src/main/java/eu/openanalytics/services/DockerService.java @@ -29,6 +29,8 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.PreDestroy; import javax.inject.Inject; @@ -48,6 +50,7 @@ import com.spotify.docker.client.messages.ContainerCreation; import com.spotify.docker.client.messages.ContainerInfo; import com.spotify.docker.client.messages.HostConfig; +import com.spotify.docker.client.messages.HostConfig.Builder; import com.spotify.docker.client.messages.PortBinding; import eu.openanalytics.ShinyProxyException; @@ -228,7 +231,12 @@ private Proxy startProxy(String userName, String appName) { List hostPorts = new ArrayList(); hostPorts.add(PortBinding.of("0.0.0.0", proxy.port)); portBindings.put("3838", hostPorts); - final HostConfig hostConfig = HostConfig.builder() + + long memoryLimit = convertMemory(app.getDockerMemory()); + + Builder hostConfigBuilder = HostConfig.builder(); + if (memoryLimit > 0) hostConfigBuilder.memory(memoryLimit); + final HostConfig hostConfig = hostConfigBuilder .portBindings(portBindings) .dns(app.getDockerDns()) .build(); @@ -312,6 +320,27 @@ private void releasePort(int port) { occupiedPorts.remove(port); } + private long convertMemory(String memory) { + if (memory == null || memory.isEmpty()) return -1; + Matcher matcher = Pattern.compile("(\\d+)([bkmg]?)").matcher(memory.toLowerCase()); + if (!matcher.matches()) throw new IllegalArgumentException("Invalid memory argument: " + memory); + long mem = Long.parseLong(matcher.group(1)); + String unit = matcher.group(2); + switch (unit) { + case "k": + mem *= 1024; + break; + case "m": + mem *= 1024*1024; + break; + case "g": + mem *= 1024*1024*1024; + break; + default: + } + return mem; + } + public void addMappingListener(MappingListener listener) { mappingListeners.add(listener); } From 4b37173a58d0f155271670f794546547c0c87bc0 Mon Sep 17 00:00:00 2001 From: tverbeke Date: Wed, 7 Dec 2016 19:08:04 +0100 Subject: [PATCH 20/23] 0.7.8 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7810aedd..7a3de134 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ eu.openanalytics shinyproxy - 0.7.5 + 0.7.8 jar shinyproxy From b87dfb13ad17810a537405b940626a2343244cbb Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Thu, 15 Dec 2016 17:12:30 +0100 Subject: [PATCH 21/23] Added support for new app setting: docker-env-file --- .../eu/openanalytics/services/AppService.java | 8 +++++++ .../openanalytics/services/DockerService.java | 22 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/openanalytics/services/AppService.java b/src/main/java/eu/openanalytics/services/AppService.java index 893b2d4d..774ae3e1 100644 --- a/src/main/java/eu/openanalytics/services/AppService.java +++ b/src/main/java/eu/openanalytics/services/AppService.java @@ -54,6 +54,7 @@ public static class ShinyApp { private String dockerImage; private String[] dockerDns; private String dockerMemory; + private String dockerEnvFile; private String[] groups; public String getName() { @@ -112,6 +113,13 @@ public void setDockerMemory(String dockerMemory) { this.dockerMemory = dockerMemory; } + public String getDockerEnvFile() { + return dockerEnvFile; + } + public void setDockerEnvFile(String dockerEnvFile) { + this.dockerEnvFile = dockerEnvFile; + } + public String[] getGroups() { return groups; } diff --git a/src/main/java/eu/openanalytics/services/DockerService.java b/src/main/java/eu/openanalytics/services/DockerService.java index 8c6000c0..f9ac360a 100644 --- a/src/main/java/eu/openanalytics/services/DockerService.java +++ b/src/main/java/eu/openanalytics/services/DockerService.java @@ -15,10 +15,13 @@ */ package eu.openanalytics.services; +import java.io.FileInputStream; +import java.io.IOException; import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; @@ -26,6 +29,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -246,7 +250,7 @@ private Proxy startProxy(String userName, String appName) { .image(app.getDockerImage()) .exposedPorts("3838") .cmd(app.getDockerCmd()) - .env(String.format("SHINYPROXY_USERNAME=%s", userName)) + .env(buildEnv(userName, app)) .build(); ContainerCreation container = dockerClient.createContainer(containerConfig); @@ -308,6 +312,22 @@ private boolean testContainer(Proxy proxy, int maxTries, int waitMs, int timeout return false; } + private List buildEnv(String userName, ShinyApp app) throws IOException { + List env = new ArrayList<>(); + env.add(String.format("SHINYPROXY_USERNAME=%s", userName)); + + String envFile = app.getDockerEnvFile(); + if (envFile != null && Files.isRegularFile(Paths.get(envFile))) { + Properties envProps = new Properties(); + envProps.load(new FileInputStream(envFile)); + for (Object key: envProps.keySet()) { + env.add(String.format("%s=%s", key, envProps.get(key))); + } + } + + return env; + } + private int getFreePort() { int startPort = Integer.valueOf(environment.getProperty("shiny.proxy.docker.port-range-start")); int nextPort = startPort; From 83a91a596ca89ffbf3c23d6fef66d06138a9bfb5 Mon Sep 17 00:00:00 2001 From: Frederick Michielssen Date: Thu, 15 Dec 2016 17:33:07 +0100 Subject: [PATCH 22/23] Added new property to pass volumes to the container --- .../java/eu/openanalytics/services/AppService.java | 8 ++++++++ .../eu/openanalytics/services/DockerService.java | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/main/java/eu/openanalytics/services/AppService.java b/src/main/java/eu/openanalytics/services/AppService.java index 774ae3e1..6043b5f9 100644 --- a/src/main/java/eu/openanalytics/services/AppService.java +++ b/src/main/java/eu/openanalytics/services/AppService.java @@ -55,6 +55,7 @@ public static class ShinyApp { private String[] dockerDns; private String dockerMemory; private String dockerEnvFile; + private String[] dockerVolumes; private String[] groups; public String getName() { @@ -119,6 +120,13 @@ public String getDockerEnvFile() { public void setDockerEnvFile(String dockerEnvFile) { this.dockerEnvFile = dockerEnvFile; } + + public String[] getDockerVolumes() { + return dockerVolumes; + } + public void setDockerVolumes(String[] dockerVolumes) { + this.dockerVolumes = dockerVolumes; + } public String[] getGroups() { return groups; diff --git a/src/main/java/eu/openanalytics/services/DockerService.java b/src/main/java/eu/openanalytics/services/DockerService.java index f9ac360a..b6d59a2e 100644 --- a/src/main/java/eu/openanalytics/services/DockerService.java +++ b/src/main/java/eu/openanalytics/services/DockerService.java @@ -251,6 +251,7 @@ private Proxy startProxy(String userName, String appName) { .exposedPorts("3838") .cmd(app.getDockerCmd()) .env(buildEnv(userName, app)) + .volumes(buildVolumes(app)) .build(); ContainerCreation container = dockerClient.createContainer(containerConfig); @@ -328,6 +329,18 @@ private List buildEnv(String userName, ShinyApp app) throws IOException return env; } + private Set buildVolumes(ShinyApp app) { + Set volumes = new HashSet<>(); + + if (app.getDockerVolumes() != null) { + for (String vol: app.getDockerVolumes()) { + volumes.add(vol); + } + } + + return volumes; + } + private int getFreePort() { int startPort = Integer.valueOf(environment.getProperty("shiny.proxy.docker.port-range-start")); int nextPort = startPort; From 2fd23bb1964c9774d0beb3f586060abe270f4ddb Mon Sep 17 00:00:00 2001 From: tverbeke Date: Thu, 15 Dec 2016 21:07:39 +0100 Subject: [PATCH 23/23] 0.8.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7a3de134..e6314192 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ eu.openanalytics shinyproxy - 0.7.8 + 0.8.0 jar shinyproxy