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

Feature/ldap rules in new core #414

Merged
merged 22 commits into from Mar 11, 2019
Merged
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.
+4,167 −540
Diff settings

Always

Just for now

Copy path View file
@@ -25,7 +25,7 @@ fi

if [[ $TRAVIS != "true" ]] || [[ $ROR_TASK == "integration_es63x-experimental" ]]; then
echo ">>> es63x-experimental => Running testcontainers.."
./gradlew integration-tests:test '-PesModule=es63x-experimental' --tests "tech.beshu.ror.integration.other.*" || ( find . |grep hs_err |xargs cat && exit 1 )
./gradlew integration-tests:test '-PesModule=es63x-experimental' || ( find . |grep hs_err |xargs cat && exit 1 )
fi

if [[ $TRAVIS != "true" ]] || [[ $ROR_TASK == "integration_es63x" ]]; then
Copy path View file
@@ -84,6 +84,7 @@ dependencies {
shadowCompile group: 'com.jayway.jsonpath', name: 'json-path', version: '2.2.0'
simpleCompile group: 'com.softwaremill.sttp', name: 'async-http-client-backend-cats_2.12', version: '1.5.8'
simpleCompile group: 'com.softwaremill.sttp', name: 'core_2.12', version: '1.5.1'
simpleCompile group: 'com.unboundid', name: 'unboundid-ldapsdk', version: '4.0.9'
simpleCompile group: 'commons-codec', name: 'commons-codec', version: '1.10'
simpleCompile group: 'cz.seznam.euphoria', name: 'shaded-guava', version: '21.0'
simpleCompile group: 'eu.timepit', name: 'refined_2.12', version: '0.9.3'
@@ -102,14 +103,21 @@ dependencies {
exclude group: 'org.slf4j', module: 'slf4j-simple'
exclude group: 'dom4j', module: 'dom4j'
}
simpleCompile group: 'com.github.blemale', name: 'scaffeine_2.12', version: '2.5.0'
simpleCompile group: 'org.scala-lang', name: 'scala-library', version: '2.12.4'
simpleCompile group: 'org.scala-lang.modules', name: 'scala-java8-compat_2.12', version: '0.9.0'
simpleCompile group: 'io.lemonlabs', name: 'scala-uri_2.12', version: '1.4.1'
simpleCompile group: 'org.typelevel', name: 'squants_2.12', version: '1.4.0'

testCompile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.2'
testRuntime group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3'
testRuntime group: 'org.pegdown', name: 'pegdown', version: '1.4.2'
testCompile group: 'com.typesafe.scala-logging', name: 'scala-logging_2.12', version: '3.9.2'
testCompile group: 'org.scalamock', name: 'scalamock_2.12', version: '3.6.0'
testCompile group: 'org.scalamock', name: 'scalamock-scalatest-support_2.12', version: '3.6.0'
testCompile group: 'org.scalatest', name: 'scalatest_2.12', version: '3.0.5'
testCompile group: 'com.dimafeng', name: 'testcontainers-scala_2.12', version: '0.23.0'

}

license {
@@ -21,7 +21,6 @@ import cats.Order
import org.apache.logging.log4j.scala.Logging
import tech.beshu.ror.acl.blocks.rules._
import tech.beshu.ror.acl.orders._
import tech.beshu.ror.acl.show.logs._

class RuleOrdering extends Ordering[Rule] with Logging {

@@ -56,6 +55,8 @@ object RuleOrdering {
classOf[JwtAuthRule],
classOf[RorKbnAuthRule],
// then we could check potentially slow async rules
classOf[LdapAuthRule],
classOf[LdapAuthenticationRule],
classOf[ExternalAuthenticationRule],
classOf[GroupsRule],
// Inspection rules next; these act based on properties of the request.
@@ -75,6 +76,7 @@ object RuleOrdering {
classOf[ActionsRule],
classOf[UsersRule],
// all authorization rules should be placed before any authentication rule
classOf[LdapAuthorizationRule],
classOf[ExternalAuthorizationRule],
// At the end the sync rule chain are those that can mutate the client request.
classOf[KibanaHideAppsRule],
@@ -17,20 +17,21 @@
package tech.beshu.ror.acl.blocks.definitions

import java.nio.charset.Charset
import java.util.concurrent.TimeUnit

import cats.{Eq, Show}
import cats.implicits._
import com.softwaremill.sttp._
import cz.seznam.euphoria.shaded.guava.com.google.common.cache.{Cache, CacheBuilder}
import cz.seznam.euphoria.shaded.guava.com.google.common.hash.Hashing
import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Positive
import monix.eval.Task
import tech.beshu.ror.acl.blocks.definitions.CacheableExternalAuthenticationServiceDecorator.HashedUserCredentials
import tech.beshu.ror.acl.domain.{BasicAuth, Header, Secret, User}
import tech.beshu.ror.acl.blocks.definitions.ExternalAuthenticationService.Name
import tech.beshu.ror.acl.domain
import tech.beshu.ror.acl.factory.HttpClientsFactory.HttpClient
import tech.beshu.ror.acl.factory.decoders.definitions.Definitions.Item
import tech.beshu.ror.acl.utils.CacheableActionWithKeyMapping

import scala.concurrent.duration.FiniteDuration

@@ -77,36 +78,31 @@ class JwtExternalAuthenticationService(override val id: ExternalAuthenticationSe
}
}

class CachingExternalAuthenticationService(underlying: ExternalAuthenticationService, ttl: FiniteDuration Refined Positive)
class CacheableExternalAuthenticationServiceDecorator(underlying: ExternalAuthenticationService,
ttl: FiniteDuration Refined Positive)
extends ExternalAuthenticationService {

private val cache: Cache[String, String] =
CacheBuilder
.newBuilder
.expireAfterWrite(ttl.value.toMillis, TimeUnit.MILLISECONDS)
.build[String, String]
private val cacheableAuthentication =
new CacheableActionWithKeyMapping[(User.Id, domain.Secret), HashedUserCredentials, Boolean](ttl, authenticateAction, hashCredential)

override val id: ExternalAuthenticationService#Id = underlying.id

override def authenticate(user: User.Id, secret: Secret): Task[Boolean] = {
Option(cache.getIfPresent(user.value)) match {
case Some(cachedUserHashedPass) => Task {
cachedUserHashedPass === hashFrom(secret)
}
case None =>
underlying
.authenticate(user, secret)
.map { authenticated =>
if (authenticated) {
cache.put(user.value, hashFrom(secret))
}
authenticated
}
}
cacheableAuthentication.call((user, secret))
}

private def hashFrom(password: Secret) = {
Hashing.sha256.hashString(password.value, Charset.defaultCharset).toString
private def hashCredential(value: (User.Id, domain.Secret)) = {
val (user, secret) = value
HashedUserCredentials(user, Hashing.sha256.hashString(secret.value, Charset.defaultCharset).toString)
}

private def authenticateAction(value: (User.Id, domain.Secret)) = {
val (userId, secret) = value
underlying.authenticate(userId, secret)
}

}

object CacheableExternalAuthenticationServiceDecorator {
private[CacheableExternalAuthenticationServiceDecorator] final case class HashedUserCredentials(user: User.Id, hashedCredentials: String)
}
@@ -16,29 +16,28 @@
*/
package tech.beshu.ror.acl.blocks.definitions

import java.util.concurrent.TimeUnit

import cats.implicits._
import cats.{Eq, Show}
import com.jayway.jsonpath.JsonPath
import monix.eval.Task
import tech.beshu.ror.acl.domain._
import tech.beshu.ror.acl.show.logs._
import tech.beshu.ror.acl.blocks.definitions.ExternalAuthorizationService.Name
import com.softwaremill.sttp._
import cz.seznam.euphoria.shaded.guava.com.google.common.cache.{Cache, CacheBuilder}
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric.Positive
import eu.timepit.refined.types.string.NonEmptyString
import monix.eval.Task
import org.apache.logging.log4j.scala.Logging
import tech.beshu.ror.acl.blocks.definitions.ExternalAuthorizationService.Name
import tech.beshu.ror.acl.blocks.definitions.HttpExternalAuthorizationService.AuthTokenSendMethod.{UsingHeader, UsingQueryParam}
import tech.beshu.ror.acl.blocks.definitions.HttpExternalAuthorizationService._
import tech.beshu.ror.acl.domain._
import tech.beshu.ror.acl.factory.HttpClientsFactory.HttpClient
import tech.beshu.ror.acl.factory.decoders.definitions.Definitions.Item
import tech.beshu.ror.acl.show.logs._
import tech.beshu.ror.acl.utils.CacheableAction

import scala.collection.JavaConverters._
import scala.concurrent.duration.FiniteDuration
import scala.util.{Failure, Success, Try}
import eu.timepit.refined.auto._

trait ExternalAuthorizationService extends Item {
override type Id = Name
@@ -149,29 +148,14 @@ object HttpExternalAuthorizationService {
final case class InvalidResponse(message: String) extends Exception(message)
}

class CachingExternalAuthorizationService(underlying: ExternalAuthorizationService, ttl: FiniteDuration Refined Positive)
class CacheableExternalAuthorizationServiceDecorator(underlying: ExternalAuthorizationService,
ttl: FiniteDuration Refined Positive)
extends ExternalAuthorizationService {

private val cache: Cache[User.Id, Set[Group]] =
CacheBuilder
.newBuilder
.expireAfterWrite(ttl.value.toMillis, TimeUnit.MILLISECONDS)
.build[User.Id, Set[Group]]

private val cacheableGrantsFor = new CacheableAction[LoggedUser, Set[Group]](ttl, underlying.grantsFor)

override val id: ExternalAuthorizationService#Id = underlying.id

override def grantsFor(loggedUser: LoggedUser): Task[Set[Group]] = {
Option(cache.getIfPresent(loggedUser.id)) match {
case Some(groups) =>
Task.now(groups)
case None =>
underlying
.grantsFor(loggedUser)
.map { groups =>
cache.put(loggedUser.id, groups)
groups
}
}
}
override def grantsFor(loggedUser: LoggedUser): Task[Set[Group]] =
cacheableGrantsFor.call(loggedUser)
}
@@ -0,0 +1,95 @@
package tech.beshu.ror.acl.blocks.definitions.ldap

import java.nio.charset.Charset

import com.google.common.hash.Hashing
import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Positive
import monix.eval.Task
import tech.beshu.ror.acl.blocks.definitions.ldap.CacheableLdapAuthenticationServiceDecorator.HashedUserCredentials
import tech.beshu.ror.acl.domain
import tech.beshu.ror.acl.domain.{Group, User}
import tech.beshu.ror.acl.utils.{CacheableAction, CacheableActionWithKeyMapping}

import scala.concurrent.duration.FiniteDuration
import scala.language.higherKinds

class CacheableLdapAuthenticationServiceDecorator(underlying: LdapAuthenticationService,
ttl: FiniteDuration Refined Positive)
extends LdapAuthenticationService {

private val cacheableAuthentication =
new CacheableActionWithKeyMapping[(User.Id, domain.Secret), HashedUserCredentials, Boolean](ttl, authenticateAction, hashCredential)
private val cacheableLdapUserService = new CacheableLdapUserServiceDecorator(underlying, ttl)

override val id: LdapService.Name = underlying.id

override def ldapUserBy(userId: User.Id): Task[Option[LdapUser]] =
cacheableLdapUserService.ldapUserBy(userId)

override def authenticate(user: User.Id, secret: domain.Secret): Task[Boolean] =
cacheableAuthentication.call((user, secret))

private def hashCredential(value: (User.Id, domain.Secret)) = {
val (user, secret) = value
HashedUserCredentials(user, Hashing.sha256.hashString(secret.value, Charset.defaultCharset).toString)
}

private def authenticateAction(value: (User.Id, domain.Secret)) = {
val (userId, secret) = value
underlying.authenticate(userId, secret)
}
}

object CacheableLdapAuthenticationServiceDecorator {
private[CacheableLdapAuthenticationServiceDecorator] final case class HashedUserCredentials(user: User.Id,
hashedCredentials: String)
}

class CacheableLdapAuthorizationServiceDecorator(underlying: LdapAuthorizationService,
ttl: FiniteDuration Refined Positive)
extends LdapAuthorizationService {

private val cacheableGroupsOf = new CacheableAction[User.Id, Set[Group]](ttl, underlying.groupsOf)
private val cacheableLdapUserService = new CacheableLdapUserServiceDecorator(underlying, ttl)

override val id: LdapService.Name = underlying.id

override def ldapUserBy(userId: User.Id): Task[Option[LdapUser]] =
cacheableLdapUserService.ldapUserBy(userId)

override def groupsOf(id: User.Id): Task[Set[domain.Group]] =
cacheableGroupsOf.call(id)
}

class CacheableLdapServiceDecorator(underlying: LdapAuthService,
ttl: FiniteDuration Refined Positive)
extends LdapAuthService {

private val cacheableLdapAuthenticationService = new CacheableLdapAuthenticationServiceDecorator(underlying, ttl)
private val cacheableLdapAuthorizationService = new CacheableLdapAuthorizationServiceDecorator(underlying, ttl)

override val id: LdapService.Name = underlying.id

override def ldapUserBy(userId: User.Id): Task[Option[LdapUser]] =
cacheableLdapAuthenticationService.ldapUserBy(userId)

override def authenticate(user: User.Id, secret: domain.Secret): Task[Boolean] =
cacheableLdapAuthenticationService.authenticate(user, secret)

override def groupsOf(id: User.Id): Task[Set[domain.Group]] =
cacheableLdapAuthorizationService.groupsOf(id)
}

private class CacheableLdapUserServiceDecorator(underlying: LdapUserService,
ttl: FiniteDuration Refined Positive)
extends LdapUserService {

private val cacheableLdapUserById = new CacheableAction[User.Id, Option[LdapUser]](ttl, underlying.ldapUserBy)

override val id: LdapService.Name = underlying.id

override def ldapUserBy(userId: User.Id): Task[Option[LdapUser]] =
cacheableLdapUserById.call(userId)

}
@@ -0,0 +1,57 @@
package tech.beshu.ror.acl.blocks.definitions.ldap

import cats.{Eq, Show}
import eu.timepit.refined.types.string.NonEmptyString
import monix.eval.Task
import tech.beshu.ror.acl.blocks.definitions.ldap.LdapService.Name
import tech.beshu.ror.acl.domain.{Group, Secret, User}
import tech.beshu.ror.acl.factory.decoders.definitions.Definitions.Item

sealed trait LdapService extends Item {
override type Id = Name
def id: Id

override implicit def show: Show[Name] = Name.nameShow
}

object LdapService {
final case class Name(value: NonEmptyString)
object Name {
implicit val nameEq: Eq[Name] = Eq.fromUniversalEquals
implicit val nameShow: Show[Name] = Show.show(_.value.value)
}
}

trait LdapUserService extends LdapService {
def ldapUserBy(userId: User.Id): Task[Option[LdapUser]]
}

trait LdapAuthenticationService extends LdapUserService {
def authenticate(user: User.Id, secret: Secret): Task[Boolean]
}

trait LdapAuthorizationService extends LdapUserService {
def groupsOf(id: User.Id): Task[Set[Group]]
}

trait LdapAuthService extends LdapAuthenticationService with LdapAuthorizationService

class ComposedLdapAuthService(override val id: LdapService#Id,
ldapAuthenticationService: LdapAuthenticationService,
ldapAuthorizationService: LdapAuthorizationService)
extends LdapAuthService {

def ldapUserBy(userId: User.Id): Task[Option[LdapUser]] =
ldapAuthenticationService.ldapUserBy(userId)

override def authenticate(user: User.Id, secret: Secret): Task[Boolean] =
ldapAuthenticationService.authenticate(user, secret)

override def groupsOf(id: User.Id): Task[Set[Group]] =
ldapAuthorizationService.groupsOf(id)
}


final case class LdapUser(id: User.Id, dn: Dn)
final case class Dn(value: NonEmptyString)

Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.