Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ldap permissions support #23

Merged
merged 7 commits into from
Oct 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions ldap/README.md
Original file line number Diff line number Diff line change
@@ -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`
15 changes: 15 additions & 0 deletions ldap/marathon.ldif
Original file line number Diff line number Diff line change
@@ -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 )
12 changes: 12 additions & 0 deletions ldap/marathon.schema
Original file line number Diff line number Diff line change
@@ -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 )
13 changes: 13 additions & 0 deletions ldap/overlay.ldif
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions ldap/refint.ldif
Original file line number Diff line number Diff line change
@@ -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
9 changes: 5 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
<groupId>io.containx</groupId>
<artifactId>marathon-ldap</artifactId>
<packaging>jar</packaging>
<version>1.0</version>
<version>1.0.1</version>
<name>Marathon LDAP authentication</name>

<properties>
<project.jdk.version>1.8</project.jdk.version>
<maven.compiler.source>${project.jdk.version}</maven.compiler.source>
<maven.compiler.target>${project.jdk.version}</maven.compiler.target>
<marathon.version>1.6.352</marathon.version>
</properties>

<licenses>
Expand All @@ -38,8 +39,8 @@
<dependencies>
<dependency>
<groupId>mesosphere.marathon</groupId>
<artifactId>plugin-interface_2.11</artifactId>
<version>1.4.8</version>
<artifactId>plugin-interface_2.12</artifactId>
<version>${marathon.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -97,7 +98,7 @@
</executions>
<configuration>
<description>Marathon LDAP/AD Plugin</description>
<releaseName>v${project.version}</releaseName>
<releaseName>v${project.version}-${marathon.version}</releaseName>
<tag>${project.version}</tag>
<artifact>${project.build.directory}/marathon-ldap.jar</artifact>

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Authorization> accessRules = new AtomicReference<>();
private Set<Access> 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));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<AuthKey, UserIdentity> USERS = CacheBuilder.newBuilder()
.maximumSize(2000)
.expireAfterWrite(60, TimeUnit.MINUTES)
.refreshAfterWrite(5, TimeUnit.MINUTES)
.build(
CacheLoader.asyncReloading(
new CacheLoader<AuthKey, UserIdentity>() {
@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<AuthKey, UserIdentity> USERS;

private Configuration config;

@Override
public void initialize(scala.collection.immutable.Map<String, Object> map, JsObject jsObject) {
Map<String, JsValue> 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<AuthKey, UserIdentity>() {
@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);
}
}
Expand All @@ -78,6 +93,9 @@ public Future<Option<Identity>> authenticate(HttpRequest request) {

private Identity doAuth(HttpRequest request) {
try {
if(accessRulesUpdaterTask != null) {
config.setAuthorization(accessRulesUpdaterTask.getAccessRules());
}
AuthKey ak = HTTPHelper.authKeyFromHeaders(request);
if (ak != null) {

Expand Down Expand Up @@ -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<String> memberships = LDAPHelper.validate(username, password, config.getLdap());
Expand All @@ -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;
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/containx/marathon/plugin/auth/LDAPAuthorizor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -37,6 +38,11 @@ public <Resource> boolean isAuthorized(Identity identity, AuthorizedAction<Resou
if (action == Action.VIEW_RESOURCE) {
return true;
}

if (resource instanceof AuthorizedResource) {
return isAuthorized(user, action);
}

return resource instanceof PathId && isAuthorized(user, action, (PathId) resource);
}
return false;
Expand All @@ -48,6 +54,12 @@ private boolean isAuthorized(UserIdentity identity, Action action, PathId path)
return authorized;
}

private boolean isAuthorized(UserIdentity identity, Action action) {
boolean authorized = identity.isAuthorized(action, "/");
LOGGER.debug("IsAuthorized (private): Action :: {}, Path = {}, authorized = {}", action, authorized);
return authorized;
}

@Override
public void handleNotAuthorized(Identity identity, HttpResponse response) {
HTTPHelper.applyNotAuthorizedToResponse(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ public enum Action {
UPDATE_APP(UpdateRunSpec$.MODULE$, PermissionType.UPDATE, EntityType.APP),
DELETE_APP(DeleteRunSpec$.MODULE$, PermissionType.DELETE, EntityType.APP),
VIEW_APP(ViewRunSpec$.MODULE$, PermissionType.VIEW, EntityType.APP),
VIEW_RESOURCE(ViewResource$.MODULE$, PermissionType.VIEW, EntityType.APP),
UPDATE_RESOURCE(UpdateResource$.MODULE$, PermissionType.UPDATE, EntityType.APP),

// Resource Actions
VIEW_RESOURCE(ViewResource$.MODULE$, PermissionType.VIEW, EntityType.RESOURCE),
UPDATE_RESOURCE(UpdateResource$.MODULE$, PermissionType.UPDATE, EntityType.RESOURCE),
DELETE_RESOURCE(DeleteResource$.MODULE$, PermissionType.DELETE, EntityType.RESOURCE),

// Group Actions
CREATE_GROUP(CreateGroup$.MODULE$, PermissionType.CREATE, EntityType.GROUP),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ public Set<Access> getAccess() {
return access;
}

public void setAccess(Set<Access> access) {
this.access = access;
}

public Optional<Access> accessFor(String group) {
return access.stream().filter(a -> a.getGroup().equalsIgnoreCase(group)).findFirst();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

public enum EntityType {
APP,
GROUP
GROUP,
RESOURCE
;

@JsonCreator
Expand Down
Loading