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 07dcf64..c6298db 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,13 +6,14 @@
io.containx
marathon-ldap
jar
- 1.0
+ 1.0.1
Marathon LDAP authentication
1.8
${project.jdk.version}
${project.jdk.version}
+ 1.6.352
@@ -38,8 +39,8 @@
mesosphere.marathon
- plugin-interface_2.11
- 1.4.8
+ 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
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..241ab19
--- /dev/null
+++ b/src/main/java/io/containx/marathon/plugin/auth/AccessRulesUpdaterTask.java
@@ -0,0 +1,45 @@
+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.Configuration;
+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 Configuration config;
+ private AtomicReference accessRules = new AtomicReference<>();
+ private Set staticAccessRules;
+
+
+ AccessRulesUpdaterTask(Configuration config) {
+ this.config = config;
+ staticAccessRules = config.getAuthorization().getAccess();
+ }
+
+ Authorization getAccessRules() {
+ return accessRules.get();
+ }
+
+ private Authorization parse() throws Exception {
+ Authorization auth = new Authorization();
+ auth.setAccess(LDAPHelper.getAccessRules(config.getLdap(),staticAccessRules));
+ 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..407cb4e 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);
+ scheduler.scheduleAtFixedRate(accessRulesUpdaterTask, 0, refreshInterval, TimeUnit.SECONDS);
+ }
+ } catch (Exception e) {
LOGGER.error("Error reading configuration JSON: {}", e.getMessage(), e);
}
}
@@ -78,6 +93,9 @@ public Future