auth.ldap.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otoroshi_2.12 Show documentation
Show all versions of otoroshi_2.12 Show documentation
Lightweight api management on top of a modern http reverse proxy
The newest version!
package otoroshi.auth
import java.util
import akka.http.scaladsl.util.FastFuture
import com.google.common.base.Charsets
import org.apache.pulsar.client.api.PulsarClientException.AuthenticationException
import otoroshi.auth.LdapAuthModuleConfig.fromJson
import otoroshi.controllers.routes
import otoroshi.env.Env
import javax.naming.{CommunicationException, Context, ServiceUnavailableException}
import javax.naming.directory.{Attribute, InitialDirContext, SearchControls}
import otoroshi.models._
import otoroshi.models.{TeamAccess, TenantAccess, UserRight, UserRights}
import play.api.Logger
import play.api.libs.json.{JsArray, JsObject, _}
import play.api.mvc._
import otoroshi.security.{IdGenerator, OtoroshiClaim}
import otoroshi.utils.{JsonPathValidator, JsonValidator, RegexPool}
import otoroshi.utils.syntax.implicits._
import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.naming.ldap.{Control, InitialLdapContext}
import scala.annotation.tailrec
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success, Try}
case class LdapAuthUser(
name: String,
email: String,
metadata: JsObject = Json.obj(),
userRights: Option[UserRights],
ldapProfile: Option[JsValue],
adminEntityValidators: Map[String, Seq[JsonValidator]]
) {
def asJson: JsValue = LdapAuthUser.fmt.writes(this)
}
object LdapAuthUser {
def fmt =
new Format[LdapAuthUser] {
override def writes(o: LdapAuthUser) =
Json.obj(
"name" -> o.name,
"email" -> o.email,
"metadata" -> o.metadata,
"ldapProfile" -> o.ldapProfile.getOrElse(JsNull).as[JsValue],
"userRights" -> o.userRights.map(UserRights.format.writes),
"adminEntityValidators" -> o.adminEntityValidators.mapValues(v => JsArray(v.map(_.json)))
)
override def reads(json: JsValue) =
Try {
JsSuccess(
LdapAuthUser(
name = (json \ "name").as[String],
email = (json \ "email").as[String],
ldapProfile = (json \ "ldapProfile").asOpt[JsObject],
metadata = (json \ "metadata").asOpt[JsObject].getOrElse(Json.obj()),
userRights = (json \ "userRights").asOpt[UserRights](UserRights.format),
adminEntityValidators = json
.select("adminEntityValidators")
.asOpt[JsObject]
.map { obj =>
obj.value.mapValues { arr =>
arr.asArray.value
.map { item =>
JsonValidator.format.reads(item)
}
.collect { case JsSuccess(v, _) =>
v
}
}.toMap
}
.getOrElse(Map.empty[String, Seq[JsonValidator]])
)
)
} recover { case e =>
JsError(e.getMessage)
} get
}
}
object LdapAuthModuleConfig extends FromJson[AuthModuleConfig] {
lazy val logger = Logger("otoroshi-ldap-auth-config")
def fromJsons(value: JsValue): LdapAuthModuleConfig =
try {
_fmt.reads(value).get
} catch {
case e: Throwable => {
logger.error(s"Try to deserialize ${Json.prettyPrint(value)}")
throw e
}
}
val _fmt = new Format[LdapAuthModuleConfig] {
override def reads(json: JsValue) =
fromJson(json) match {
case Left(e) => JsError(e.getMessage)
case Right(v) => JsSuccess(v.asInstanceOf[LdapAuthModuleConfig])
}
override def writes(o: LdapAuthModuleConfig) = o.asJson
}
override def fromJson(json: JsValue): Either[Throwable, LdapAuthModuleConfig] =
Try {
val location = otoroshi.models.EntityLocation.readFromKey(json)
Right(
LdapAuthModuleConfig(
location = location,
id = (json \ "id").as[String],
name = (json \ "name").as[String],
desc = (json \ "desc").asOpt[String].getOrElse("--"),
sessionMaxAge = (json \ "sessionMaxAge").asOpt[Int].getOrElse(86400),
clientSideSessionEnabled = (json \ "clientSideSessionEnabled").asOpt[Boolean].getOrElse(true),
basicAuth = (json \ "basicAuth").asOpt[Boolean].getOrElse(false),
allowEmptyPassword = (json \ "allowEmptyPassword").asOpt[Boolean].getOrElse(false),
serverUrls = (json \ "serverUrl").asOpt[String] match {
case Some(url) => Seq(url)
case None => (json \ "serverUrls").asOpt[Seq[String]].getOrElse(Seq.empty[String])
},
searchBase = (json \ "searchBase").as[String],
userBase = (json \ "userBase").asOpt[String].filterNot(_.trim.isEmpty),
groupFilters = (json \ "groupFilter").asOpt[String] match {
case Some(filter) =>
location.teams.map(t => GroupFilter(filter, TenantAccess(location.tenant.value), t.value))
case None =>
(json \ "groupFilters")
.asOpt[Seq[GroupFilter]](Reads.seq(GroupFilter._fmt))
.getOrElse(Seq.empty[GroupFilter])
},
allowedUsers = json.select("allowedUsers").asOpt[Seq[String]].getOrElse(Seq.empty),
deniedUsers = json.select("deniedUsers").asOpt[Seq[String]].getOrElse(Seq.empty),
searchFilter = (json \ "searchFilter").as[String],
adminUsername = (json \ "adminUsername").asOpt[String].filterNot(_.trim.isEmpty),
adminPassword = (json \ "adminPassword").asOpt[String].filterNot(_.trim.isEmpty),
nameField = (json \ "nameField").as[String],
emailField = (json \ "emailField").as[String],
metadataField = (json \ "metadataField").asOpt[String].filterNot(_.trim.isEmpty),
extraMetadata = (json \ "extraMetadata").asOpt[JsObject].getOrElse(Json.obj()),
metadata = (json \ "metadata").asOpt[Map[String, String]].getOrElse(Map.empty),
tags = (json \ "tags").asOpt[Seq[String]].getOrElse(Seq.empty[String]),
sessionCookieValues =
(json \ "sessionCookieValues").asOpt(SessionCookieValues.fmt).getOrElse(SessionCookieValues()),
superAdmins = (json \ "superAdmins").asOpt[Boolean].getOrElse(false), // for backward compatibility reasons
extractProfile = (json \ "extractProfile").asOpt[Boolean].getOrElse(false),
extractProfileFilter = (json \ "extractProfileFilter").asOpt[Seq[String]].getOrElse(Seq.empty),
extractProfileFilterNot = (json \ "extractProfileFilterNot").asOpt[Seq[String]].getOrElse(Seq.empty),
rightsOverride = (json \ "rightsOverride")
.asOpt[Map[String, JsArray]]
.map(_.mapValues(UserRights.readFromArray))
.getOrElse(Map.empty),
dataOverride = (json \ "dataOverride").asOpt[Map[String, JsObject]].getOrElse(Map.empty),
groupRights = (json \ "groupRights")
.asOpt[Map[String, JsObject]]
.map(_.mapValues(GroupRights.reads).collect { case (key, Some(v)) =>
(key, v)
})
.getOrElse(Map.empty),
userValidators = (json \ "userValidators")
.asOpt[Seq[JsValue]]
.map(_.flatMap(v => JsonPathValidator.format.reads(v).asOpt))
.getOrElse(Seq.empty),
remoteValidators = (json \ "remoteValidators")
.asOpt[Seq[JsValue]]
.map(_.flatMap(v => RemoteUserValidatorSettings.format.reads(v).asOpt))
.getOrElse(Seq.empty),
adminEntityValidatorsOverride = json
.select("adminEntityValidatorsOverride")
.asOpt[JsObject]
.map { o =>
o.value.mapValues { obj =>
obj.asObject.value.mapValues { arr =>
arr.asArray.value
.map { item =>
JsonValidator.format.reads(item)
}
.collect { case JsSuccess(v, _) =>
v
}
}.toMap
}.toMap
}
.getOrElse(Map.empty[String, Map[String, Seq[JsonValidator]]])
)
)
} recover { case e =>
e.printStackTrace()
Left(e)
} get
}
case class GroupRights(userRights: UserRights, users: Seq[String])
object GroupRights {
def _fmt = new Format[GroupRights] {
override def writes(o: GroupRights) =
Json.obj(
"rights" -> o.userRights.json,
"users" -> o.users
)
override def reads(json: JsValue): JsResult[GroupRights] =
Try {
JsSuccess(
GroupRights(
userRights = (json \ "rights").asOpt[UserRights](UserRights.format).getOrElse(UserRights(Seq.empty)),
users = (json \ "users").asOpt[Seq[String]].getOrElse(Seq.empty[String])
)
)
} recover { case e =>
JsError(e.getMessage)
} get
}
def reads(json: JsObject): Option[GroupRights] =
this._fmt.reads(json).asOpt
}
case class GroupFilter(group: String, tenant: TenantAccess, team: String)
object GroupFilter {
def _fmt = new Format[GroupFilter] {
override def writes(o: GroupFilter) =
Json.obj(
"group" -> o.group,
"team" -> o.team,
"tenant" -> o.tenant.raw
)
override def reads(json: JsValue) =
Try {
JsSuccess(
GroupFilter(
group = (json \ "group").asOpt[String].getOrElse(""),
tenant = (json \ "tenant").asOpt[String].map(TenantAccess(_)).getOrElse(TenantAccess("*:rw")),
team = (json \ "team").asOpt[String].getOrElse("")
)
)
} recover { case e =>
JsError(e.getMessage)
} get
}
}
case class LdapAuthModuleConfig(
id: String,
name: String,
desc: String,
sessionMaxAge: Int = 86400,
clientSideSessionEnabled: Boolean,
userValidators: Seq[JsonPathValidator] = Seq.empty,
remoteValidators: Seq[RemoteUserValidatorSettings] = Seq.empty,
basicAuth: Boolean = false,
allowEmptyPassword: Boolean = false,
serverUrls: Seq[String] = Seq.empty,
searchBase: String,
userBase: Option[String] = None,
groupFilters: Seq[GroupFilter] = Seq.empty,
searchFilter: String = "(mail=${username})",
adminUsername: Option[String] = None,
adminPassword: Option[String] = None,
nameField: String = "cn",
emailField: String = "mail",
metadataField: Option[String] = None,
extraMetadata: JsObject = Json.obj(),
tags: Seq[String],
metadata: Map[String, String],
sessionCookieValues: SessionCookieValues,
location: otoroshi.models.EntityLocation = otoroshi.models.EntityLocation(),
superAdmins: Boolean = false,
extractProfile: Boolean = false,
extractProfileFilter: Seq[String] = Seq.empty,
extractProfileFilterNot: Seq[String] = Seq.empty,
rightsOverride: Map[String, UserRights] = Map.empty,
dataOverride: Map[String, JsObject] = Map.empty,
adminEntityValidatorsOverride: Map[String, Map[String, Seq[JsonValidator]]] = Map.empty,
groupRights: Map[String, GroupRights] = Map.empty,
allowedUsers: Seq[String] = Seq.empty,
deniedUsers: Seq[String] = Seq.empty
) extends AuthModuleConfig {
def `type`: String = "ldap"
def humanName: String = "Ldap auth. provider"
def theDescription: String = desc
def theMetadata: Map[String, String] = metadata
def theName: String = name
def theTags: Seq[String] = tags
override def form: Option[Form] = None
override def authModule(config: GlobalConfig): AuthModule = LdapAuthModule(this)
override def withLocation(location: EntityLocation): AuthModuleConfig = copy(location = location)
override def _fmt()(implicit env: Env): Format[AuthModuleConfig] = AuthModuleConfig._fmt(env)
override def asJson =
location.jsonWithKey ++ Json.obj(
"type" -> "ldap",
"id" -> id,
"name" -> name,
"desc" -> desc,
"basicAuth" -> basicAuth,
"allowEmptyPassword" -> allowEmptyPassword,
"clientSideSessionEnabled" -> clientSideSessionEnabled,
"sessionMaxAge" -> sessionMaxAge,
"userValidators" -> JsArray(userValidators.map(_.json)),
"remoteValidators" -> JsArray(remoteValidators.map(_.json)),
"serverUrls" -> serverUrls,
"searchBase" -> searchBase,
"userBase" -> userBase.map(JsString.apply).getOrElse(JsNull).as[JsValue],
"groupFilters" -> JsArray(groupFilters.map(o => GroupFilter._fmt.writes(o))),
"searchFilter" -> searchFilter,
"adminUsername" -> adminUsername.map(JsString.apply).getOrElse(JsNull).as[JsValue],
"adminPassword" -> adminPassword.map(JsString.apply).getOrElse(JsNull).as[JsValue],
"nameField" -> nameField,
"emailField" -> emailField,
"metadataField" -> metadataField.map(JsString.apply).getOrElse(JsNull).as[JsValue],
"extraMetadata" -> extraMetadata,
"metadata" -> metadata,
"tags" -> JsArray(tags.map(JsString.apply)),
"sessionCookieValues" -> SessionCookieValues.fmt.writes(this.sessionCookieValues),
"superAdmins" -> superAdmins,
"extractProfile" -> extractProfile,
"extractProfileFilter" -> extractProfileFilter,
"extractProfileFilterNot" -> extractProfileFilterNot,
"rightsOverride" -> JsObject(rightsOverride.mapValues(_.json)),
"dataOverride" -> JsObject(dataOverride),
"allowedUsers" -> allowedUsers,
"deniedUsers" -> deniedUsers,
"groupRights" -> JsObject(groupRights.mapValues(GroupRights._fmt.writes)),
"adminEntityValidatorsOverride" -> JsObject(adminEntityValidatorsOverride.mapValues { o =>
JsObject(o.mapValues(v => JsArray(v.map(_.json))))
})
)
def save()(implicit ec: ExecutionContext, env: Env): Future[Boolean] = env.datastores.authConfigsDataStore.set(this)
override def cookieSuffix(desc: ServiceDescriptor) = s"ldap-auth-$id"
private def getLdapContext(principal: String, password: String, url: String): util.Hashtable[String, AnyRef] = {
val env = new util.Hashtable[String, AnyRef]
env.put(Context.SECURITY_AUTHENTICATION, "simple")
env.put(Context.SECURITY_PRINCIPAL, principal)
env.put(Context.SECURITY_CREDENTIALS, password)
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
env.put(Context.PROVIDER_URL, url)
env
}
private def getInitialLdapContext(principal: String, password: String, url: String) = {
new InitialLdapContext(getLdapContext(principal, password, url), Array.empty[Control])
}
private def getInitialDirContext(principal: String, password: String, url: String) =
new InitialDirContext(getLdapContext(principal, password, url))
/*
val ldapAdServer = "ldap://ldap.forumsys.com:389"
val ldapSearchBase = "dc=example,dc=com"
val searchFilter = "(uid=${username})"
val ldapUsername = "cn=read-only-admin,dc=example,dc=com"
val ldapPassword = "password"
val nameField = "cn"
val emailField = "mail"
*/
def bindUser(username: String, password: String): Either[String, LdapAuthUser] = {
if (!allowEmptyPassword && password.trim.isEmpty) {
LdapAuthModuleConfig.logger.error("Empty user password are not allowed for this LDAP auth. module")
Left("Empty user password are not allowed for this LDAP auth. module")
} else if (!allowEmptyPassword && adminPassword.exists(_.trim.isEmpty)) {
LdapAuthModuleConfig.logger.error("Empty admin password are not allowed for this LDAP auth. module")
Left("Empty admin password are not allowed for this LDAP auth. module")
} else
_bindUser(serverUrls.filter(_ => true), username, password)
}
private def getDefaultSearchControls() = {
val searchControls = new SearchControls()
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE)
searchControls
}
private def _bindUser(urls: Seq[String], username: String, password: String): Either[String, LdapAuthUser] = {
import javax.naming._
import collection.JavaConverters._
if (urls.isEmpty)
Left(s"Missing LDAP server URLs or all down")
else {
val url = urls.head
try {
val ctx = getInitialLdapContext(
adminUsername.map(u => u).getOrElse(""),
adminPassword.map(p => p).getOrElse(""),
url
)
if (LdapAuthModuleConfig.logger.isDebugEnabled) LdapAuthModuleConfig.logger.debug(s"bind user for $username")
// GROUP TENANT LIST[TEAM] LIST[USER]
val usersInGroup: Map[((String, TenantAccess), Seq[String]), Seq[String]] = groupFilters
.groupBy(record => (record.group, record.tenant))
.map { group => (group._1, group._2.map(_.team)) }
.map { filter =>
if (LdapAuthModuleConfig.logger.isDebugEnabled)
LdapAuthModuleConfig.logger.debug(s"searching `$searchBase` with filter `${filter._1._1}` ")
val groupSearch = ctx.search(searchBase, filter._1._1, getDefaultSearchControls())
val uids: Seq[String] = if (groupSearch.hasMore) {
val item = groupSearch.next()
val attrs = item.getAttributes
attrs.getAll.asScala.toSeq
.filter(a => a.getID == "uniqueMember" || a.getID == "member" || a.getID == "memberUid")
.flatMap { attr =>
if (attr.getID == "memberUid") {
attr.getAll.asScala.toSeq
.map(a => s"uid=${a.toString},${userBase.map(ub => s"${ub},").getOrElse("")}${searchBase}")
} else {
attr.getAll.asScala.toSeq.map(_.toString)
}
}
} else {
Seq.empty[String]
}
groupSearch.close()
(filter, uids)
}
if (LdapAuthModuleConfig.logger.isDebugEnabled)
LdapAuthModuleConfig.logger.debug(
s"found ${usersInGroup.flatMap(_._2).size} users in group : ${usersInGroup.mkString(", ")}"
)
if (LdapAuthModuleConfig.logger.isDebugEnabled)
LdapAuthModuleConfig.logger.debug(
s"searching user in ${userBase.map(_ + ",").getOrElse("") + searchBase} with filter ${searchFilter.replace("${username}", username)}"
)
val res = ctx.search(
userBase.map(_ + ",").getOrElse("") + searchBase,
searchFilter.replace("${username}", username),
getDefaultSearchControls()
)
val boundUser: Either[String, LdapAuthUser] = if (res.hasMore) {
val item = res.next()
val dn = item.getNameInNamespace
if (LdapAuthModuleConfig.logger.isDebugEnabled) LdapAuthModuleConfig.logger.debug(s"found user with dn `$dn`")
val userGroup = usersInGroup
.find(group => group._2.exists(g => g.contains(dn)))
if (groupFilters.isEmpty) {
if (LdapAuthModuleConfig.logger.isDebugEnabled)
LdapAuthModuleConfig.logger.debug(s"none groups defined - user found anyway")
val attrs = item.getAttributes
try {
val ctx2 = getInitialDirContext(dn, password, url)
ctx2.close()
val email = attrs.get(emailField).toString.split(":").last.trim
val metadata = extraMetadata.deepMerge(
metadataField
.map(m => Json.parse(attrs.get(m).toString.split(":").last.trim).as[JsObject])
.getOrElse(Json.obj())
)
val profile: Option[JsValue] = if (extractProfile) {
val all = attrs.getAll
var jsonAttrs = Json.obj()
val regexes = extractProfileFilter.map(f => RegexPool.regex(f))
while (all.hasMore) {
val next: Attribute = all.next()
val name = next.getID
if (regexes.isEmpty || regexes.exists(_.matches(name))) {
val value = if (next.size() > 1) {
JsArray((0 until next.size()).map(idx => JsString(next.get(idx).toString)).toSeq)
} else if (next.size() == 1) {
JsString(next.get(0).toString)
} else {
JsNull
}
jsonAttrs = jsonAttrs ++ Json.obj(name -> value)
}
}
Some(jsonAttrs)
} else {
None
}
Right(
LdapAuthUser(
name = attrs.get(nameField).toString.split(":").last.trim,
email,
metadata = dataOverride
.get(email)
.map(v => metadata.deepMerge(v))
.getOrElse(metadata),
ldapProfile = profile,
adminEntityValidators = adminEntityValidatorsOverride.getOrElse(email, Map.empty),
userRights = Some(
UserRights(
UserRights.default.rights
++
groupRights.values
.filter { group => group.users.contains(email) }
.flatMap { group => group.userRights.rights }
.toList
.groupBy(f => f.tenant)
.map(m => UserRight(m._1, m._2.flatMap(_.teams)))
.toSeq
)
)
)
)
} catch {
case _: ServiceUnavailableException | _: CommunicationException => Left(s"Communication error")
case e: Throwable =>
if (LdapAuthModuleConfig.logger.isDebugEnabled) LdapAuthModuleConfig.logger.debug(s"bind failed", e)
Left(s"bind failed ${e.getMessage}")
}
} else if (userGroup.isDefined) {
val group = userGroup.get
if (LdapAuthModuleConfig.logger.isDebugEnabled)
LdapAuthModuleConfig.logger.debug(s"user found in ${group._1} group")
val attrs = item.getAttributes
try {
val ctx2 = getInitialDirContext(dn, password, url)
ctx2.close()
val email = attrs.get(emailField).toString.split(":").last.trim
val profile: Option[JsValue] = if (extractProfile) {
val all = attrs.getAll
var jsonAttrs = Json.obj()
val regexes = extractProfileFilter.map(f => RegexPool.regex(f))
val regexesNot = extractProfileFilterNot.map(f => RegexPool.regex(f))
while (all.hasMore) {
val next: Attribute = all.next()
val name = next.getID
if (
regexes.isEmpty || (regexes.exists(_.matches(name))) && regexesNot.forall(rx => !rx.matches(name))
) {
val value = if (next.size() > 1) {
JsArray((0 until next.size()).map(idx => JsString(next.get(idx).toString)).toSeq)
} else if (next.size() == 1) {
JsString(next.get(0).toString)
} else {
JsNull
}
jsonAttrs = jsonAttrs ++ Json.obj(name -> value)
}
}
Some(jsonAttrs)
} else {
None
}
Right(
LdapAuthUser(
name = attrs.get(nameField).toString.split(":").last.trim,
email,
metadata = extraMetadata.deepMerge(
metadataField
.map(m => Json.parse(attrs.get(m).toString.split(":").last.trim).as[JsObject])
.getOrElse(Json.obj())
),
ldapProfile = profile,
adminEntityValidators = adminEntityValidatorsOverride.getOrElse(email, Map.empty),
userRights = Some(
UserRights(
(
usersInGroup
.filter { group => group._2.exists(g => g.contains(dn)) }
.map(userGroup =>
UserRight(
userGroup._1._1._2,
userGroup._1._2.map(team => TeamAccess(s"$team:${userGroup._1._1._2.raw.split(":")(1)}"))
)
)
.toList
++
groupRights.values
.filter { group => group.users.contains(email) }
.flatMap { group => group.userRights.rights }
.toList
)
.groupBy(f => f.tenant)
.map(m => UserRight(m._1, m._2.flatMap(_.teams)))
.toSeq
)
)
)
)
} catch {
case _: ServiceUnavailableException | _: CommunicationException => Left(s"Communication error")
case e: Throwable =>
if (LdapAuthModuleConfig.logger.isDebugEnabled) LdapAuthModuleConfig.logger.debug(s"bind failed", e)
Left(s"bind failed ${e.getMessage}")
}
} else {
if (LdapAuthModuleConfig.logger.isDebugEnabled)
LdapAuthModuleConfig.logger.debug(s"user not found in groups")
Left(s"user not found in group")
}
} else {
if (LdapAuthModuleConfig.logger.isDebugEnabled) LdapAuthModuleConfig.logger.debug(s"no user found")
Left(s"no user found")
}
res.close()
ctx.close()
boundUser
} catch {
case _: CommunicationException | _: ServiceUnavailableException =>
_bindUser(urls.tail, username, password)
case e: Throwable =>
if (LdapAuthModuleConfig.logger.isDebugEnabled)
LdapAuthModuleConfig.logger.debug(s"error on LDAP searching method", e)
Left(s"error on LDAP searching method ${e.getMessage}")
}
}
}
def checkConnection(): Future[(Boolean, String)] = {
val env = new util.Hashtable[String, AnyRef]
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory")
env.put(Context.SECURITY_AUTHENTICATION, "simple")
adminUsername.foreach(u => env.put(Context.SECURITY_PRINCIPAL, u))
adminPassword.foreach(p => env.put(Context.SECURITY_CREDENTIALS, p))
try {
for (url <- serverUrls) {
env.put(Context.PROVIDER_URL, url)
scala.util.Try {
val ctx2 = new InitialDirContext(env)
ctx2.close()
} match {
case Success(_) => return FastFuture.successful((true, "--"))
case Failure(_: ServiceUnavailableException | _: CommunicationException) =>
case Failure(e) => throw e
}
}
FastFuture.successful((false, "Missing LDAP server URLs or all down"))
} catch {
case e: Exception => FastFuture.successful((false, e.getMessage))
}
}
}
object LdapAuthModule {
def defaultConfig = LdapAuthModuleConfig(
id = IdGenerator.namedId("auth_mod", IdGenerator.uuid),
name = "New auth. module",
desc = "New auth. module",
serverUrls = Seq("ldap://ldap.forumsys.com:389"),
searchBase = "dc=example,dc=com",
searchFilter = "(uid=${username})",
adminUsername = Some("cn=read-only-admin,dc=example,dc=com"),
adminPassword = Some("password"),
tags = Seq.empty,
metadata = Map.empty,
sessionCookieValues = SessionCookieValues(),
clientSideSessionEnabled = true
)
}
case class LdapAuthModule(authConfig: LdapAuthModuleConfig) extends AuthModule {
import otoroshi.utils.future.Implicits._
def this() = this(LdapAuthModule.defaultConfig)
def decodeBase64(encoded: String): String = new String(OtoroshiClaim.decoder.decode(encoded), Charsets.UTF_8)
def extractUsernamePassword(header: String): Option[(String, String)] = {
val base64 = header.replace("Basic ", "").replace("basic ", "")
Option(base64)
.map(decodeBase64)
.map(_.split(":").toSeq)
.filter(v => v.nonEmpty && v.length > 1)
.flatMap(a => a.headOption.map(head => (head, a.tail.mkString(":"))))
}
def bindUser(username: String, password: String, descriptor: ServiceDescriptor)(implicit
env: Env,
ec: ExecutionContext
): Future[Either[ErrorReason, PrivateAppsUser]] = {
authConfig.bindUser(username, password).toOption match {
case Some(user) =>
PrivateAppsUser(
randomId = IdGenerator.token(64),
name = user.name,
email = user.email,
profile = user.asJson,
realm = authConfig.cookieSuffix(descriptor),
// otoroshiData = authConfig.dataOverride.get(user.email).map(v => authConfig.extraMetadata.deepMerge(v)).orElse(Some(user.metadata)),
otoroshiData = authConfig.dataOverride
.get(user.email)
.map(v => authConfig.extraMetadata.deepMerge(v))
.orElse(Some(authConfig.extraMetadata.deepMerge(user.metadata))),
authConfigId = authConfig.id,
tags = Seq.empty,
metadata = Map.empty,
location = authConfig.location
).validate(descriptor, isRoute = true, authConfig)
case None => Left(ErrorReason(s"You're not authorized here")).vfuture
}
}
private def userRightContainsTenant(userRights: UserRights): Boolean =
userRights.rights.exists(f => f.tenant.containsWildcard || f.tenant.value.equals(authConfig.location.tenant.value))
private def hasOverrideRightsForEmailAndTenant(email: String): Option[UserRight] =
authConfig.rightsOverride
.get(email)
.flatMap(_.rights.find(p => p.tenant.value.equals(authConfig.location.tenant.value)))
def bindAdminUser(username: String, password: String)(implicit
env: Env,
ec: ExecutionContext
): Future[Either[ErrorReason, BackOfficeUser]] = {
authConfig.bindUser(username, password).toOption match {
case Some(user) =>
BackOfficeUser(
randomId = IdGenerator.token(64),
name = user.name,
email = user.email,
profile = user.asJson,
simpleLogin = false,
authConfigId = authConfig.id,
tags = Seq.empty,
metadata = Map.empty,
rights =
if (authConfig.superAdmins) UserRights.superAdmin
else {
user.userRights match {
case Some(userRight) if userRightContainsTenant(userRight) =>
hasOverrideRightsForEmailAndTenant(user.email) match {
case Some(rightOverride) => UserRights(Seq(rightOverride))
case None =>
UserRights(
Seq(
userRight.rights
.find(f =>
f.tenant.containsWildcard ||
f.tenant.value.equals(authConfig.location.tenant.value)
)
.get
)
)
}
case None =>
authConfig.rightsOverride.getOrElse(
user.email,
UserRights(
Seq(
UserRight(
TenantAccess(authConfig.location.tenant.value),
authConfig.location.teams.map(t => TeamAccess(t.value))
)
)
)
)
}
},
location = authConfig.location,
adminEntityValidators = user.adminEntityValidators
).validate(
env.backOfficeServiceDescriptor,
isRoute = false,
authConfig
)
case None => Left(ErrorReason(s"You're not authorized here")).vfuture
}
}
override def paLoginPage(
request: RequestHeader,
config: GlobalConfig,
descriptor: ServiceDescriptor,
isRoute: Boolean
)(implicit
ec: ExecutionContext,
env: Env
): Future[Result] = {
implicit val req = request
val redirect = request
.getQueryString("redirect")
.filter(redirect =>
request.getQueryString("hash").contains(env.sign(s"desc=${descriptor.id}&redirect=${redirect}"))
)
.map(redirectBase64Encoded =>
new String(Base64.getUrlDecoder.decode(redirectBase64Encoded), StandardCharsets.UTF_8)
)
val hash = env.sign(s"${authConfig.id}:::${descriptor.id}")
env.datastores.authConfigsDataStore.generateLoginToken().flatMap { token =>
if (authConfig.basicAuth) {
def unauthorized() =
Results
.Unauthorized(otoroshi.views.html.oto.error("You are not authorized here", env))
.withHeaders("WWW-Authenticate" -> s"""Basic realm="${authConfig.cookieSuffix(descriptor)}"""")
.addingToSession(
s"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}" -> redirect.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
)
.vfuture
req.headers.get("Authorization") match {
case Some(auth) if auth.startsWith("Basic ") =>
extractUsernamePassword(auth) match {
case None => Results.Forbidden(otoroshi.views.html.oto.error("Forbidden access", env)).vfuture
case Some((username, password)) =>
bindUser(username, password, descriptor) flatMap {
case Left(_) => Results.Forbidden(otoroshi.views.html.oto.error("Forbidden access", env)).vfuture
case Right(user) =>
env.datastores.authConfigsDataStore.setUserForToken(token, user.toJson).map { _ =>
Results.Redirect(
s"/privateapps/generic/callback?route=$isRoute&desc=${descriptor.id}&token=$token&hash=$hash&ref=${authConfig.id}"
)
}
}
}
case _ => unauthorized()
}
} else {
Results
.Ok(
otoroshi.views.html.oto
.login(
s"/privateapps/generic/callback?route=${isRoute}&desc=${descriptor.id}&hash=$hash&ref=${authConfig.id}",
"POST",
token,
false,
env
)
)
.addingToSession(
s"pa-redirect-after-login-${authConfig.cookieSuffix(descriptor)}" -> redirect.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
)
.vfuture
}
}
}
override def paLogout(
request: RequestHeader,
user: Option[PrivateAppsUser],
config: GlobalConfig,
descriptor: ServiceDescriptor
)(implicit
ec: ExecutionContext,
env: Env
) = FastFuture.successful(Right(None))
override def paCallback(request: Request[AnyContent], config: GlobalConfig, descriptor: ServiceDescriptor)(implicit
ec: ExecutionContext,
env: Env
): Future[Either[ErrorReason, PrivateAppsUser]] = {
implicit val req = request
if (req.method == "GET" && authConfig.basicAuth) {
req.getQueryString("token") match {
case Some(token) =>
env.datastores.authConfigsDataStore
.getUserForToken(token)
.map(_.flatMap(a => PrivateAppsUser.fmt.reads(a).asOpt))
.flatMap {
case Some(user) =>
user.validate(
descriptor,
isRoute = true,
authConfig
)
case None => Left(ErrorReason("No user found")).vfuture
}
case _ => FastFuture.successful(Left(ErrorReason("Forbidden access")))
}
} else {
request.body.asFormUrlEncoded match {
case None => FastFuture.successful(Left(ErrorReason("No Authorization form here")))
case Some(form) => {
(form.get("username").map(_.last), form.get("password").map(_.last), form.get("token").map(_.last)) match {
case (Some(username), Some(password), Some(token)) => {
env.datastores.authConfigsDataStore.validateLoginToken(token).flatMap {
case false => Left(ErrorReason("Bad token")).vfuture
case true => bindUser(username, password, descriptor)
}
}
case _ => {
FastFuture.successful(Left(ErrorReason("Authorization form is not complete")))
}
}
}
}
}
}
override def boLoginPage(request: RequestHeader, config: GlobalConfig)(implicit
ec: ExecutionContext,
env: Env
): Future[Result] = {
implicit val req = request
val redirect = request.getQueryString("redirect")
val hash = env.sign(s"${authConfig.id}:::backoffice")
env.datastores.authConfigsDataStore.generateLoginToken().flatMap { token =>
if (authConfig.basicAuth) {
def unauthorized() =
Results
.Unauthorized(otoroshi.views.html.oto.error("You are not authorized here", env))
.withHeaders("WWW-Authenticate" -> "otoroshi-admin-realm")
.addingToSession(
"bo-redirect-after-login" -> redirect.getOrElse(
routes.PrivateAppsController.home.absoluteURL(env.exposedRootSchemeIsHttps)
)
)
.vfuture
req.headers.get("Authorization") match {
case Some(auth) if auth.startsWith("Basic ") =>
extractUsernamePassword(auth) match {
case None => Results.Forbidden(otoroshi.views.html.oto.error("Forbidden access", env)).vfuture
case Some((username, password)) =>
bindAdminUser(username, password) flatMap {
case Left(_) => Results.Forbidden(otoroshi.views.html.oto.error("Forbidden access", env)).vfuture
case Right(user) =>
env.datastores.authConfigsDataStore.setUserForToken(token, user.toJson).map { _ =>
Results.Redirect(s"/backoffice/auth0/callback?token=$token&hash=$hash")
}
}
}
case _ => unauthorized()
}
} else {
Results
.Ok(otoroshi.views.html.oto.login(s"/backoffice/auth0/callback?hash=$hash", "POST", token, false, env))
.addingToSession(
"bo-redirect-after-login" -> redirect.getOrElse(
routes.BackOfficeController.dashboard.absoluteURL(env.exposedRootSchemeIsHttps)
)
)
.vfuture
}
}
}
override def boLogout(request: RequestHeader, user: BackOfficeUser, config: GlobalConfig)(implicit
ec: ExecutionContext,
env: Env
) =
FastFuture.successful(Right(None))
override def boCallback(
request: Request[AnyContent],
config: GlobalConfig
)(implicit ec: ExecutionContext, env: Env): Future[Either[ErrorReason, BackOfficeUser]] = {
implicit val req = request
if (req.method == "GET" && authConfig.basicAuth) {
req.getQueryString("token") match {
case Some(token) =>
env.datastores.authConfigsDataStore
.getUserForToken(token)
.map(_.flatMap(a => BackOfficeUser.fmt.reads(a).asOpt))
.map {
case Some(user) => Right(user)
case None => Left(ErrorReason("No user found"))
}
case _ => FastFuture.successful(Left(ErrorReason("Forbidden access")))
}
} else {
request.body.asFormUrlEncoded match {
case None => FastFuture.successful(Left(ErrorReason("No Authorization form here")))
case Some(form) => {
(form.get("username").map(_.last), form.get("password").map(_.last), form.get("token").map(_.last)) match {
case (Some(username), Some(password), Some(token)) => {
env.datastores.authConfigsDataStore.validateLoginToken(token).flatMap {
case false => Left(ErrorReason("Bad token")).vfuture
case true => bindAdminUser(username, password)
}
}
case _ => {
FastFuture.successful(Left(ErrorReason("Authorization form is not complete")))
}
}
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy