Skip to content

Commit

Permalink
Merge pull request #307 from lmenezes/ldap
Browse files Browse the repository at this point in the history
Add LDAP group search
  • Loading branch information
moliware authored Apr 2, 2019
2 parents 1a974c5 + cb4e7f9 commit bc50c3e
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 58 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 16 additions & 2 deletions app/controllers/auth/ldap/LDAPAuthConfig.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,24 @@ class LDAPAuthConfig(config: Configuration) extends AuthConfig {

implicit val conf = config

final val domain = getSetting("user-domain")
final val userformat = getSetting("user-format")
final val userTemplate = getSetting("user-template")
final val method = getSetting("method")
final val url = getSetting("url")
final val baseDN = getSetting("base-dn")
final val bindDN = getSetting("bind-dn")
final val bindPwd = getSetting("bind-pw")


final val groupMembership: Option[LDAPGroupSearchConfig] = {
val groupAuthConfig = config.get[Configuration]("group-search")
groupAuthConfig.getOptional[String]("group").map { group =>
LDAPGroupSearchConfig(
groupAuthConfig.getOptional[String]("base-dn").getOrElse(baseDN),
getSetting("user-attr")(groupAuthConfig),
group
)
}
}
}

case class LDAPGroupSearchConfig(baseDN: String, userAttr: String, group: String)
72 changes: 46 additions & 26 deletions app/controllers/auth/ldap/LDAPAuthService.scala
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
}

}
63 changes: 33 additions & 30 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit bc50c3e

Please sign in to comment.