From 19a8f11ebc6d714f667082f85da4d6d887f4f539 Mon Sep 17 00:00:00 2001 From: minyk Date: Thu, 2 Nov 2017 13:27:25 +0900 Subject: [PATCH 1/7] update marathon to v.1.5.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 07dcf64..184dd3b 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ mesosphere.marathon plugin-interface_2.11 - 1.4.8 + 1.5.1 provided From 0f5a511b7ce5d388bddf8e1722c2a09bf521c113 Mon Sep 17 00:00:00 2001 From: minyk Date: Tue, 10 Apr 2018 10:04:15 +0900 Subject: [PATCH 2/7] update marathon to v.1.5.5 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 184dd3b..5de57ab 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.containx marathon-ldap jar - 1.0 + 1.0.1 Marathon LDAP authentication @@ -39,7 +39,7 @@ mesosphere.marathon plugin-interface_2.11 - 1.5.1 + 1.5.5 provided From a778d9de1b7191bddfbe596fc5827b27257c19bb Mon Sep 17 00:00:00 2001 From: minyk Date: Tue, 10 Apr 2018 10:06:08 +0900 Subject: [PATCH 3/7] Search groups with bindUser when it set. --- .../marathon/plugin/auth/util/LDAPHelper.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java b/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java index 747174f..9a2710b 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java +++ b/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java @@ -36,7 +36,7 @@ public static Set validate(String username, String userPassword, LDAPCon do { DirContext context = null; - + try { String dn = ""; String bindUser = config.getBindUser(); @@ -98,6 +98,17 @@ public static Set validate(String username, String userPassword, LDAPCon LOGGER.info("LDAP user search found {}", result.toString()); if (bindUser != null) { + dn = bindUser.replace("{username}", username); + bindPassword = (config.getBindPassword() == null) ? userPassword : config.getBindPassword(); + + LOGGER.debug("Authenticate with DN {}", dn); + env.put(Context.SECURITY_PRINCIPAL, dn); + env.put(Context.SECURITY_CREDENTIALS, bindPassword); + + context = new InitialDirContext(env); + + LOGGER.info("LDAP Auth succeeded for user {}", dn); + } else { dn = result.getNameInNamespace().toString(); if (userPassword == null || userPassword.isEmpty()) { From a7da47a9099eb781378c587bed9fe1811b5ee688 Mon Sep 17 00:00:00 2001 From: minyk Date: Fri, 27 Apr 2018 11:22:27 +0900 Subject: [PATCH 4/7] Bump up to marathon v1.6.322 --- pom.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 5de57ab..4f38e3f 100644 --- a/pom.xml +++ b/pom.xml @@ -13,6 +13,7 @@ 1.8 ${project.jdk.version} ${project.jdk.version} + 1.6.322 @@ -38,8 +39,8 @@ mesosphere.marathon - plugin-interface_2.11 - 1.5.5 + plugin-interface_2.12 + ${marathon.version} provided @@ -97,7 +98,7 @@ Marathon LDAP/AD Plugin - v${project.version} + v${project.version}-${marathon.version} ${project.version} ${project.build.directory}/marathon-ldap.jar From b960ac461b9455c21b1160074b047aa7e0987cf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ste=CC=81phane=20Cottin?= Date: Tue, 11 Sep 2018 12:29:09 +0200 Subject: [PATCH 5/7] Store permissions in LDAP groups. Add runtime updater. Took 5 hours 16 minutes --- README.md | 28 ++++ ldap/README.md | 19 +++ ldap/marathon.ldif | 15 +++ ldap/marathon.schema | 12 ++ ldap/overlay.ldif | 13 ++ ldap/refint.ldif | 14 ++ pom.xml | 2 +- .../plugin/auth/AccessRulesUpdaterTask.java | 42 ++++++ .../plugin/auth/LDAPAuthenticator.java | 71 ++++++---- .../plugin/auth/type/Authorization.java | 4 + .../marathon/plugin/auth/type/LDAPConfig.java | 16 +++ .../plugin/auth/type/PermissionType.java | 5 +- .../plugin/auth/type/UserIdentity.java | 4 +- .../marathon/plugin/auth/util/HTTPHelper.java | 6 +- .../marathon/plugin/auth/util/LDAPHelper.java | 122 +++++++++++++++--- .../marathon/plugin/auth/plugin-conf.json | 2 +- 16 files changed, 323 insertions(+), 52 deletions(-) create mode 100644 ldap/README.md create mode 100644 ldap/marathon.ldif create mode 100644 ldap/marathon.schema create mode 100644 ldap/overlay.ldif create mode 100644 ldap/refint.ldif create mode 100644 src/main/java/io/containx/marathon/plugin/auth/AccessRulesUpdaterTask.java diff --git a/README.md b/README.md index bed9986..a176524 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,34 @@ comments to the one you deploy) } ``` +see `test/resources/config.json` for a complete configuration example. + +**LDAP permissions support** + +The permissions can be stored and retrieved from the LDAP directory at regular intervals. +Permissions and group updates changes no longer require restarting marathon. +When enabled, the config key `plugins.authentication.configuration.authorization` is ignored. + +You must first add the marathon schema to your LDAP server, see the [ldap README](./ldap/README.md) . + +The schema provide two objectclasses : +- `marathonUser` add this to the user entries and in the configuration `userSearch` filter. + - Only user having this objectclass will be allowed to autenticate. (optional) +- `marathonAccess` add this objectclass to the group entries. + - Add at least one `marathonAccessRule` attribute. + - Each `marathonAccessRule` **must** contain a valid permission json like `{"allowed":"*","path":"/","type":"app"}` + + +Edit the `/var/marathon/plugins/plugin-conf.json` and configure these keys : +``` +plugins.authentication.configuration.ldap.rulesUpdaterBindUser +plugins.authentication.configuration.ldap.rulesUpdaterBindPassword +plugins.authentication.configuration.refresh-interval-seconds +``` +- `rulesUpdaterBindUser` and `rulesUpdaterBindPassword` are required for LDAP permissions. + Be careful to give this LDAP userDN search and read access to the configured `groupSubTree`. +- `refresh-interval-seconds` is optional, default is `60`. + **Configure Marathon** Depending on your environment your Marathon configuration is either using files per option, typically found under ```/etc/marathon/conf``` or options are being passed in via the service. diff --git a/ldap/README.md b/ldap/README.md new file mode 100644 index 0000000..29f7a5d --- /dev/null +++ b/ldap/README.md @@ -0,0 +1,19 @@ +## openldap schemas + +#### memberof overlay + +If you want to use the `memberof` attribute, you need to install the overlay: + +##### openldap >= 2.4 + +``` + ldapadd -Y EXTERNAL -H ldapi:/// -f overlay.ldif + ldapadd -Y EXTERNAL -H ldapi:/// -f refint.ldif +``` + +#### marathon schema +Installing the marathon schema is required if you want to store permissions in openldap. + +##### openldap >= 2.4 + +`ldapadd -Y EXTERNAL -H ldapi:/// -f marathon.ldif` \ No newline at end of file diff --git a/ldap/marathon.ldif b/ldap/marathon.ldif new file mode 100644 index 0000000..5942d0b --- /dev/null +++ b/ldap/marathon.ldif @@ -0,0 +1,15 @@ +dn: cn=marathon,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: marathon +olcAttributeTypes: ( 2.5.6.9.1.1 NAME 'marathonAccessRule' + DESC 'marathon-ldap permissions in json format.' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + USAGE userApplications ) +olcObjectClasses: ( 2.5.6.9.1 NAME 'marathonAccess' + DESC 'marathon-ldap group with permissions' + AUXILIARY + MUST ( marathonAccessRule $ cn ) ) +olcObjectClasses: ( 2.5.6.9.2 NAME 'marathonUser' + DESC 'marathon User' + AUXILIARY ) \ No newline at end of file diff --git a/ldap/marathon.schema b/ldap/marathon.schema new file mode 100644 index 0000000..068858b --- /dev/null +++ b/ldap/marathon.schema @@ -0,0 +1,12 @@ +attributetype ( 2.5.6.9.1.1 NAME 'marathonAccessRule' + DESC 'marathon-ldap permissions in json format.' + EQUALITY caseIgnoreMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + USAGE userApplications ) +objectclass ( 2.5.6.9.1 NAME 'marathonAccess' + DESC 'marathon-ldap group with permissions' + AUXILIARY + MUST ( marathonAccessRule $ cn ) ) +objectclass ( 2.5.6.9.2 NAME 'marathonUser' + DESC 'marathon User' + AUXILIARY ) diff --git a/ldap/overlay.ldif b/ldap/overlay.ldif new file mode 100644 index 0000000..c47fee3 --- /dev/null +++ b/ldap/overlay.ldif @@ -0,0 +1,13 @@ +dn: cn=module,cn=config +cn: module +objectclass: olcModuleList +objectclass: top +olcmoduleload: memberof.la +olcmodulepath: /usr/lib/ldap + +dn: olcOverlay={0}memberof,olcDatabase={1}hdb,cn=config +objectClass: olcConfig +objectClass: olcMemberOf +objectClass: olcOverlayConfig +objectClass: top +olcOverlay: memberof \ No newline at end of file diff --git a/ldap/refint.ldif b/ldap/refint.ldif new file mode 100644 index 0000000..9b42c08 --- /dev/null +++ b/ldap/refint.ldif @@ -0,0 +1,14 @@ +dn: cn=module,cn=config +cn: module +objectclass: olcModuleList +objectclass: top +olcmoduleload: refint.la +olcmodulepath: /usr/lib/ldap + +dn: olcOverlay={1}refint,olcDatabase={1}hdb,cn=config +objectClass: olcConfig +objectClass: olcOverlayConfig +objectClass: olcRefintConfig +objectClass: top +olcOverlay: {1}refint +olcRefintAttribute: memberof member manager owner \ No newline at end of file diff --git a/pom.xml b/pom.xml index 4f38e3f..c6298db 100644 --- a/pom.xml +++ b/pom.xml @@ -13,7 +13,7 @@ 1.8 ${project.jdk.version} ${project.jdk.version} - 1.6.322 + 1.6.352 diff --git a/src/main/java/io/containx/marathon/plugin/auth/AccessRulesUpdaterTask.java b/src/main/java/io/containx/marathon/plugin/auth/AccessRulesUpdaterTask.java new file mode 100644 index 0000000..8ed06a1 --- /dev/null +++ b/src/main/java/io/containx/marathon/plugin/auth/AccessRulesUpdaterTask.java @@ -0,0 +1,42 @@ +package io.containx.marathon.plugin.auth; + +import java.util.concurrent.atomic.AtomicReference; + +import io.containx.marathon.plugin.auth.type.Authorization; +import io.containx.marathon.plugin.auth.type.LDAPConfig; +import io.containx.marathon.plugin.auth.util.LDAPHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class AccessRulesUpdaterTask implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(AccessRulesUpdaterTask.class); + private LDAPConfig ldapConfig; + private AtomicReference accessRules = new AtomicReference<>(); + + + AccessRulesUpdaterTask(LDAPConfig ldapconfig) throws Exception { + this.ldapConfig = ldapconfig; + this.accessRules.set(parse()); + } + + Authorization getAccessRules() { + return accessRules.get(); + } + + private Authorization parse() throws Exception { + Authorization auth = new Authorization(); + auth.setAccess(LDAPHelper.getAccessRules(ldapConfig)); + LOGGER.debug(String.format("Authorization - %s ", auth.toString())); + return auth; + } + + @Override + public void run() { + try { + this.accessRules.set(parse()); + } catch (Exception e) { + LOGGER.error(String.format("Cannot get configuration from ldap - %s ", e)); + } + } +} diff --git a/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthenticator.java b/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthenticator.java index f0dce59..50cfb76 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthenticator.java +++ b/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthenticator.java @@ -6,8 +6,6 @@ import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListenableFutureTask; import io.containx.marathon.plugin.auth.type.AuthKey; import io.containx.marathon.plugin.auth.type.Configuration; import io.containx.marathon.plugin.auth.type.UserIdentity; @@ -19,54 +17,71 @@ import mesosphere.marathon.plugin.http.HttpResponse; import mesosphere.marathon.plugin.plugin.PluginConfiguration; import play.api.libs.json.JsObject; +import play.api.libs.json.JsValue; import scala.Option; import scala.concurrent.ExecutionContext; import scala.concurrent.Future; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.naming.NamingException; -import java.io.IOException; +import java.util.Map; import java.util.Set; -import java.util.concurrent.Callable; import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import javax.naming.NamingException; public class LDAPAuthenticator implements Authenticator, PluginConfiguration { private static final Logger LOGGER = LoggerFactory.getLogger(LDAPAuthenticator.class); + private AccessRulesUpdaterTask accessRulesUpdaterTask; + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private final long DEFAULT_INTERVAL_IN_SECONDS = 60; + private long refreshInterval = DEFAULT_INTERVAL_IN_SECONDS; private final ExecutionContext EC = ExecutionContexts .fromExecutorService( Executors.newSingleThreadExecutor(r -> new Thread(r, "Ldap-ExecutorThread")) ); - private final LoadingCache USERS = CacheBuilder.newBuilder() - .maximumSize(2000) - .expireAfterWrite(60, TimeUnit.MINUTES) - .refreshAfterWrite(5, TimeUnit.MINUTES) - .build( - CacheLoader.asyncReloading( - new CacheLoader() { - @Override - public UserIdentity load(AuthKey key) throws Exception { - return (UserIdentity) doAuth(key.getUsername(), key.getPassword()); - } - } - , Executors.newSingleThreadExecutor( - r -> new Thread(r, "Ldap-CacheLoaderExecutorThread") - ) - ) - ); + private LoadingCache USERS; private Configuration config; @Override public void initialize(scala.collection.immutable.Map map, JsObject jsObject) { + Map conf = scala.collection.JavaConverters.mapAsJavaMap(jsObject.value()); + String intervalKey = "refresh-interval-seconds"; + if(conf.containsKey(intervalKey)){ + refreshInterval = Long.parseLong(conf.get(intervalKey).toString()); + if(refreshInterval <= 0) { + refreshInterval = DEFAULT_INTERVAL_IN_SECONDS; + } + } + USERS = CacheBuilder.newBuilder() + .maximumSize(2000) + .expireAfterWrite(60, TimeUnit.MINUTES) + .refreshAfterWrite(refreshInterval, TimeUnit.SECONDS) + .build( + CacheLoader.asyncReloading( + new CacheLoader() { + @Override + public UserIdentity load(AuthKey key) throws Exception { + return (UserIdentity) doAuth(key.getUsername(), key.getPassword()); + } + } + , Executors.newSingleThreadExecutor( + r -> new Thread(r, "Ldap-CacheLoaderExecutorThread") + ) + ) + ); try { config = new ObjectMapper().readValue(jsObject.toString(), Configuration.class); - } catch (IOException e) { + if(config.getLdap().getRulesUpdaterBindUser() != null ) { + accessRulesUpdaterTask = new AccessRulesUpdaterTask(config.getLdap()); + scheduler.scheduleAtFixedRate(accessRulesUpdaterTask, refreshInterval, refreshInterval, TimeUnit.SECONDS); + } + } catch (Exception e) { LOGGER.error("Error reading configuration JSON: {}", e.getMessage(), e); } } @@ -78,6 +93,9 @@ public Future> authenticate(HttpRequest request) { private Identity doAuth(HttpRequest request) { try { + if(accessRulesUpdaterTask != null) { + config.setAuthorization(accessRulesUpdaterTask.getAccessRules()); + } AuthKey ak = HTTPHelper.authKeyFromHeaders(request); if (ak != null) { @@ -110,6 +128,10 @@ private Identity doAuth(String username, String password) throws NamingException int count = 0; int maxTries = 5; + if(accessRulesUpdaterTask != null) { + config.setAuthorization(accessRulesUpdaterTask.getAccessRules()); + } + while(true) { try { Set memberships = LDAPHelper.validate(username, password, config.getLdap()); @@ -120,7 +142,6 @@ private Identity doAuth(String username, String password) throws NamingException } } catch (Exception ex) { LOGGER.error("LDAP error Exception: {}", ex); - if (++count == maxTries) throw ex; } } diff --git a/src/main/java/io/containx/marathon/plugin/auth/type/Authorization.java b/src/main/java/io/containx/marathon/plugin/auth/type/Authorization.java index 1ecfa9b..3111bc1 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/type/Authorization.java +++ b/src/main/java/io/containx/marathon/plugin/auth/type/Authorization.java @@ -13,6 +13,10 @@ public Set getAccess() { return access; } + public void setAccess(Set access) { + this.access = access; + } + public Optional accessFor(String group) { return access.stream().filter(a -> a.getGroup().equalsIgnoreCase(group)).findFirst(); } diff --git a/src/main/java/io/containx/marathon/plugin/auth/type/LDAPConfig.java b/src/main/java/io/containx/marathon/plugin/auth/type/LDAPConfig.java index 638738d..743b61b 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/type/LDAPConfig.java +++ b/src/main/java/io/containx/marathon/plugin/auth/type/LDAPConfig.java @@ -31,6 +31,12 @@ public class LDAPConfig { @JsonProperty(required = false) private String bindPassword = null; + @JsonProperty(required = false) + private String rulesUpdaterBindUser = null; + + @JsonProperty(required = false) + private String rulesUpdaterBindPassword = null; + @JsonProperty(required = false) private boolean useSimpleAuthentication; @@ -66,6 +72,14 @@ public String getBindPassword() { return bindPassword; } + public String getRulesUpdaterBindUser() { + return rulesUpdaterBindUser; + } + + public String getRulesUpdaterBindPassword() { + return rulesUpdaterBindPassword; + } + public boolean useSimpleAuthentication() { return useSimpleAuthentication; } @@ -138,6 +152,8 @@ public String toString() { ", dn='" + dn + '\'' + ", bindUser='" + bindUser + '\'' + ", bindPassword='" + bindPassword + '\'' + + ", rulesUpdaterBindUser='" + rulesUpdaterBindUser + '\'' + + ", rulesUpdaterBindPassword='" + rulesUpdaterBindPassword + '\'' + ", userSearch='" + userSearch + '\'' + ", userSubTree='" + userSubTree + '\'' + ", groupSearch='" + groupSearch + '\'' + diff --git a/src/main/java/io/containx/marathon/plugin/auth/type/PermissionType.java b/src/main/java/io/containx/marathon/plugin/auth/type/PermissionType.java index 0cbc942..683ecb8 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/type/PermissionType.java +++ b/src/main/java/io/containx/marathon/plugin/auth/type/PermissionType.java @@ -16,7 +16,10 @@ public static PermissionType forValue(String type) { if (type != null && (type.equals("*") || type.equals("ALL"))) { return ALL; } - return PermissionType.valueOf(type.toUpperCase()); + if (type != null) { + return PermissionType.valueOf(type.toUpperCase()); + } + return null; } @JsonValue diff --git a/src/main/java/io/containx/marathon/plugin/auth/type/UserIdentity.java b/src/main/java/io/containx/marathon/plugin/auth/type/UserIdentity.java index bd14108..1abc595 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/type/UserIdentity.java +++ b/src/main/java/io/containx/marathon/plugin/auth/type/UserIdentity.java @@ -54,9 +54,7 @@ public UserIdentity applyResolvePermissions(Configuration config) { Set perms = access.get().getPermissions(); if (perms != null) { - for (Permission p : perms) { - combinedPerms.add(p); - } + combinedPerms.addAll(perms); } } return this; diff --git a/src/main/java/io/containx/marathon/plugin/auth/util/HTTPHelper.java b/src/main/java/io/containx/marathon/plugin/auth/util/HTTPHelper.java index 47ff3ce..bbe977e 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/util/HTTPHelper.java +++ b/src/main/java/io/containx/marathon/plugin/auth/util/HTTPHelper.java @@ -3,8 +3,8 @@ import io.containx.marathon.plugin.auth.type.AuthKey; import mesosphere.marathon.plugin.http.HttpRequest; import mesosphere.marathon.plugin.http.HttpResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; import scala.Option; import java.util.Base64; @@ -12,7 +12,7 @@ public final class HTTPHelper { - private static final Logger LOGGER = LoggerFactory.getLogger(HTTPHelper.class); + //private static final Logger LOGGER = LoggerFactory.getLogger(HTTPHelper.class); public static AuthKey authKeyFromHeaders(HttpRequest request) throws Exception { Option header = request.header("Authorization").headOption(); diff --git a/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java b/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java index 9a2710b..8a8e0b2 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java +++ b/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java @@ -1,6 +1,9 @@ package io.containx.marathon.plugin.auth.util; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.containx.marathon.plugin.auth.type.Access; import io.containx.marathon.plugin.auth.type.LDAPConfig; +import io.containx.marathon.plugin.auth.type.Permission; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -14,6 +17,7 @@ import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; +import java.io.IOException; import java.util.HashSet; import java.util.Hashtable; import java.util.Set; @@ -24,7 +28,7 @@ public final class LDAPHelper { private static final Logger LOGGER = LoggerFactory.getLogger(LDAPHelper.class); - public static Set validate(String username, String userPassword, LDAPConfig config) { + public static Set validate(String username, String userPassword, LDAPConfig config) throws NamingException { if (config == null) { LOGGER.warn("LDAP Configuration not defined. Skipping LDAP authentication"); @@ -34,11 +38,11 @@ public static Set validate(String username, String userPassword, LDAPCon int count = 0; int maxTries = 5; - do { + while(true) { DirContext context = null; try { - String dn = ""; + String dn; String bindUser = config.getBindUser(); String bindPassword = userPassword; @@ -49,11 +53,10 @@ public static Set validate(String username, String userPassword, LDAPCon if (config.useSimpleAuthentication()) { dn = config.getDn().replace("{username}", username); } else { - dn = new StringBuilder(config.getDn().replace("{username}", username)) - .append(",") - .append(config.getUserSubTree() != null ? config.getUserSubTree() + "," : "") - .append(config.getBase()) - .toString(); + dn = config.getDn().replace("{username}", username) + + "," + + (config.getUserSubTree() != null ? config.getUserSubTree() + "," : "") + + config.getBase(); } } @@ -81,9 +84,8 @@ public static Set validate(String username, String userPassword, LDAPCon String searchString = config.getUserSearch().replace("{username}", username); String searchContext = config.getBase(); if (config.getUserSubTree() != null) { - searchContext = new StringBuilder(config.getUserSubTree()) - .append(",").append(searchContext) - .toString(); + searchContext = config.getUserSubTree() + + "," + searchContext; } LOGGER.info("LDAP searching {} in {}", searchString, searchContext); NamingEnumeration renum = @@ -109,9 +111,9 @@ public static Set validate(String username, String userPassword, LDAPCon LOGGER.info("LDAP Auth succeeded for user {}", dn); } else { - dn = result.getNameInNamespace().toString(); + dn = result.getNameInNamespace(); - if (userPassword == null || userPassword.isEmpty()) { + if (userPassword.isEmpty()) { return null; } @@ -145,9 +147,8 @@ public static Set validate(String username, String userPassword, LDAPCon searchString = config.getGroupSearch().replace("{username}", username); searchContext = config.getBase(); if (config.getUserSubTree() != null) { - searchContext = new StringBuilder(config.getGroupSubTree()) - .append(",").append(searchContext) - .toString(); + searchContext = config.getGroupSubTree() + + "," + searchContext; } LOGGER.debug("LDAP searching for group membership {} in {}", searchString, searchContext); renum = context.search(searchContext, searchString, controls); @@ -166,6 +167,7 @@ public static Set validate(String username, String userPassword, LDAPCon } catch (NamingException e) { LOGGER.error("LDAP NamingException during authentication: {}", e.getMessage()); + if (++count == maxTries) throw e; } finally { try { if (context != null) { @@ -175,8 +177,92 @@ public static Set validate(String username, String userPassword, LDAPCon LOGGER.error("LDAP exception handling resource cleanup: {}", e.getMessage()); } } - } while(++count < maxTries); + } + } + + public static Set getAccessRules(LDAPConfig config) throws NamingException { + + if (config == null) { + LOGGER.warn("LDAP Configuration not defined. Skipping LDAP authentication"); + return null; + } + + int count = 0; + int maxTries = 5; + + while(true) { + DirContext context = null; + + try { + String bindUser = config.getRulesUpdaterBindUser(); + String bindPassword = config.getRulesUpdaterBindPassword(); + + LOGGER.info("LDAP trying to connect as {} on {}", bindUser, config.getUrl()); + Hashtable env = new Hashtable<>(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put(Context.PROVIDER_URL, config.getUrl()); + env.put(Context.SECURITY_PRINCIPAL, bindUser); + env.put(Context.SECURITY_CREDENTIALS, bindPassword); + env.put("com.sun.jndi.ldap.connect.timeout", config.getLdapConnectTimeout().toString()); + env.put("com.sun.jndi.ldap.read.timeout", config.getLdapReadTimeout().toString()); + + context = new InitialDirContext(env); + + LOGGER.debug("getEnvironment: " + context.getEnvironment()); + + // if an exception wasn't raised, then we managed to bind to the directory + LOGGER.info("LDAP Bind succeeded for user {}", bindUser); - return null; + SearchControls controls = new SearchControls(); + controls.setSearchScope(SUBTREE_SCOPE); + // openLDAP needs the following (assumption). Does anything else break if it's added? + controls.setReturningAttributes(new String[]{"*", "+"}); + + Set accessRules = new HashSet<>(); + String searchString = "(&(ObjectClass=marathonAccess)(ObjectClass=groupOfNames))"; + String searchContext = config.getBase(); + if (config.getUserSubTree() != null) { + searchContext = config.getGroupSubTree() + + "," + searchContext; + } + LOGGER.debug("LDAP searching for access rules {} in {}", searchString, searchContext); + NamingEnumeration renum = context.search(searchContext, searchString, controls); + + while (renum.hasMore()) { + SearchResult group = renum.next(); + String groupName = group.getAttributes().get("cn").get().toString(); + Access access = new Access(); + access.setGroup(groupName); + Set permissions = new HashSet<>(); + NamingEnumeration rules = group.getAttributes().get("marathonAccessRule").getAll(); + while (rules.hasMore()) { + try { + Permission p = new ObjectMapper().readValue(rules.next().toString(), Permission.class); + permissions.add(p); + } catch(IOException e) { + LOGGER.error("Error reading permission JSON: {}", e.getMessage(), e); + } + } + access.setPermissions(permissions); + accessRules.add(access); + LOGGER.debug("LDAP found {} in group {}", group, groupName); + } + + LOGGER.info("LDAP accessRules {}", accessRules); + return accessRules; + + } catch (NamingException e) { + LOGGER.error("LDAP NamingException during authentication: {}", e.getMessage()); + if (++count == maxTries) throw e; + } finally { + try { + if (context != null) { + context.close(); + } + } catch (NamingException e) { + LOGGER.error("LDAP exception handling resource cleanup: {}", e.getMessage()); + } + } + } } } diff --git a/src/main/resources/io/containx/marathon/plugin/auth/plugin-conf.json b/src/main/resources/io/containx/marathon/plugin/auth/plugin-conf.json index 53c2ff4..69abf52 100644 --- a/src/main/resources/io/containx/marathon/plugin/auth/plugin-conf.json +++ b/src/main/resources/io/containx/marathon/plugin/auth/plugin-conf.json @@ -14,7 +14,7 @@ "dn": "uid={username}", "bindUser": "usernameToBind", "bindPassword": "passwordToBind", - "userSearch": "(&(uid={username})(objectClass=inetOrgPerson))", + "userSearch": "(&(uid={username})(objectClass=marathonUser))", "userSubTree": "ou=People", "groupSearch": "(&(memberUid={username})(objectClass=posixGroup))", "groupSubTree": "ou=Group", From 83f220f6cff42903284a0b0a8f74559bb82cdbe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ste=CC=81phane=20Cottin?= Date: Wed, 12 Sep 2018 00:17:01 +0200 Subject: [PATCH 6/7] keep static permissions and merge them with LDAP permissions on refresh. Took 1 hour 39 minutes --- .../plugin/auth/AccessRulesUpdaterTask.java | 15 +++++++++------ .../marathon/plugin/auth/LDAPAuthenticator.java | 4 ++-- .../marathon/plugin/auth/util/LDAPHelper.java | 3 +-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/containx/marathon/plugin/auth/AccessRulesUpdaterTask.java b/src/main/java/io/containx/marathon/plugin/auth/AccessRulesUpdaterTask.java index 8ed06a1..241ab19 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/AccessRulesUpdaterTask.java +++ b/src/main/java/io/containx/marathon/plugin/auth/AccessRulesUpdaterTask.java @@ -1,9 +1,11 @@ package io.containx.marathon.plugin.auth; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import io.containx.marathon.plugin.auth.type.Access; import io.containx.marathon.plugin.auth.type.Authorization; -import io.containx.marathon.plugin.auth.type.LDAPConfig; +import io.containx.marathon.plugin.auth.type.Configuration; import io.containx.marathon.plugin.auth.util.LDAPHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,13 +13,14 @@ public class AccessRulesUpdaterTask implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(AccessRulesUpdaterTask.class); - private LDAPConfig ldapConfig; + private Configuration config; private AtomicReference accessRules = new AtomicReference<>(); + private Set staticAccessRules; - AccessRulesUpdaterTask(LDAPConfig ldapconfig) throws Exception { - this.ldapConfig = ldapconfig; - this.accessRules.set(parse()); + AccessRulesUpdaterTask(Configuration config) { + this.config = config; + staticAccessRules = config.getAuthorization().getAccess(); } Authorization getAccessRules() { @@ -26,7 +29,7 @@ Authorization getAccessRules() { private Authorization parse() throws Exception { Authorization auth = new Authorization(); - auth.setAccess(LDAPHelper.getAccessRules(ldapConfig)); + auth.setAccess(LDAPHelper.getAccessRules(config.getLdap(),staticAccessRules)); LOGGER.debug(String.format("Authorization - %s ", auth.toString())); return auth; } diff --git a/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthenticator.java b/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthenticator.java index 50cfb76..407cb4e 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthenticator.java +++ b/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthenticator.java @@ -78,8 +78,8 @@ public UserIdentity load(AuthKey key) throws Exception { try { config = new ObjectMapper().readValue(jsObject.toString(), Configuration.class); if(config.getLdap().getRulesUpdaterBindUser() != null ) { - accessRulesUpdaterTask = new AccessRulesUpdaterTask(config.getLdap()); - scheduler.scheduleAtFixedRate(accessRulesUpdaterTask, refreshInterval, refreshInterval, TimeUnit.SECONDS); + accessRulesUpdaterTask = new AccessRulesUpdaterTask(config); + scheduler.scheduleAtFixedRate(accessRulesUpdaterTask, 0, refreshInterval, TimeUnit.SECONDS); } } catch (Exception e) { LOGGER.error("Error reading configuration JSON: {}", e.getMessage(), e); diff --git a/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java b/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java index 8a8e0b2..e72c160 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java +++ b/src/main/java/io/containx/marathon/plugin/auth/util/LDAPHelper.java @@ -180,7 +180,7 @@ public static Set validate(String username, String userPassword, LDAPCon } } - public static Set getAccessRules(LDAPConfig config) throws NamingException { + public static Set getAccessRules(LDAPConfig config, Set accessRules) throws NamingException { if (config == null) { LOGGER.warn("LDAP Configuration not defined. Skipping LDAP authentication"); @@ -218,7 +218,6 @@ public static Set getAccessRules(LDAPConfig config) throws NamingExcepti // openLDAP needs the following (assumption). Does anything else break if it's added? controls.setReturningAttributes(new String[]{"*", "+"}); - Set accessRules = new HashSet<>(); String searchString = "(&(ObjectClass=marathonAccess)(ObjectClass=groupOfNames))"; String searchContext = config.getBase(); if (config.getUserSubTree() != null) { From 9b68eedfb1872025057cb9cd341d06ee5e1b918e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ste=CC=81phane=20Cottin?= Date: Wed, 12 Sep 2018 00:18:15 +0200 Subject: [PATCH 7/7] add a new 'resource' permission type. --- .../marathon/plugin/auth/LDAPAuthorizor.java | 12 ++++++++++++ .../containx/marathon/plugin/auth/type/Action.java | 7 +++++-- .../marathon/plugin/auth/type/EntityType.java | 3 ++- .../containx/marathon/plugin/auth/plugin-conf.json | 9 +++++++++ src/test/resources/config.json | 4 ++++ 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthorizor.java b/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthorizor.java index b6e02c8..a3cf1be 100644 --- a/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthorizor.java +++ b/src/main/java/io/containx/marathon/plugin/auth/LDAPAuthorizor.java @@ -7,6 +7,7 @@ import mesosphere.marathon.plugin.PathId; import mesosphere.marathon.plugin.RunSpec; import mesosphere.marathon.plugin.auth.AuthorizedAction; +import mesosphere.marathon.plugin.auth.AuthorizedResource; import mesosphere.marathon.plugin.auth.Authorizer; import mesosphere.marathon.plugin.auth.Identity; import mesosphere.marathon.plugin.http.HttpResponse; @@ -37,6 +38,11 @@ public boolean isAuthorized(Identity identity, AuthorizedAction