Skip to content

Commit

Permalink
Cache the active realm information in Identities (#4325)
Browse files Browse the repository at this point in the history
  • Loading branch information
olivergrabinski authored Oct 5, 2023
1 parent c6a155c commit b8ed2c5
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package ch.epfl.bluebrain.nexus.delta.sdk.identities

import akka.http.scaladsl.model.headers.{Authorization, OAuth2BearerToken}
import akka.http.scaladsl.model.{HttpRequest, StatusCodes, Uri}
import cats.data.NonEmptySet
import cats.data.{NonEmptySet, OptionT}
import cats.effect.IO
import cats.syntax.all._
import ch.epfl.bluebrain.nexus.delta.kernel.Logger
Expand All @@ -12,7 +12,7 @@ import ch.epfl.bluebrain.nexus.delta.kernel.kamon.KamonMetricComponent
import ch.epfl.bluebrain.nexus.delta.kernel.search.Pagination.FromPagination
import ch.epfl.bluebrain.nexus.delta.sdk.http.HttpClient
import ch.epfl.bluebrain.nexus.delta.sdk.http.HttpClientError.HttpClientStatusError
import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesImpl.{extractGroups, logger, GroupsCache}
import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesImpl.{extractGroups, logger, GroupsCache, RealmCache}
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.TokenRejection.{GetGroupsFromOidcError, InvalidAccessToken, UnknownAccessTokenIssuer}
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.{AuthToken, Caller}
import ch.epfl.bluebrain.nexus.delta.sdk.model.ResourceF
Expand All @@ -31,6 +31,7 @@ import io.circe.{Decoder, HCursor, Json}
import scala.util.Try

class IdentitiesImpl private[identities] (
realm: RealmCache,
findActiveRealm: String => IO[Option[Realm]],
getUserInfo: (Uri, OAuth2BearerToken) => IO[Json],
groups: GroupsCache
Expand Down Expand Up @@ -61,6 +62,11 @@ class IdentitiesImpl private[identities] (
)
}

def fetchRealm(parsedToken: ParsedToken): IO[Realm] = {
val getRealm = realm.getOrElseAttemptUpdate(parsedToken.rawToken, findActiveRealm(parsedToken.issuer))
OptionT(getRealm).getOrRaise(UnknownAccessTokenIssuer)
}

def fetchGroups(parsedToken: ParsedToken, realm: Realm): IO[Set[Group]] = {
parsedToken.groups
.map { s =>
Expand All @@ -77,11 +83,10 @@ class IdentitiesImpl private[identities] (
}

val result = for {
parsedToken <- IO.fromEither(ParsedToken.fromToken(token))
activeRealmOption <- findActiveRealm(parsedToken.issuer)
activeRealm <- IO.fromOption(activeRealmOption)(UnknownAccessTokenIssuer)
_ <- validate(activeRealm.acceptedAudiences, parsedToken, realmKeyset(activeRealm))
groups <- fetchGroups(parsedToken, activeRealm)
parsedToken <- IO.fromEither(ParsedToken.fromToken(token))
activeRealm <- fetchRealm(parsedToken)
_ <- validate(activeRealm.acceptedAudiences, parsedToken, realmKeyset(activeRealm))
groups <- fetchGroups(parsedToken, activeRealm)
} yield {
val user = User(parsedToken.subject, activeRealm.label)
Caller(user, groups ++ Set(Anonymous, user, Authenticated(activeRealm.label)))
Expand All @@ -95,6 +100,7 @@ class IdentitiesImpl private[identities] (
object IdentitiesImpl {

type GroupsCache = LocalCache[String, Set[Group]]
type RealmCache = LocalCache[String, Realm]

private val logger = Logger.cats[this.type]

Expand Down Expand Up @@ -133,10 +139,14 @@ object IdentitiesImpl {
* the cache configuration
*/
def apply(realms: Realms, hc: HttpClient, config: CacheConfig): IO[Identities] = {
val groupsCache = LocalCache[String, Set[Group]](config)
val realmCache = LocalCache[String, Realm](config)

val findActiveRealm: String => IO[Option[Realm]] = { (issuer: String) =>
val pagination = FromPagination(0, 1000)
val params = RealmSearchParams(issuer = Some(issuer), deprecated = Some(false))
val sort = ResourceF.defaultSort[Realm]

realms.list(pagination, params, sort).map {
_.results.map(entry => entry.source.value).headOption
}
Expand All @@ -145,8 +155,8 @@ object IdentitiesImpl {
hc.toJson(HttpRequest(uri = uri, headers = List(Authorization(token))))
}

LocalCache[String, Set[Group]](config).map { groups =>
new IdentitiesImpl(findActiveRealm, getUserInfo, groups)
(realmCache, groupsCache).mapN { (realm, groups) =>
new IdentitiesImpl(realm, findActiveRealm, getUserInfo, groups)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import akka.http.scaladsl.model.headers.OAuth2BearerToken
import akka.http.scaladsl.model.{HttpRequest, Uri}
import cats.data.NonEmptySet
import cats.effect.IO
import cats.effect.concurrent.Ref
import cats.implicits._
import ch.epfl.bluebrain.nexus.delta.kernel.cache.LocalCache
import ch.epfl.bluebrain.nexus.delta.sdk.generators.{RealmGen, WellKnownGen}
import ch.epfl.bluebrain.nexus.delta.sdk.http.HttpClientError.HttpUnexpectedError
import ch.epfl.bluebrain.nexus.delta.sdk.identities.IdentitiesImpl.{GroupsCache, RealmCache}
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.TokenRejection.{AccessTokenDoesNotContainAnIssuer, AccessTokenDoesNotContainSubject, GetGroupsFromOidcError, InvalidAccessToken, InvalidAccessTokenFormat, UnknownAccessTokenIssuer}
import ch.epfl.bluebrain.nexus.delta.sdk.identities.model.{AuthToken, Caller}
import ch.epfl.bluebrain.nexus.delta.sdk.realms.model.Realm
Expand Down Expand Up @@ -108,6 +110,8 @@ class IdentitiesImplSuite extends CatsEffectSuite with TestHelpers with IOFromMa
keys = Set(parser.parse(rsaKey.toPublicJWK.toJSONString).rightValue)
)

type FindRealm = String => IO[Option[Realm]]

private val findActiveRealm: String => IO[Option[Realm]] = ioFromMap[String, Realm](
githubLabel.value -> github,
githubLabel2.value -> github2,
Expand All @@ -120,15 +124,21 @@ class IdentitiesImplSuite extends CatsEffectSuite with TestHelpers with IOFromMa
(_: Uri) => HttpUnexpectedError(HttpRequest(), "Error while getting response")
)(uri)

private val identities: Identities = LocalCache[String, Set[Group]]()
.map { cache =>
new IdentitiesImpl(
findActiveRealm,
(uri: Uri, _: OAuth2BearerToken) => userInfo(uri),
cache
)
}
.unsafeRunSync()
private val realmCache = LocalCache[String, Realm]()
private val groupsCache = LocalCache[String, Set[Group]]()

private val identitiesFromCaches: (RealmCache, GroupsCache) => FindRealm => Identities =
(realmCache, groupsCache) =>
findRealm =>
new IdentitiesImpl(
realmCache,
findRealm,
(uri: Uri, _: OAuth2BearerToken) => userInfo(uri),
groupsCache
)

private val identities =
identitiesFromCaches(realmCache.unsafeRunSync(), groupsCache.unsafeRunSync())(findActiveRealm)

private val auth = Authenticated(githubLabel)
private val group1 = Group("group1", githubLabel)
Expand Down Expand Up @@ -326,4 +336,53 @@ class IdentitiesImplSuite extends CatsEffectSuite with TestHelpers with IOFromMa
identities.exchange(token).intercept(expectedError)
}

test("Cache realm and groups") {
val token = generateToken(
subject = "Bobby",
issuer = githubLabel,
rsaKey = rsaKey,
expires = nowPlus1h,
groups = None,
useCommas = true
)

for {
parsedToken <- IO.fromEither(ParsedToken.fromToken(token))
realm <- realmCache
groups <- groupsCache
_ <- realm.get(parsedToken.rawToken).assertNone
_ <- groups.get(parsedToken.rawToken).assertNone
_ <- identitiesFromCaches(realm, groups)(findActiveRealm).exchange(token)
_ <- realm.get(parsedToken.rawToken).assertSome(github)
_ <- groups.get(parsedToken.rawToken).assertSome(Set(group3, group4))
} yield ()
}

test("Find active realm function should not run once value is cached") {
val token = generateToken(
subject = "Robert",
issuer = githubLabel,
rsaKey = rsaKey,
expires = nowPlus1h,
groups = Some(Set("group1", "group2"))
)

def findRealmOnce: Ref[IO, Boolean] => String => IO[Option[Realm]] = ref =>
_ =>
for {
flag <- ref.get
_ <- IO.raiseWhen(!flag)(new RuntimeException("Function executed more than once!"))
_ <- ref.set(false)
} yield Some(github)

for {
sem <- Ref.of[IO, Boolean](true)
realm <- realmCache
groups <- groupsCache
identities = identitiesFromCaches(realm, groups)(findRealmOnce(sem))
_ <- identities.exchange(token)
_ <- identities.exchange(token)
} yield ()
}

}

0 comments on commit b8ed2c5

Please sign in to comment.