-
Notifications
You must be signed in to change notification settings - Fork 716
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #307 from lmenezes/ldap
Add LDAP group search
- Loading branch information
Showing
4 changed files
with
145 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,56 @@ You can run cerebro listening on a different host and port(defaults to 0.0.0.0:9 | |
bin/cerebro -Dhttp.port=1234 -Dhttp.address=127.0.0.1 | ||
``` | ||
|
||
#### LDAP config | ||
|
||
LDAP can be configured using environment variables. If you typically run cerebro using docker, | ||
you can pass a file with all the env vars. The file would look like: | ||
|
||
```bash | ||
# Set it to ldap to activate ldap authorization | ||
AUTH_TYPE=ldap | ||
|
||
# Your ldap url | ||
LDAP_URL=ldap://exammple.com:389 | ||
|
||
LDAP_BASE_DN=OU=users,DC=example,DC=com | ||
|
||
# Usually method should be "simple" otherwise, set it to the SASL mechanisms | ||
LDAP_METHOD=simple | ||
|
||
# user-template executes a string.format() operation where | ||
# username is passed in first, followed by base-dn. Some examples | ||
# - %s => leave user untouched | ||
# - %[email protected] => append "@domain.com" to username | ||
# - uid=%s,%s => usual case of OpenLDAP | ||
LDAP_USER_TEMPLATE=%[email protected] | ||
|
||
# User identifier that can perform searches | ||
[email protected] | ||
LDAP_BIND_PWD=adminpass | ||
|
||
# Group membership settings (optional) | ||
|
||
# If left unset LDAP_BASE_DN will be used | ||
# LDAP_GROUP_BASE_DN=OU=users,DC=example,DC=com | ||
|
||
# Attribute that represent the user, for example uid or mail | ||
# LDAP_USER_ATTR=mail | ||
|
||
# Filter that tests membership of the group. If this property is empty then there is no group membership check | ||
# AD example => memberOf=CN=mygroup,ou=ouofthegroup,DC=domain,DC=com | ||
# OpenLDAP example => CN=mygroup | ||
# LDAP_GROUP=memberOf=memberOf=CN=mygroup,ou=ouofthegroup,DC=domain,DC=com | ||
|
||
``` | ||
|
||
You can the pass this file as argument using: | ||
|
||
```bash | ||
docker run -p 9000:9000 --env-file env-ldap lmenezes/cerebro | ||
``` | ||
|
||
|
||
#### Other settings | ||
|
||
Other settings are exposed through the **conf/application.conf** file found on the application directory. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,69 @@ | ||
package controllers.auth.ldap | ||
|
||
import java.util.Hashtable | ||
import javax.naming._ | ||
import javax.naming.directory._ | ||
|
||
import com.google.inject.Inject | ||
import com.sun.jndi.ldap.LdapCtxFactory | ||
import controllers.auth.AuthService | ||
import play.api.Configuration | ||
import javax.naming._ | ||
import javax.naming.directory.SearchControls | ||
import play.api.{Configuration, Logger} | ||
|
||
import scala.util.control.NonFatal | ||
|
||
class LDAPAuthService @Inject()(globalConfig: Configuration) extends AuthService { | ||
|
||
private val log = org.slf4j.LoggerFactory.getLogger(classOf[LDAPAuthService]) | ||
private val log = Logger(this.getClass) | ||
|
||
private final val config = new LDAPAuthConfig(globalConfig.get[Configuration]("auth.settings")) | ||
|
||
def auth(username: String, password: String): Option[String] = { | ||
val env = new Hashtable[String, String](11) | ||
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory") | ||
env.put(Context.PROVIDER_URL, s"${config.url}/${config.baseDN}") | ||
env.put(Context.SECURITY_AUTHENTICATION, config.method) | ||
|
||
if (username.contains("@")) { | ||
env.put(Context.SECURITY_PRINCIPAL, username) | ||
} else if (!config.domain.isEmpty()){ | ||
env.put(Context.SECURITY_PRINCIPAL, s"$username@${config.domain}") | ||
} else { | ||
env.put(Context.SECURITY_PRINCIPAL, config.userformat.format(username,config.baseDN)) | ||
} | ||
log.debug(s"Logging into LDAP with user ${env.get(Context.SECURITY_PRINCIPAL)}") | ||
env.put(Context.SECURITY_CREDENTIALS, password) | ||
def checkUserAuth(username: String, password: String): Boolean = { | ||
val props = new Hashtable[String, String]() | ||
props.put(Context.SECURITY_PRINCIPAL, config.userTemplate.format(username, config.baseDN)) | ||
props.put(Context.SECURITY_CREDENTIALS, password) | ||
|
||
try { | ||
val ctx = new InitialDirContext(env) | ||
ctx.close() | ||
Some(username) | ||
LdapCtxFactory.getLdapCtxInstance(config.url, props) | ||
true | ||
} catch { | ||
case ex: AuthenticationException => | ||
log.info(s"login of $username failed with: ${ex.getMessage}") | ||
None | ||
case e: AuthenticationException => | ||
log.info(s"login of $username failed with: ${e.getMessage}") | ||
false | ||
case NonFatal(e) => | ||
log.error(s"login of $username failed", e) | ||
None | ||
false | ||
} | ||
} | ||
|
||
def checkGroupMembership(username: String, groupConfig: LDAPGroupSearchConfig): Boolean = { | ||
val props = new Hashtable[String, String]() | ||
props.put(Context.SECURITY_PRINCIPAL, config.bindDN) | ||
props.put(Context.SECURITY_CREDENTIALS, config.bindPwd) | ||
props.put(Context.REFERRAL, "follow") | ||
val user = config.userTemplate.format(username, config.baseDN) | ||
val controls = new SearchControls() | ||
controls.setSearchScope(SearchControls.SUBTREE_SCOPE) | ||
try { | ||
val context = LdapCtxFactory.getLdapCtxInstance(config.url, props) | ||
val search = context.search(groupConfig.baseDN,s"(& (${groupConfig.userAttr}=$user)(${groupConfig.group}))", controls) | ||
context.close() | ||
search.hasMore() | ||
} catch { | ||
case e: AuthenticationException => | ||
log.info(s"User $username doesn't fulfill condition (${groupConfig.group}) : ${e.getMessage}") | ||
false | ||
case NonFatal(e) => | ||
log.error(s"Unexpected error while checking group membership of $username", e) | ||
false | ||
} | ||
} | ||
|
||
def auth(username: String, password: String): Option[String] = { | ||
val isValidUser = config.groupMembership match { | ||
case Some(groupConfig) => checkGroupMembership(username, groupConfig) && checkUserAuth(username, password) | ||
case None => checkUserAuth(username, password) | ||
} | ||
if (isValidUser) Some(username) else None | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,36 +23,39 @@ es = { | |
|
||
# Authentication | ||
auth = { | ||
# Example of LDAP authentication | ||
#type: ldap | ||
#settings: { | ||
#url = "ldap://host:port" | ||
#base-dn = "ou=active,ou=Employee" | ||
# OpenLDAP might be something like | ||
#base-dn = "ou=People,dc=domain,dc=com" | ||
# Usually method should be left as simple | ||
# Otherwise, set it to the SASL mechanisms to try | ||
#method = "simple" | ||
# Usernames in the form of email addresses (containing @) are passed through unchanged | ||
# Set user-domain to append @user-domain to bare usernames | ||
#user-domain = "domain.com" | ||
# Or leave empty to use user-format formatting | ||
#user-domain = "" | ||
# user-format executes a string.format() operation where | ||
# username is passed in first, followed by base-dn | ||
# Leave username unchanged | ||
#user-format = "%s" | ||
# Like setting user-domain | ||
#user-format = "%[email protected]" | ||
# Common for OpenLDAP | ||
#user-format = "uid=%s,%s" | ||
#} | ||
# Example of simple username/password authentication | ||
#type: basic | ||
#settings: { | ||
#username = "admin" | ||
#password = "1234" | ||
#} | ||
# either basic or ldap | ||
type: ${?AUTH_TYPE} | ||
settings { | ||
# LDAP | ||
url = ${?LDAP_URL} | ||
# OpenLDAP might be something like "ou=People,dc=domain,dc=com" | ||
base-dn = ${?LDAP_BASE_DN} | ||
# Usually method should be "simple" otherwise, set it to the SASL mechanisms to try | ||
method = ${?LDAP_METHOD} | ||
# user-template executes a string.format() operation where | ||
# username is passed in first, followed by base-dn. Some examples | ||
# - %s => leave user untouched | ||
# - %[email protected] => append "@domain.com" to username | ||
# - uid=%s,%s => usual case of OpenLDAP | ||
user-template = ${?LDAP_USER_TEMPLATE} | ||
// User identifier that can perform searches | ||
bind-dn = ${?LDAP_BIND_DN} | ||
bind-pw = ${?LDAP_BIND_PWD} | ||
group-search { | ||
// If left unset parent's base-dn will be used | ||
base-dn = ${?LDAP_GROUP_BASE_DN} | ||
// Attribute that represent the user, for example uid or mail | ||
user-attr = ${?LDAP_USER_ATTR} | ||
// Filter that tests membership of the group. If this property is empty then there is no group membership check | ||
// AD example => memberOf=CN=mygroup,ou=ouofthegroup,DC=domain,DC=com | ||
// OpenLDAP example => CN=mygroup | ||
group = ${?LDAP_GROUP} | ||
} | ||
|
||
# Basic auth | ||
username = ${?BASIC_AUTH_USER} | ||
password = ${?BASIC_AUTH_PWD} | ||
} | ||
} | ||
|
||
# A list of known hosts | ||
|