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
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion bin/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 0 additions & 35 deletions commons/src/main/java/tech/beshu/ror/commons/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,39 +82,4 @@ public static String makeAbsolutePath(String path, String basePath) {
}
return path;
}

public static final Set<String> RO_ACTIONS = Sets.newHashSet(
"indices:admin/exists",
"indices:admin/mappings/fields/get*",
"indices:admin/mappings/get*",
"indices:admin/validate/query",
"indices:admin/get",
"indices:admin/refresh*",
"indices:data/read/*"
);

public static final Set<String> CLUSTER_ACTIONS = Sets.newHashSet(
"cluster:monitor/nodes/info",
"cluster:monitor/main",
"cluster:monitor/health",
"cluster:monitor/state",
"cluster:monitor/xpack/*",
"indices:admin/template/get*"
);

public static final Set<String> RW_ACTIONS = Sets.newHashSet(
"indices:admin/create",
"indices:admin/mapping/put",
"indices:data/write/delete*",
"indices:data/write/index",
"indices:data/write/update*",
"indices:data/write/bulk*",
"indices:admin/template/*"
);

public static final Set<String> ADMIN_ACTIONS = Sets.newHashSet(
"cluster:admin/rradmin/*",
"indices:data/write/*", // <-- DEPRECATED!
"indices:admin/create"
);
}
8 changes: 8 additions & 0 deletions corex/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion corex/src/main/java/tech/beshu/ror/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public static String makeAbsolutePath(String path, String basePath) {
"cluster:monitor/main",
"cluster:monitor/health",
"cluster:monitor/state",
"cluster:monitor/xpack/*",
"cluster:*/xpack/*",
"indices:admin/template/get*"
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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.
Expand All @@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* This file is part of ReadonlyREST.
*
* ReadonlyREST is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ReadonlyREST is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ReadonlyREST. If not, see http://www.gnu.org/licenses/
*/
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)

}
Loading