Skip to content

Commit 4f2fc95

Browse files
authored
Merge pull request #23 from vixns/ldap-permissions
Ldap permissions support
2 parents 4ec0288 + 9b68eed commit 4f2fc95

File tree

20 files changed

+373
-59
lines changed

20 files changed

+373
-59
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,34 @@ comments to the one you deploy)
129129
}
130130
```
131131

132+
see `test/resources/config.json` for a complete configuration example.
133+
134+
**LDAP permissions support**
135+
136+
The permissions can be stored and retrieved from the LDAP directory at regular intervals.
137+
Permissions and group updates changes no longer require restarting marathon.
138+
When enabled, the config key `plugins.authentication.configuration.authorization` is ignored.
139+
140+
You must first add the marathon schema to your LDAP server, see the [ldap README](./ldap/README.md) .
141+
142+
The schema provide two objectclasses :
143+
- `marathonUser` add this to the user entries and in the configuration `userSearch` filter.
144+
- Only user having this objectclass will be allowed to autenticate. (optional)
145+
- `marathonAccess` add this objectclass to the group entries.
146+
- Add at least one `marathonAccessRule` attribute.
147+
- Each `marathonAccessRule` **must** contain a valid permission json like `{"allowed":"*","path":"/","type":"app"}`
148+
149+
150+
Edit the `/var/marathon/plugins/plugin-conf.json` and configure these keys :
151+
```
152+
plugins.authentication.configuration.ldap.rulesUpdaterBindUser
153+
plugins.authentication.configuration.ldap.rulesUpdaterBindPassword
154+
plugins.authentication.configuration.refresh-interval-seconds
155+
```
156+
- `rulesUpdaterBindUser` and `rulesUpdaterBindPassword` are required for LDAP permissions.
157+
Be careful to give this LDAP userDN search and read access to the configured `groupSubTree`.
158+
- `refresh-interval-seconds` is optional, default is `60`.
159+
132160
**Configure Marathon**
133161

134162
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.

ldap/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## openldap schemas
2+
3+
#### memberof overlay
4+
5+
If you want to use the `memberof` attribute, you need to install the overlay:
6+
7+
##### openldap >= 2.4
8+
9+
```
10+
ldapadd -Y EXTERNAL -H ldapi:/// -f overlay.ldif
11+
ldapadd -Y EXTERNAL -H ldapi:/// -f refint.ldif
12+
```
13+
14+
#### marathon schema
15+
Installing the marathon schema is required if you want to store permissions in openldap.
16+
17+
##### openldap >= 2.4
18+
19+
`ldapadd -Y EXTERNAL -H ldapi:/// -f marathon.ldif`

ldap/marathon.ldif

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
dn: cn=marathon,cn=schema,cn=config
2+
objectClass: olcSchemaConfig
3+
cn: marathon
4+
olcAttributeTypes: ( 2.5.6.9.1.1 NAME 'marathonAccessRule'
5+
DESC 'marathon-ldap permissions in json format.'
6+
EQUALITY caseIgnoreMatch
7+
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
8+
USAGE userApplications )
9+
olcObjectClasses: ( 2.5.6.9.1 NAME 'marathonAccess'
10+
DESC 'marathon-ldap group with permissions'
11+
AUXILIARY
12+
MUST ( marathonAccessRule $ cn ) )
13+
olcObjectClasses: ( 2.5.6.9.2 NAME 'marathonUser'
14+
DESC 'marathon User'
15+
AUXILIARY )

ldap/marathon.schema

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
attributetype ( 2.5.6.9.1.1 NAME 'marathonAccessRule'
2+
DESC 'marathon-ldap permissions in json format.'
3+
EQUALITY caseIgnoreMatch
4+
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
5+
USAGE userApplications )
6+
objectclass ( 2.5.6.9.1 NAME 'marathonAccess'
7+
DESC 'marathon-ldap group with permissions'
8+
AUXILIARY
9+
MUST ( marathonAccessRule $ cn ) )
10+
objectclass ( 2.5.6.9.2 NAME 'marathonUser'
11+
DESC 'marathon User'
12+
AUXILIARY )

ldap/overlay.ldif

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
dn: cn=module,cn=config
2+
cn: module
3+
objectclass: olcModuleList
4+
objectclass: top
5+
olcmoduleload: memberof.la
6+
olcmodulepath: /usr/lib/ldap
7+
8+
dn: olcOverlay={0}memberof,olcDatabase={1}hdb,cn=config
9+
objectClass: olcConfig
10+
objectClass: olcMemberOf
11+
objectClass: olcOverlayConfig
12+
objectClass: top
13+
olcOverlay: memberof

ldap/refint.ldif

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
dn: cn=module,cn=config
2+
cn: module
3+
objectclass: olcModuleList
4+
objectclass: top
5+
olcmoduleload: refint.la
6+
olcmodulepath: /usr/lib/ldap
7+
8+
dn: olcOverlay={1}refint,olcDatabase={1}hdb,cn=config
9+
objectClass: olcConfig
10+
objectClass: olcOverlayConfig
11+
objectClass: olcRefintConfig
12+
objectClass: top
13+
olcOverlay: {1}refint
14+
olcRefintAttribute: memberof member manager owner

pom.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
<groupId>io.containx</groupId>
77
<artifactId>marathon-ldap</artifactId>
88
<packaging>jar</packaging>
9-
<version>1.0</version>
9+
<version>1.0.1</version>
1010
<name>Marathon LDAP authentication</name>
1111

1212
<properties>
1313
<project.jdk.version>1.8</project.jdk.version>
1414
<maven.compiler.source>${project.jdk.version}</maven.compiler.source>
1515
<maven.compiler.target>${project.jdk.version}</maven.compiler.target>
16+
<marathon.version>1.6.352</marathon.version>
1617
</properties>
1718

1819
<licenses>
@@ -38,8 +39,8 @@
3839
<dependencies>
3940
<dependency>
4041
<groupId>mesosphere.marathon</groupId>
41-
<artifactId>plugin-interface_2.11</artifactId>
42-
<version>1.4.8</version>
42+
<artifactId>plugin-interface_2.12</artifactId>
43+
<version>${marathon.version}</version>
4344
<scope>provided</scope>
4445
</dependency>
4546
<dependency>
@@ -97,7 +98,7 @@
9798
</executions>
9899
<configuration>
99100
<description>Marathon LDAP/AD Plugin</description>
100-
<releaseName>v${project.version}</releaseName>
101+
<releaseName>v${project.version}-${marathon.version}</releaseName>
101102
<tag>${project.version}</tag>
102103
<artifact>${project.build.directory}/marathon-ldap.jar</artifact>
103104

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package io.containx.marathon.plugin.auth;
2+
3+
import java.util.Set;
4+
import java.util.concurrent.atomic.AtomicReference;
5+
6+
import io.containx.marathon.plugin.auth.type.Access;
7+
import io.containx.marathon.plugin.auth.type.Authorization;
8+
import io.containx.marathon.plugin.auth.type.Configuration;
9+
import io.containx.marathon.plugin.auth.util.LDAPHelper;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
13+
14+
public class AccessRulesUpdaterTask implements Runnable {
15+
private static final Logger LOGGER = LoggerFactory.getLogger(AccessRulesUpdaterTask.class);
16+
private Configuration config;
17+
private AtomicReference<Authorization> accessRules = new AtomicReference<>();
18+
private Set<Access> staticAccessRules;
19+
20+
21+
AccessRulesUpdaterTask(Configuration config) {
22+
this.config = config;
23+
staticAccessRules = config.getAuthorization().getAccess();
24+
}
25+
26+
Authorization getAccessRules() {
27+
return accessRules.get();
28+
}
29+
30+
private Authorization parse() throws Exception {
31+
Authorization auth = new Authorization();
32+
auth.setAccess(LDAPHelper.getAccessRules(config.getLdap(),staticAccessRules));
33+
LOGGER.debug(String.format("Authorization - %s ", auth.toString()));
34+
return auth;
35+
}
36+
37+
@Override
38+
public void run() {
39+
try {
40+
this.accessRules.set(parse());
41+
} catch (Exception e) {
42+
LOGGER.error(String.format("Cannot get configuration from ldap - %s ", e));
43+
}
44+
}
45+
}

src/main/java/io/containx/marathon/plugin/auth/LDAPAuthenticator.java

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
import com.google.common.cache.CacheBuilder;
77
import com.google.common.cache.CacheLoader;
88
import com.google.common.cache.LoadingCache;
9-
import com.google.common.util.concurrent.ListenableFuture;
10-
import com.google.common.util.concurrent.ListenableFutureTask;
119
import io.containx.marathon.plugin.auth.type.AuthKey;
1210
import io.containx.marathon.plugin.auth.type.Configuration;
1311
import io.containx.marathon.plugin.auth.type.UserIdentity;
@@ -19,54 +17,71 @@
1917
import mesosphere.marathon.plugin.http.HttpResponse;
2018
import mesosphere.marathon.plugin.plugin.PluginConfiguration;
2119
import play.api.libs.json.JsObject;
20+
import play.api.libs.json.JsValue;
2221
import scala.Option;
2322
import scala.concurrent.ExecutionContext;
2423
import scala.concurrent.Future;
2524
import org.slf4j.Logger;
2625
import org.slf4j.LoggerFactory;
2726

28-
import javax.naming.NamingException;
29-
import java.io.IOException;
27+
import java.util.Map;
3028
import java.util.Set;
31-
import java.util.concurrent.Callable;
3229
import java.util.concurrent.Executors;
33-
import java.util.concurrent.ThreadFactory;
30+
import java.util.concurrent.ScheduledExecutorService;
3431
import java.util.concurrent.TimeUnit;
32+
import javax.naming.NamingException;
3533

3634
public class LDAPAuthenticator implements Authenticator, PluginConfiguration {
3735

3836
private static final Logger LOGGER = LoggerFactory.getLogger(LDAPAuthenticator.class);
37+
private AccessRulesUpdaterTask accessRulesUpdaterTask;
38+
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
39+
private final long DEFAULT_INTERVAL_IN_SECONDS = 60;
40+
private long refreshInterval = DEFAULT_INTERVAL_IN_SECONDS;
3941

4042
private final ExecutionContext EC = ExecutionContexts
4143
.fromExecutorService(
4244
Executors.newSingleThreadExecutor(r -> new Thread(r, "Ldap-ExecutorThread"))
4345
);
4446

45-
private final LoadingCache<AuthKey, UserIdentity> USERS = CacheBuilder.newBuilder()
46-
.maximumSize(2000)
47-
.expireAfterWrite(60, TimeUnit.MINUTES)
48-
.refreshAfterWrite(5, TimeUnit.MINUTES)
49-
.build(
50-
CacheLoader.asyncReloading(
51-
new CacheLoader<AuthKey, UserIdentity>() {
52-
@Override
53-
public UserIdentity load(AuthKey key) throws Exception {
54-
return (UserIdentity) doAuth(key.getUsername(), key.getPassword());
55-
}
56-
}
57-
, Executors.newSingleThreadExecutor(
58-
r -> new Thread(r, "Ldap-CacheLoaderExecutorThread")
59-
)
60-
)
61-
);
47+
private LoadingCache<AuthKey, UserIdentity> USERS;
6248

6349
private Configuration config;
6450

6551
@Override
6652
public void initialize(scala.collection.immutable.Map<String, Object> map, JsObject jsObject) {
53+
Map<String, JsValue> conf = scala.collection.JavaConverters.mapAsJavaMap(jsObject.value());
54+
String intervalKey = "refresh-interval-seconds";
55+
if(conf.containsKey(intervalKey)){
56+
refreshInterval = Long.parseLong(conf.get(intervalKey).toString());
57+
if(refreshInterval <= 0) {
58+
refreshInterval = DEFAULT_INTERVAL_IN_SECONDS;
59+
}
60+
}
61+
USERS = CacheBuilder.newBuilder()
62+
.maximumSize(2000)
63+
.expireAfterWrite(60, TimeUnit.MINUTES)
64+
.refreshAfterWrite(refreshInterval, TimeUnit.SECONDS)
65+
.build(
66+
CacheLoader.asyncReloading(
67+
new CacheLoader<AuthKey, UserIdentity>() {
68+
@Override
69+
public UserIdentity load(AuthKey key) throws Exception {
70+
return (UserIdentity) doAuth(key.getUsername(), key.getPassword());
71+
}
72+
}
73+
, Executors.newSingleThreadExecutor(
74+
r -> new Thread(r, "Ldap-CacheLoaderExecutorThread")
75+
)
76+
)
77+
);
6778
try {
6879
config = new ObjectMapper().readValue(jsObject.toString(), Configuration.class);
69-
} catch (IOException e) {
80+
if(config.getLdap().getRulesUpdaterBindUser() != null ) {
81+
accessRulesUpdaterTask = new AccessRulesUpdaterTask(config);
82+
scheduler.scheduleAtFixedRate(accessRulesUpdaterTask, 0, refreshInterval, TimeUnit.SECONDS);
83+
}
84+
} catch (Exception e) {
7085
LOGGER.error("Error reading configuration JSON: {}", e.getMessage(), e);
7186
}
7287
}
@@ -78,6 +93,9 @@ public Future<Option<Identity>> authenticate(HttpRequest request) {
7893

7994
private Identity doAuth(HttpRequest request) {
8095
try {
96+
if(accessRulesUpdaterTask != null) {
97+
config.setAuthorization(accessRulesUpdaterTask.getAccessRules());
98+
}
8199
AuthKey ak = HTTPHelper.authKeyFromHeaders(request);
82100
if (ak != null) {
83101

@@ -110,6 +128,10 @@ private Identity doAuth(String username, String password) throws NamingException
110128
int count = 0;
111129
int maxTries = 5;
112130

131+
if(accessRulesUpdaterTask != null) {
132+
config.setAuthorization(accessRulesUpdaterTask.getAccessRules());
133+
}
134+
113135
while(true) {
114136
try {
115137
Set<String> memberships = LDAPHelper.validate(username, password, config.getLdap());
@@ -120,7 +142,6 @@ private Identity doAuth(String username, String password) throws NamingException
120142
}
121143
} catch (Exception ex) {
122144
LOGGER.error("LDAP error Exception: {}", ex);
123-
124145
if (++count == maxTries) throw ex;
125146
}
126147
}

src/main/java/io/containx/marathon/plugin/auth/LDAPAuthorizor.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import mesosphere.marathon.plugin.PathId;
88
import mesosphere.marathon.plugin.RunSpec;
99
import mesosphere.marathon.plugin.auth.AuthorizedAction;
10+
import mesosphere.marathon.plugin.auth.AuthorizedResource;
1011
import mesosphere.marathon.plugin.auth.Authorizer;
1112
import mesosphere.marathon.plugin.auth.Identity;
1213
import mesosphere.marathon.plugin.http.HttpResponse;
@@ -37,6 +38,11 @@ public <Resource> boolean isAuthorized(Identity identity, AuthorizedAction<Resou
3738
if (action == Action.VIEW_RESOURCE) {
3839
return true;
3940
}
41+
42+
if (resource instanceof AuthorizedResource) {
43+
return isAuthorized(user, action);
44+
}
45+
4046
return resource instanceof PathId && isAuthorized(user, action, (PathId) resource);
4147
}
4248
return false;
@@ -48,6 +54,12 @@ private boolean isAuthorized(UserIdentity identity, Action action, PathId path)
4854
return authorized;
4955
}
5056

57+
private boolean isAuthorized(UserIdentity identity, Action action) {
58+
boolean authorized = identity.isAuthorized(action, "/");
59+
LOGGER.debug("IsAuthorized (private): Action :: {}, Path = {}, authorized = {}", action, authorized);
60+
return authorized;
61+
}
62+
5163
@Override
5264
public void handleNotAuthorized(Identity identity, HttpResponse response) {
5365
HTTPHelper.applyNotAuthorizedToResponse(response);

0 commit comments

Comments
 (0)