mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 09:17:43 +03:00
IdentityProviderConfig persistence layer [DPP-1331] (#15634)
This commit is contained in:
parent
fd8439144a
commit
7760ec9fb3
@ -4,12 +4,15 @@
|
||||
package com.daml.caching
|
||||
|
||||
import com.daml.metrics.CacheMetrics
|
||||
import com.github.benmanes.caffeine.cache.AsyncCacheLoader
|
||||
import com.github.benmanes.caffeine.{cache => caffeine}
|
||||
|
||||
import java.util.concurrent.{CompletableFuture, Executor}
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.jdk.FutureConverters.CompletionStageOps
|
||||
import scala.jdk.OptionConverters.{RichOptional, RichOptionalLong}
|
||||
import scala.concurrent.Future
|
||||
import scala.jdk.CollectionConverters._
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
object CaffeineCache {
|
||||
|
||||
@ -85,4 +88,17 @@ object CaffeineCache {
|
||||
)
|
||||
}
|
||||
|
||||
class FutureAsyncCacheLoader[K, V](func: K => Future[V])(implicit
|
||||
executionContext: ExecutionContext
|
||||
) extends AsyncCacheLoader[K, V] {
|
||||
override def asyncLoad(key: K, executor: Executor): CompletableFuture[_ <: V] = {
|
||||
val cf = new CompletableFuture[V]
|
||||
func(key).onComplete {
|
||||
case Success(value) => cf.complete(value)
|
||||
case Failure(e) => cf.completeExceptionally(e)
|
||||
}
|
||||
cf
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,7 +16,10 @@ import com.daml.logging.entries.{LoggingValue, ToLoggingValue}
|
||||
import scalaz.syntax.tag._
|
||||
import scalaz.{@@, Tag}
|
||||
|
||||
import java.net.URL
|
||||
import scala.collection.immutable
|
||||
import scala.util.Try
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
object domain {
|
||||
|
||||
@ -329,4 +332,75 @@ object domain {
|
||||
object Feature {
|
||||
case object UserManagement extends Feature
|
||||
}
|
||||
|
||||
final case class JwksUrl(value: String) extends AnyVal {
|
||||
def toURL = new URL(value)
|
||||
}
|
||||
object JwksUrl {
|
||||
def fromString(value: String): Either[String, JwksUrl] =
|
||||
Try(new URL(value)).toEither.left
|
||||
.map { case NonFatal(e) =>
|
||||
e.getMessage
|
||||
}
|
||||
.map(_ => JwksUrl(value))
|
||||
|
||||
def assertFromString(str: String): JwksUrl = fromString(str) match {
|
||||
case Right(value) => value
|
||||
case Left(err) => throw new IllegalArgumentException(err)
|
||||
}
|
||||
}
|
||||
|
||||
sealed trait IdentityProviderId {
|
||||
def toRequestString: String
|
||||
|
||||
def toDb: Option[IdentityProviderId.Id]
|
||||
}
|
||||
|
||||
object IdentityProviderId {
|
||||
final case object Default extends IdentityProviderId {
|
||||
override def toRequestString: String = ""
|
||||
override def toDb: Option[Id] = None
|
||||
}
|
||||
|
||||
final case class Id(value: Ref.LedgerString) extends IdentityProviderId {
|
||||
override def toRequestString: String = value
|
||||
|
||||
override def toDb: Option[Id] = Some(this)
|
||||
}
|
||||
|
||||
object Id {
|
||||
def fromString(id: String): Either[String, IdentityProviderId.Id] = {
|
||||
Ref.LedgerString.fromString(id).map(Id.apply)
|
||||
}
|
||||
|
||||
def assertFromString(id: String): Id = {
|
||||
Id(Ref.LedgerString.assertFromString(id))
|
||||
}
|
||||
}
|
||||
|
||||
def apply(identityProviderId: String): IdentityProviderId =
|
||||
Some(identityProviderId).filter(_.nonEmpty) match {
|
||||
case Some(id) => Id(Ref.LedgerString.assertFromString(id))
|
||||
case None => Default
|
||||
}
|
||||
|
||||
def fromString(identityProviderId: String): Either[String, IdentityProviderId] =
|
||||
Some(identityProviderId).filter(_.nonEmpty) match {
|
||||
case Some(id) => Ref.LedgerString.fromString(id).map(Id.apply)
|
||||
case None => Right(Default)
|
||||
}
|
||||
|
||||
def fromDb(identityProviderId: Option[IdentityProviderId.Id]): IdentityProviderId =
|
||||
identityProviderId match {
|
||||
case None => IdentityProviderId.Default
|
||||
case Some(id) => id
|
||||
}
|
||||
}
|
||||
|
||||
final case class IdentityProviderConfig(
|
||||
identityProviderId: IdentityProviderId.Id,
|
||||
isDeactivated: Boolean = false,
|
||||
jwksUrl: JwksUrl,
|
||||
issuer: String,
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.ledger.api
|
||||
|
||||
import com.daml.ledger.api.domain.IdentityProviderId
|
||||
import com.daml.lf.data.Ref.LedgerString
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.wordspec.AnyWordSpec
|
||||
|
||||
class IdentityProviderIdSpec extends AnyWordSpec with Matchers {
|
||||
|
||||
"IdentityProviderId.Default" in {
|
||||
IdentityProviderId.Default.toDb shouldBe None
|
||||
IdentityProviderId.Default.toRequestString shouldBe ""
|
||||
}
|
||||
|
||||
"IdentityProviderId.Id" in {
|
||||
IdentityProviderId.Id(LedgerString.assertFromString("a123")).toDb shouldBe Some(
|
||||
IdentityProviderId.Id(LedgerString.assertFromString("a123"))
|
||||
)
|
||||
IdentityProviderId.Id(LedgerString.assertFromString("a123")).toRequestString shouldBe "a123"
|
||||
}
|
||||
|
||||
"IdentityProviderId.Id.fromString" in {
|
||||
IdentityProviderId.Id.fromString("") shouldBe Left("Daml-LF Ledger String is empty")
|
||||
|
||||
IdentityProviderId.Id.fromString("a" * 256) shouldBe Left(
|
||||
"Daml-LF Ledger String is too long (max: 255)"
|
||||
)
|
||||
|
||||
IdentityProviderId.Id.fromString("a123") shouldBe Right(
|
||||
IdentityProviderId.Id(
|
||||
LedgerString.assertFromString("a123")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
"IdentityProviderId.Id.assertFromString" in {
|
||||
assertThrows[IllegalArgumentException] {
|
||||
IdentityProviderId.Id.assertFromString("")
|
||||
}
|
||||
|
||||
assertThrows[IllegalArgumentException] {
|
||||
IdentityProviderId.Id.assertFromString("a" * 256)
|
||||
}
|
||||
|
||||
IdentityProviderId.Id.assertFromString("a123") shouldBe
|
||||
IdentityProviderId.Id(
|
||||
LedgerString.assertFromString("a123")
|
||||
)
|
||||
}
|
||||
|
||||
"IdentityProviderId.apply" in {
|
||||
IdentityProviderId("") shouldBe IdentityProviderId.Default
|
||||
IdentityProviderId("a123") shouldBe IdentityProviderId.Id.assertFromString("a123")
|
||||
}
|
||||
|
||||
"IdentityProviderId.fromString" in {
|
||||
IdentityProviderId.fromString("") shouldBe Right(IdentityProviderId.Default)
|
||||
IdentityProviderId.fromString("a123") shouldBe Right(
|
||||
IdentityProviderId.Id.assertFromString("a123")
|
||||
)
|
||||
}
|
||||
|
||||
"IdentityProviderId.fromDb" in {
|
||||
IdentityProviderId.fromDb(None) shouldBe IdentityProviderId.Default
|
||||
IdentityProviderId.fromDb(
|
||||
Some(IdentityProviderId.Id.assertFromString("a123"))
|
||||
) shouldBe IdentityProviderId.Id.assertFromString("a123")
|
||||
}
|
||||
}
|
||||
|
||||
object IdentityProviderIdSpec {}
|
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.metrics
|
||||
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import com.daml.metrics.api.dropwizard.FactoryWithDBMetrics
|
||||
import com.daml.metrics.api.{MetricDoc, MetricName}
|
||||
|
||||
@MetricDoc.GroupTag(
|
||||
representative = "daml.identity_provider_config_store.<operation>",
|
||||
groupableClass = classOf[DatabaseMetrics],
|
||||
)
|
||||
class IdentityProviderConfigStoreMetrics(
|
||||
override val prefix: MetricName,
|
||||
override val registry: MetricRegistry,
|
||||
) extends FactoryWithDBMetrics {
|
||||
|
||||
val cache = new CacheMetrics(prefix :+ "cache", registry)
|
||||
val createIdpConfig: DatabaseMetrics = createDbMetrics("create_identity_provider_config")
|
||||
val getIdpConfig: DatabaseMetrics = createDbMetrics("get_identity_provider_config")
|
||||
val deleteIdpConfig: DatabaseMetrics = createDbMetrics("delete_identity_provider_config")
|
||||
val updateIdpConfig: DatabaseMetrics = createDbMetrics("update_identity_provider_config")
|
||||
val listIdpConfigs: DatabaseMetrics = createDbMetrics("list_identity_provider_configs")
|
||||
}
|
@ -41,6 +41,12 @@ final class Metrics(override val registry: MetricRegistry, val otelMeter: OtelMe
|
||||
object partyRecordStore
|
||||
extends PartyRecordStoreMetrics(prefix :+ "party_record_store", registry)
|
||||
|
||||
object identityProviderConfigStore
|
||||
extends IdentityProviderConfigStoreMetrics(
|
||||
prefix :+ "identity_provider_config_store",
|
||||
registry,
|
||||
)
|
||||
|
||||
object index extends IndexMetrics(prefix :+ "index", registry)
|
||||
|
||||
object indexer extends IndexerMetrics(prefix :+ "indexer", registry)
|
||||
|
@ -1 +1 @@
|
||||
a03ea7d689a0f2babb56fe2fce7cc11d853c27e587e934d8c2d51c3a9837b38e
|
||||
b17769a61b71fedc597a1c55896fc5af10c97801dac4e7b9c20feebece7f39ea
|
||||
|
@ -384,6 +384,17 @@ CREATE UNIQUE INDEX participant_metering_from_to_application ON participant_mete
|
||||
|
||||
-- NOTE: We keep participant user and party record tables independent from indexer-based tables, such that
|
||||
-- we maintain a property that they can be moved to a separate database without any extra schema changes.
|
||||
---------------------------------------------------------------------------------------------------
|
||||
-- Participant local store: identity provider configurations
|
||||
---------------------------------------------------------------------------------------------------
|
||||
CREATE TABLE participant_identity_provider_config
|
||||
(
|
||||
identity_provider_id VARCHAR(255) PRIMARY KEY NOT NULL,
|
||||
issuer VARCHAR NOT NULL UNIQUE,
|
||||
jwks_url VARCHAR NOT NULL,
|
||||
is_deactivated BOOLEAN NOT NULL
|
||||
);
|
||||
|
||||
---------------------------------------------------------------------------------------------------
|
||||
-- Participant local store: users
|
||||
---------------------------------------------------------------------------------------------------
|
||||
|
@ -0,0 +1 @@
|
||||
1358474f56f8c553bf071b3790aa691c9a2e6d6f013f7898fa16349e9c879cc0
|
@ -0,0 +1,7 @@
|
||||
CREATE TABLE participant_identity_provider_config
|
||||
(
|
||||
identity_provider_id VARCHAR2(255) PRIMARY KEY NOT NULL,
|
||||
issuer VARCHAR2(4000) NOT NULL UNIQUE,
|
||||
jwks_url VARCHAR2(4000) NOT NULL,
|
||||
is_deactivated NUMBER DEFAULT 0 NOT NULL
|
||||
);
|
@ -0,0 +1 @@
|
||||
7cd4dbb7ff7886500a11a41dd7b248d0af9acb2ba51e81f69258a95267a08e19
|
@ -0,0 +1,7 @@
|
||||
CREATE TABLE participant_identity_provider_config
|
||||
(
|
||||
identity_provider_id VARCHAR(255) PRIMARY KEY NOT NULL COLLATE "C",
|
||||
issuer VARCHAR NOT NULL UNIQUE,
|
||||
jwks_url VARCHAR NOT NULL,
|
||||
is_deactivated BOOLEAN NOT NULL
|
||||
);
|
@ -10,7 +10,7 @@ import com.daml.platform.store.dao.DbDispatcher
|
||||
import java.sql.Connection
|
||||
import scala.concurrent.Future
|
||||
|
||||
object DbDispatcherLeftOps {
|
||||
object Ops {
|
||||
|
||||
private[localstore] def rollbackOnLeft[E, T](sql: Connection => Either[E, T])(
|
||||
connection: Connection
|
@ -0,0 +1,206 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import com.daml.ledger.api.domain
|
||||
import com.daml.ledger.api.domain.IdentityProviderId
|
||||
import com.daml.logging.{ContextualizedLogger, LoggingContext}
|
||||
import com.daml.metrics.{DatabaseMetrics, Metrics}
|
||||
import com.daml.platform.localstore.api.IdentityProviderConfigStore.{
|
||||
IdentityProviderConfigExists,
|
||||
IdentityProviderConfigNotFound,
|
||||
IdentityProviderConfigWithIssuerExists,
|
||||
Result,
|
||||
TooManyIdentityProviderConfigs,
|
||||
}
|
||||
import com.daml.platform.localstore.api.{IdentityProviderConfigStore, IdentityProviderConfigUpdate}
|
||||
import com.daml.platform.store.DbSupport
|
||||
import com.daml.platform.store.dao.DbDispatcher
|
||||
import Ops._
|
||||
|
||||
import java.sql.Connection
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
|
||||
class PersistentIdentityProviderConfigStore(
|
||||
dbSupport: DbSupport,
|
||||
metrics: Metrics,
|
||||
maxIdentityProviderConfigs: Int,
|
||||
)(implicit executionContext: ExecutionContext)
|
||||
extends IdentityProviderConfigStore {
|
||||
|
||||
private val backend = dbSupport.storageBackendFactory.createIdentityProviderConfigStorageBackend
|
||||
private val dbDispatcher: DbDispatcher = dbSupport.dbDispatcher
|
||||
private val logger = ContextualizedLogger.get(getClass)
|
||||
|
||||
override def createIdentityProviderConfig(identityProviderConfig: domain.IdentityProviderConfig)(
|
||||
implicit loggingContext: LoggingContext
|
||||
): Future[Result[domain.IdentityProviderConfig]] =
|
||||
inTransaction(_.createIdpConfig) { implicit connection =>
|
||||
val id = identityProviderConfig.identityProviderId
|
||||
for {
|
||||
_ <- idpConfigDoesNotExist(id)
|
||||
_ <- idpConfigByIssuerDoesNotExist(
|
||||
Some(identityProviderConfig.issuer),
|
||||
identityProviderConfig.identityProviderId,
|
||||
)
|
||||
_ = backend.createIdentityProviderConfig(identityProviderConfig)(connection)
|
||||
_ <- tooManyIdentityProviderConfigs()(connection)
|
||||
domainConfig <- backend
|
||||
.getIdentityProviderConfig(id)(connection)
|
||||
.toRight(IdentityProviderConfigNotFound(id))
|
||||
} yield domainConfig
|
||||
}.map(tapSuccess { cfg =>
|
||||
logger.info(
|
||||
s"Created new identity provider configuration: $cfg"
|
||||
)
|
||||
})
|
||||
|
||||
override def getIdentityProviderConfig(id: IdentityProviderId.Id)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[domain.IdentityProviderConfig]] =
|
||||
inTransaction(_.getIdpConfig) { implicit connection =>
|
||||
backend
|
||||
.getIdentityProviderConfig(id)(connection)
|
||||
.toRight(IdentityProviderConfigNotFound(id))
|
||||
}
|
||||
|
||||
override def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[Unit]] =
|
||||
inTransaction(_.deleteIdpConfig) { implicit connection =>
|
||||
if (!backend.deleteIdentityProviderConfig(id)(connection)) {
|
||||
Left(IdentityProviderConfigNotFound(id))
|
||||
} else {
|
||||
Right(())
|
||||
}
|
||||
}.map(tapSuccess { _ =>
|
||||
logger.info(
|
||||
s"Deleted identity provider configuration with id $id"
|
||||
)
|
||||
})
|
||||
|
||||
override def listIdentityProviderConfigs()(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[Seq[domain.IdentityProviderConfig]]] = {
|
||||
inTransaction(_.listIdpConfigs) { implicit connection =>
|
||||
Right(backend.listIdentityProviderConfigs()(connection))
|
||||
}
|
||||
}
|
||||
|
||||
override def updateIdentityProviderConfig(update: IdentityProviderConfigUpdate)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[domain.IdentityProviderConfig]] = {
|
||||
inTransaction(_.updateIdpConfig) { implicit connection =>
|
||||
val id = update.identityProviderId
|
||||
for {
|
||||
_ <- idpConfigExists(id)
|
||||
_ <- idpConfigByIssuerDoesNotExist(update.issuerUpdate, update.identityProviderId)
|
||||
_ <- updateIssuer(update)(connection)
|
||||
_ <- updateJwksUrl(update)(connection)
|
||||
_ <- updateIsDeactivated(update)(connection)
|
||||
identityProviderConfig <- backend
|
||||
.getIdentityProviderConfig(id)(connection)
|
||||
.toRight(IdentityProviderConfigNotFound(id))
|
||||
} yield identityProviderConfig
|
||||
}.map(tapSuccess { _ =>
|
||||
logger.info(
|
||||
s"Updated identity provider configuration with id ${update.identityProviderId}"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private def updateIssuer(
|
||||
update: IdentityProviderConfigUpdate
|
||||
)(connection: Connection): Result[Unit] = {
|
||||
val execute =
|
||||
update.issuerUpdate.forall(backend.updateIssuer(update.identityProviderId, _)(connection))
|
||||
Either.cond(execute, (), IdentityProviderConfigNotFound(update.identityProviderId))
|
||||
}
|
||||
private def updateJwksUrl(
|
||||
update: IdentityProviderConfigUpdate
|
||||
)(connection: Connection): Result[Unit] = {
|
||||
val execute = update.jwksUrlUpdate.forall(
|
||||
backend.updateJwksUrl(update.identityProviderId, _)(connection)
|
||||
)
|
||||
Either.cond(execute, (), IdentityProviderConfigNotFound(update.identityProviderId))
|
||||
}
|
||||
|
||||
private def updateIsDeactivated(
|
||||
update: IdentityProviderConfigUpdate
|
||||
)(connection: Connection): Result[Unit] = {
|
||||
val execute = update.isDeactivatedUpdate.forall(
|
||||
backend.updateIsDeactivated(update.identityProviderId, _)(connection)
|
||||
)
|
||||
Either.cond(execute, (), IdentityProviderConfigNotFound(update.identityProviderId))
|
||||
}
|
||||
|
||||
private def tooManyIdentityProviderConfigs()(
|
||||
connection: Connection
|
||||
): Result[Unit] =
|
||||
Either.cond(
|
||||
backend.countIdentityProviderConfigs()(connection) <= maxIdentityProviderConfigs,
|
||||
(),
|
||||
TooManyIdentityProviderConfigs(),
|
||||
)
|
||||
|
||||
private def idpConfigExists(
|
||||
id: IdentityProviderId.Id
|
||||
)(implicit connection: Connection): Result[Unit] = Either.cond(
|
||||
backend.idpConfigByIdExists(id)(connection),
|
||||
(),
|
||||
IdentityProviderConfigNotFound(id),
|
||||
)
|
||||
|
||||
private def idpConfigDoesNotExist(
|
||||
id: IdentityProviderId.Id
|
||||
)(implicit connection: Connection): Result[Unit] = Either.cond(
|
||||
!backend.idpConfigByIdExists(id)(connection),
|
||||
(),
|
||||
IdentityProviderConfigExists(id),
|
||||
)
|
||||
|
||||
private def idpConfigByIssuerDoesNotExist(
|
||||
issuer: Option[String],
|
||||
id: IdentityProviderId.Id,
|
||||
)(implicit connection: Connection): Result[Unit] = issuer match {
|
||||
case Some(value) =>
|
||||
Either.cond(
|
||||
!backend.identityProviderConfigByIssuerExists(id, value)(connection),
|
||||
(),
|
||||
IdentityProviderConfigWithIssuerExists(value),
|
||||
)
|
||||
case None => Right(())
|
||||
}
|
||||
|
||||
private def inTransaction[T](
|
||||
dbMetric: metrics.daml.identityProviderConfigStore.type => DatabaseMetrics
|
||||
)(thunk: Connection => Result[T])(implicit loggingContext: LoggingContext): Future[Result[T]] =
|
||||
dbDispatcher
|
||||
.executeSqlEither(dbMetric(metrics.daml.identityProviderConfigStore))(thunk)
|
||||
|
||||
private def tapSuccess[T](f: T => Unit)(r: Result[T]): Result[T] = {
|
||||
r.foreach(f)
|
||||
r
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object PersistentIdentityProviderConfigStore {
|
||||
def cached(
|
||||
dbSupport: DbSupport,
|
||||
metrics: Metrics,
|
||||
expiryAfterWriteInSeconds: Int,
|
||||
maximumCacheSize: Int,
|
||||
maxIdentityProviderConfigs: Int,
|
||||
)(implicit
|
||||
executionContext: ExecutionContext,
|
||||
loggingContext: LoggingContext,
|
||||
) = new CachedIdentityProviderConfigStore(
|
||||
delegate =
|
||||
new PersistentIdentityProviderConfigStore(dbSupport, metrics, maxIdentityProviderConfigs),
|
||||
expiryAfterWriteInSeconds = expiryAfterWriteInSeconds,
|
||||
maximumCacheSize = maximumCacheSize,
|
||||
metrics = metrics,
|
||||
)
|
||||
}
|
@ -6,6 +6,7 @@ package com.daml.platform.store.backend
|
||||
import com.daml.platform.store.DbType
|
||||
import com.daml.platform.store.backend.h2.H2StorageBackendFactory
|
||||
import com.daml.platform.store.backend.localstore.{
|
||||
IdentityProviderStorageBackend,
|
||||
PartyRecordStorageBackend,
|
||||
UserManagementStorageBackend,
|
||||
}
|
||||
@ -37,6 +38,7 @@ trait StorageBackendFactory {
|
||||
def createResetStorageBackend: ResetStorageBackend
|
||||
def createStringInterningStorageBackend: StringInterningStorageBackend
|
||||
def createUserManagementStorageBackend: UserManagementStorageBackend
|
||||
def createIdentityProviderConfigStorageBackend: IdentityProviderStorageBackend
|
||||
def createMeteringStorageReadBackend(ledgerEndCache: LedgerEndCache): MeteringStorageReadBackend
|
||||
def createMeteringStorageWriteBackend: MeteringStorageWriteBackend
|
||||
|
||||
|
@ -5,6 +5,8 @@ package com.daml.platform.store.backend.common
|
||||
|
||||
import com.daml.platform.store.backend._
|
||||
import com.daml.platform.store.backend.localstore.{
|
||||
IdentityProviderStorageBackend,
|
||||
IdentityProviderStorageBackendImpl,
|
||||
PartyRecordStorageBackend,
|
||||
PartyRecordStorageBackendImpl,
|
||||
UserManagementStorageBackend,
|
||||
@ -29,6 +31,9 @@ trait CommonStorageBackendFactory extends StorageBackendFactory {
|
||||
override val createUserManagementStorageBackend: UserManagementStorageBackend =
|
||||
UserManagementStorageBackendImpl
|
||||
|
||||
override val createIdentityProviderConfigStorageBackend: IdentityProviderStorageBackend =
|
||||
IdentityProviderStorageBackendImpl
|
||||
|
||||
override def createMeteringStorageReadBackend(
|
||||
ledgerEndCache: LedgerEndCache
|
||||
): MeteringStorageReadBackend =
|
||||
|
@ -30,6 +30,7 @@ object H2ResetStorageBackend extends ResetStorageBackend {
|
||||
truncate table participant_users;
|
||||
truncate table participant_user_rights;
|
||||
truncate table participant_user_annotations;
|
||||
truncate table participant_identity_provider_config;
|
||||
truncate table transaction_metering;
|
||||
truncate table participant_metering;
|
||||
truncate table metering_parameters;
|
||||
|
@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.store.backend.localstore
|
||||
|
||||
import com.daml.ledger.api.domain.{IdentityProviderConfig, IdentityProviderId, JwksUrl}
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
trait IdentityProviderStorageBackend {
|
||||
def createIdentityProviderConfig(
|
||||
identityProviderConfig: IdentityProviderConfig
|
||||
)(connection: Connection): Unit
|
||||
|
||||
def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(connection: Connection): Boolean
|
||||
|
||||
def getIdentityProviderConfig(id: IdentityProviderId.Id)(
|
||||
connection: Connection
|
||||
): Option[IdentityProviderConfig]
|
||||
|
||||
def listIdentityProviderConfigs()(
|
||||
connection: Connection
|
||||
): Vector[IdentityProviderConfig]
|
||||
|
||||
def updateIssuer(id: IdentityProviderId.Id, newIssuer: String)(
|
||||
connection: Connection
|
||||
): Boolean
|
||||
|
||||
def updateJwksUrl(id: IdentityProviderId.Id, jwksUrl: JwksUrl)(
|
||||
connection: Connection
|
||||
): Boolean
|
||||
|
||||
def updateIsDeactivated(id: IdentityProviderId.Id, isDeactivated: Boolean)(
|
||||
connection: Connection
|
||||
): Boolean
|
||||
|
||||
def identityProviderConfigByIssuerExists(ignoreId: IdentityProviderId.Id, issuer: String)(
|
||||
connection: Connection
|
||||
): Boolean
|
||||
|
||||
def countIdentityProviderConfigs()(connection: Connection): Int
|
||||
|
||||
def idpConfigByIdExists(id: IdentityProviderId.Id)(connection: Connection): Boolean
|
||||
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.store.backend.localstore
|
||||
|
||||
import anorm.SqlParser.{bool, int, str}
|
||||
import anorm.{RowParser, SqlParser, SqlStringInterpolation, ~}
|
||||
import com.daml.ledger.api.domain.{IdentityProviderConfig, IdentityProviderId, JwksUrl}
|
||||
import com.daml.platform.store.backend.common.SimpleSqlAsVectorOf._
|
||||
import com.daml.scalautil.Statement.discard
|
||||
|
||||
import java.sql.Connection
|
||||
|
||||
object IdentityProviderStorageBackendImpl extends IdentityProviderStorageBackend {
|
||||
|
||||
private val IntParser: RowParser[Int] =
|
||||
int("dummy") map { i => i }
|
||||
|
||||
private val IdpConfigRecordParser: RowParser[(String, Boolean, String, String)] = {
|
||||
import com.daml.platform.store.backend.Conversions.bigDecimalColumnToBoolean
|
||||
str("identity_provider_id") ~
|
||||
bool("is_deactivated") ~
|
||||
str("jwks_url") ~
|
||||
str("issuer") map { case identityProviderId ~ isDeactivated ~ jwksURL ~ issuer =>
|
||||
(identityProviderId, isDeactivated, jwksURL, issuer)
|
||||
}
|
||||
}
|
||||
|
||||
override def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig)(
|
||||
connection: Connection
|
||||
): Unit = {
|
||||
val identityProviderId = identityProviderConfig.identityProviderId.value: String
|
||||
val isDeactivated = identityProviderConfig.isDeactivated
|
||||
val jwksUrl = identityProviderConfig.jwksUrl.value
|
||||
val issuer = identityProviderConfig.issuer
|
||||
discard(SQL"""
|
||||
INSERT INTO participant_identity_provider_config (identity_provider_id, is_deactivated, jwks_url, issuer)
|
||||
VALUES ($identityProviderId, $isDeactivated, $jwksUrl, $issuer)
|
||||
""".execute()(connection))
|
||||
}
|
||||
|
||||
override def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(
|
||||
connection: Connection
|
||||
): Boolean = {
|
||||
val updatedRowsCount =
|
||||
SQL"""
|
||||
DELETE FROM participant_identity_provider_config WHERE identity_provider_id = ${id.value: String}
|
||||
""".executeUpdate()(connection)
|
||||
updatedRowsCount == 1
|
||||
}
|
||||
|
||||
override def getIdentityProviderConfig(id: IdentityProviderId.Id)(
|
||||
connection: Connection
|
||||
): Option[IdentityProviderConfig] = {
|
||||
SQL"""
|
||||
SELECT identity_provider_id, is_deactivated, jwks_url, issuer
|
||||
FROM participant_identity_provider_config
|
||||
WHERE identity_provider_id = ${id.value: String}
|
||||
"""
|
||||
.as(IdpConfigRecordParser.singleOpt)(connection)
|
||||
.map { case (identityProviderId, isDeactivated, jwksURL, issuer) =>
|
||||
IdentityProviderConfig(
|
||||
identityProviderId = IdentityProviderId.Id.assertFromString(identityProviderId),
|
||||
isDeactivated = isDeactivated,
|
||||
jwksUrl = JwksUrl.assertFromString(jwksURL),
|
||||
issuer = issuer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override def listIdentityProviderConfigs()(
|
||||
connection: Connection
|
||||
): Vector[IdentityProviderConfig] = {
|
||||
SQL"""
|
||||
SELECT identity_provider_id, is_deactivated, jwks_url, issuer
|
||||
FROM participant_identity_provider_config
|
||||
ORDER BY identity_provider_id
|
||||
"""
|
||||
.asVectorOf(IdpConfigRecordParser)(connection)
|
||||
.map { case (identityProviderId, isDeactivated, jwksURL, issuer) =>
|
||||
IdentityProviderConfig(
|
||||
identityProviderId = IdentityProviderId.Id.assertFromString(identityProviderId),
|
||||
isDeactivated = isDeactivated,
|
||||
jwksUrl = JwksUrl.assertFromString(jwksURL),
|
||||
issuer = issuer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override def identityProviderConfigByIssuerExists(
|
||||
ignoreId: IdentityProviderId.Id,
|
||||
issuer: String,
|
||||
)(connection: Connection): Boolean = {
|
||||
import com.daml.platform.store.backend.common.ComposableQuery.SqlStringInterpolation
|
||||
val res: Seq[_] =
|
||||
SQL"""
|
||||
SELECT 1 AS dummy
|
||||
FROM participant_identity_provider_config t
|
||||
WHERE
|
||||
t.issuer = $issuer AND
|
||||
identity_provider_id != ${ignoreId.value: String}
|
||||
""".asVectorOf(IntParser)(connection)
|
||||
assert(res.length <= 1)
|
||||
res.length == 1
|
||||
}
|
||||
|
||||
override def idpConfigByIdExists(id: IdentityProviderId.Id)(connection: Connection): Boolean = {
|
||||
import com.daml.platform.store.backend.common.ComposableQuery.SqlStringInterpolation
|
||||
val res: Seq[_] =
|
||||
SQL"""
|
||||
SELECT 1 AS dummy
|
||||
FROM participant_identity_provider_config t
|
||||
WHERE t.identity_provider_id = ${id.value: String}
|
||||
""".asVectorOf(IntParser)(connection)
|
||||
assert(res.length <= 1)
|
||||
res.length == 1
|
||||
}
|
||||
|
||||
override def updateIssuer(id: IdentityProviderId.Id, newIssuer: String)(
|
||||
connection: Connection
|
||||
): Boolean = {
|
||||
val rowsUpdated =
|
||||
SQL"""
|
||||
UPDATE participant_identity_provider_config
|
||||
SET issuer = $newIssuer
|
||||
WHERE identity_provider_id = ${id.value: String}
|
||||
""".executeUpdate()(connection)
|
||||
rowsUpdated == 1
|
||||
}
|
||||
|
||||
override def updateJwksUrl(id: IdentityProviderId.Id, jwksUrl: JwksUrl)(
|
||||
connection: Connection
|
||||
): Boolean = {
|
||||
val rowsUpdated =
|
||||
SQL"""
|
||||
UPDATE participant_identity_provider_config
|
||||
SET jwks_url = ${jwksUrl.value}
|
||||
WHERE identity_provider_id = ${id.value: String}
|
||||
""".executeUpdate()(connection)
|
||||
rowsUpdated == 1
|
||||
}
|
||||
|
||||
override def updateIsDeactivated(id: IdentityProviderId.Id, isDeactivated: Boolean)(
|
||||
connection: Connection
|
||||
): Boolean = {
|
||||
val rowsUpdated =
|
||||
SQL"""
|
||||
UPDATE participant_identity_provider_config
|
||||
SET is_deactivated = $isDeactivated
|
||||
WHERE identity_provider_id = ${id.value: String}
|
||||
""".executeUpdate()(connection)
|
||||
rowsUpdated == 1
|
||||
}
|
||||
|
||||
override def countIdentityProviderConfigs()(connection: Connection): Int = {
|
||||
SQL"SELECT count(*) AS identity_provider_configs_count from participant_identity_provider_config"
|
||||
.as(SqlParser.int("identity_provider_configs_count").single)(connection)
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ object OracleResetStorageBackend extends ResetStorageBackend {
|
||||
"participant_users",
|
||||
"participant_user_rights",
|
||||
"participant_user_annotations",
|
||||
"participant_identity_provider_config",
|
||||
"transaction_metering",
|
||||
"participant_metering",
|
||||
"metering_parameters",
|
||||
|
@ -30,6 +30,7 @@ object PostgresResetStorageBackend extends ResetStorageBackend {
|
||||
truncate table participant_users cascade;
|
||||
truncate table participant_user_annotations cascade;
|
||||
truncate table participant_user_rights cascade;
|
||||
truncate table participant_identity_provider_config cascade;
|
||||
truncate table transaction_metering cascade;
|
||||
truncate table participant_metering cascade;
|
||||
truncate table metering_parameters cascade;
|
||||
|
@ -9,14 +9,14 @@ import com.daml.ledger.api.health.{HealthStatus, ReportsHealth}
|
||||
import com.daml.ledger.resources.ResourceOwner
|
||||
import com.daml.logging.LoggingContext.withEnrichedLoggingContext
|
||||
import com.daml.logging.{ContextualizedLogger, LoggingContext}
|
||||
import com.daml.metrics.{DatabaseMetrics, Metrics}
|
||||
import com.daml.metrics.api.MetricHandle.Timer
|
||||
import com.daml.metrics.api.MetricName
|
||||
import com.daml.metrics.{DatabaseMetrics, Metrics}
|
||||
import com.daml.platform.configuration.ServerRole
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder
|
||||
|
||||
import java.sql.Connection
|
||||
import java.util.concurrent.{Executor, Executors, TimeUnit}
|
||||
import com.daml.metrics.api.MetricName
|
||||
import javax.sql.DataSource
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
@ -26,6 +26,7 @@ private[platform] trait DbDispatcher {
|
||||
def executeSql[T](databaseMetrics: DatabaseMetrics)(sql: Connection => T)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[T]
|
||||
|
||||
}
|
||||
|
||||
private[dao] final class DbDispatcherImpl private[dao] (
|
||||
@ -102,6 +103,7 @@ private[dao] final class DbDispatcherImpl private[dao] (
|
||||
}
|
||||
|
||||
object DbDispatcher {
|
||||
|
||||
private val logger = ContextualizedLogger.get(this.getClass)
|
||||
|
||||
def owner(
|
||||
|
@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import com.daml.metrics.Metrics
|
||||
import com.daml.platform.store.backend.StorageBackendProvider
|
||||
import org.scalatest.freespec.AsyncFreeSpec
|
||||
|
||||
trait PersistentIdentityProviderConfigStoreTests
|
||||
extends PersistentStoreSpecBase
|
||||
with IdentityProviderConfigStoreTests {
|
||||
self: AsyncFreeSpec with StorageBackendProvider =>
|
||||
|
||||
override def newStore() = new PersistentIdentityProviderConfigStore(
|
||||
dbSupport = dbSupport,
|
||||
metrics = Metrics.ForTesting,
|
||||
maxIdentityProviderConfigs = MaxIdentityProviderConfigs,
|
||||
)
|
||||
|
||||
}
|
@ -13,9 +13,10 @@ import com.daml.platform.store.interning.MockStringInterning
|
||||
import com.daml.testing.oracle.OracleAroundAll
|
||||
import com.daml.testing.postgresql.PostgresAroundAll
|
||||
import org.scalatest.Suite
|
||||
import java.sql.Connection
|
||||
|
||||
import java.sql.Connection
|
||||
import com.daml.platform.store.backend.localstore.{
|
||||
IdentityProviderStorageBackend,
|
||||
PartyRecordStorageBackend,
|
||||
UserManagementStorageBackend,
|
||||
}
|
||||
@ -96,6 +97,7 @@ case class TestBackend(
|
||||
userManagement: UserManagementStorageBackend,
|
||||
participantPartyStorageBackend: PartyRecordStorageBackend,
|
||||
metering: TestMeteringBackend,
|
||||
identityProviderStorageBackend: IdentityProviderStorageBackend,
|
||||
)
|
||||
|
||||
case class TestMeteringBackend(
|
||||
@ -136,6 +138,8 @@ object TestBackend {
|
||||
userManagement = storageBackendFactory.createUserManagementStorageBackend,
|
||||
participantPartyStorageBackend = storageBackendFactory.createPartyRecordStorageBackend,
|
||||
metering = createTestMeteringBackend,
|
||||
identityProviderStorageBackend =
|
||||
storageBackendFactory.createIdentityProviderConfigStorageBackend,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ trait StorageBackendSuite
|
||||
with StorageBackendTestsTimestamps
|
||||
with StorageBackendTestsStringInterning
|
||||
with StorageBackendTestsUserManagement
|
||||
with StorageBackendTestsIDPConfig
|
||||
with StorageBackendTestsPartyRecord
|
||||
with StorageBackendTestsMeteringParameters
|
||||
with StorageBackendTestsWriteMetering
|
||||
|
@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.store.backend
|
||||
|
||||
import com.daml.ledger.api.domain.{IdentityProviderConfig, IdentityProviderId, JwksUrl}
|
||||
import com.daml.lf.data.Ref
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.{Inside, OptionValues}
|
||||
|
||||
import java.sql.SQLException
|
||||
import java.util.UUID
|
||||
|
||||
private[backend] trait StorageBackendTestsIDPConfig
|
||||
extends Matchers
|
||||
with Inside
|
||||
with StorageBackendSpec
|
||||
with OptionValues {
|
||||
this: AnyFlatSpec =>
|
||||
|
||||
behavior of "StorageBackend (Identity Provider Config)"
|
||||
|
||||
private def tested = backend.identityProviderStorageBackend
|
||||
|
||||
it should "create and load unchanged an identity provider config" in {
|
||||
val cfg = config()
|
||||
executeSql(tested.createIdentityProviderConfig(cfg))
|
||||
executeSql(tested.getIdentityProviderConfig(cfg.identityProviderId)) shouldBe Some(cfg)
|
||||
}
|
||||
|
||||
it should "delete an identity provider config" in {
|
||||
val cfg = config()
|
||||
executeSql(tested.createIdentityProviderConfig(cfg))
|
||||
executeSql(tested.getIdentityProviderConfig(cfg.identityProviderId)) shouldBe Some(cfg)
|
||||
executeSql(tested.deleteIdentityProviderConfig(cfg.identityProviderId))
|
||||
executeSql(tested.getIdentityProviderConfig(cfg.identityProviderId)) shouldBe None
|
||||
}
|
||||
|
||||
it should "update existing identity provider config's isDeactivated attribute" in {
|
||||
val cfg = config().copy(isDeactivated = false)
|
||||
executeSql(tested.createIdentityProviderConfig(cfg))
|
||||
// deactivate
|
||||
executeSql(tested.updateIsDeactivated(cfg.identityProviderId, true)) shouldBe true
|
||||
executeSql(
|
||||
tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
).value.isDeactivated shouldBe true
|
||||
// activate again
|
||||
executeSql(tested.updateIsDeactivated(cfg.identityProviderId, false)) shouldBe true
|
||||
executeSql(
|
||||
tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
).value.isDeactivated shouldBe false
|
||||
}
|
||||
|
||||
it should "update existing identity provider config's jwksURL attribute" in {
|
||||
val cfg = config()
|
||||
executeSql(tested.createIdentityProviderConfig(cfg))
|
||||
val newJwksUrl = JwksUrl("http://example.com/jwks2.json")
|
||||
executeSql(tested.updateJwksUrl(cfg.identityProviderId, newJwksUrl)) shouldBe true
|
||||
executeSql(
|
||||
tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
).value.jwksUrl shouldBe newJwksUrl
|
||||
}
|
||||
|
||||
it should "update existing identity provider config's issuer attribute" in {
|
||||
val cfg = config()
|
||||
executeSql(tested.createIdentityProviderConfig(cfg))
|
||||
val newIssuer = UUID.randomUUID().toString
|
||||
executeSql(tested.updateIssuer(cfg.identityProviderId, newIssuer)) shouldBe true
|
||||
executeSql(
|
||||
tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
).value.issuer shouldBe newIssuer
|
||||
}
|
||||
|
||||
it should "check if identity provider config's issuer exists" in {
|
||||
val cfg = config()
|
||||
executeSql(
|
||||
tested.identityProviderConfigByIssuerExists(cfg.identityProviderId, cfg.issuer)
|
||||
) shouldBe false
|
||||
executeSql(tested.createIdentityProviderConfig(cfg))
|
||||
executeSql(tested.identityProviderConfigByIssuerExists(randomId(), cfg.issuer)) shouldBe true
|
||||
executeSql(
|
||||
tested.identityProviderConfigByIssuerExists(cfg.identityProviderId, cfg.issuer)
|
||||
) shouldBe false
|
||||
}
|
||||
|
||||
it should "check if identity provider config by id exists" in {
|
||||
val cfg = config()
|
||||
executeSql(tested.idpConfigByIdExists(cfg.identityProviderId)) shouldBe false
|
||||
executeSql(tested.createIdentityProviderConfig(cfg))
|
||||
executeSql(tested.idpConfigByIdExists(cfg.identityProviderId)) shouldBe true
|
||||
}
|
||||
|
||||
it should "return success for no-op issuer update" in {
|
||||
val cfg = config()
|
||||
executeSql(tested.createIdentityProviderConfig(cfg))
|
||||
executeSql(tested.updateIssuer(cfg.identityProviderId, cfg.issuer)) shouldBe true
|
||||
}
|
||||
|
||||
it should "fail to update issuer for non existing identity provider config" in {
|
||||
executeSql(tested.updateIssuer(randomId(), "whatever")) shouldBe false
|
||||
executeSql(tested.updateIssuer(randomId(), "")) shouldBe false
|
||||
}
|
||||
|
||||
it should "fail to update isDeactivated for non existing identity provider config" in {
|
||||
executeSql(tested.updateIsDeactivated(randomId(), true)) shouldBe false
|
||||
executeSql(tested.updateIsDeactivated(randomId(), false)) shouldBe false
|
||||
}
|
||||
|
||||
it should "fail to update JwksUrl for non existing identity provider config" in {
|
||||
executeSql(
|
||||
tested.updateJwksUrl(randomId(), JwksUrl("http://example.com/jwks.json"))
|
||||
) shouldBe false
|
||||
executeSql(
|
||||
tested.updateJwksUrl(randomId(), JwksUrl("http://example2.com/jwks.json"))
|
||||
) shouldBe false
|
||||
}
|
||||
|
||||
it should "return success for no-op JwksUrl update" in {
|
||||
val cfg = config()
|
||||
executeSql(tested.createIdentityProviderConfig(cfg))
|
||||
executeSql(tested.updateJwksUrl(cfg.identityProviderId, cfg.jwksUrl)) shouldBe true
|
||||
}
|
||||
|
||||
it should "return success for no-op isDeactivated update" in {
|
||||
val cfg = config()
|
||||
executeSql(tested.createIdentityProviderConfig(cfg))
|
||||
executeSql(tested.updateIsDeactivated(cfg.identityProviderId, cfg.isDeactivated)) shouldBe true
|
||||
}
|
||||
|
||||
it should "fail to update identity provider config issuer attribute to non-unique issuer" in {
|
||||
val cfg1 = config()
|
||||
val cfg2 = config()
|
||||
executeSql(tested.createIdentityProviderConfig(cfg1))
|
||||
executeSql(tested.createIdentityProviderConfig(cfg2))
|
||||
assertThrows[SQLException] {
|
||||
executeSql(tested.updateIssuer(cfg1.identityProviderId, cfg2.issuer))
|
||||
}
|
||||
}
|
||||
|
||||
it should "fail to create identity provider config with non-unique issuer" in {
|
||||
val cfg1 = config()
|
||||
val cfg2 = config()
|
||||
executeSql(tested.createIdentityProviderConfig(cfg1))
|
||||
assertThrows[SQLException] {
|
||||
executeSql(tested.createIdentityProviderConfig(cfg2.copy(issuer = cfg1.issuer)))
|
||||
}
|
||||
}
|
||||
|
||||
it should "fail to create identity provider config with non-unique id" in {
|
||||
val cfg1 = config()
|
||||
val cfg2 = config()
|
||||
executeSql(tested.createIdentityProviderConfig(cfg1))
|
||||
assertThrows[SQLException] {
|
||||
executeSql(
|
||||
tested.createIdentityProviderConfig(cfg2.copy(identityProviderId = cfg1.identityProviderId))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
it should "get all identity provider configs ordered by id" in {
|
||||
val cfg1 = config().copy(identityProviderId = id("a"))
|
||||
val cfg2 = config().copy(identityProviderId = id("b"))
|
||||
val cfg3 = config().copy(identityProviderId = id("c"))
|
||||
executeSql(tested.createIdentityProviderConfig(cfg1))
|
||||
executeSql(tested.createIdentityProviderConfig(cfg2))
|
||||
executeSql(tested.createIdentityProviderConfig(cfg3))
|
||||
|
||||
executeSql(
|
||||
tested.listIdentityProviderConfigs()
|
||||
) shouldBe Vector(cfg1, cfg2, cfg3)
|
||||
}
|
||||
|
||||
private def config() = {
|
||||
IdentityProviderConfig(
|
||||
identityProviderId = randomId(),
|
||||
isDeactivated = false,
|
||||
jwksUrl = JwksUrl.assertFromString("http://example.com/jwks.json"),
|
||||
issuer = UUID.randomUUID().toString,
|
||||
)
|
||||
}
|
||||
|
||||
private def randomId() = {
|
||||
id(UUID.randomUUID().toString)
|
||||
}
|
||||
|
||||
private def id(str: String) = IdentityProviderId.Id(Ref.LedgerString.assertFromString(str))
|
||||
|
||||
}
|
@ -13,7 +13,7 @@ class DbDispatcherLeftOpsSpec extends AnyFreeSpec with MockitoSugar with Matcher
|
||||
|
||||
"rollbackOnLeft should rollback on left" in {
|
||||
val conn = mock[Connection]
|
||||
DbDispatcherLeftOps
|
||||
Ops
|
||||
.rollbackOnLeft(_ => Left(""))(conn) shouldBe Left("")
|
||||
|
||||
verify(conn, times(1)).rollback()
|
||||
@ -22,7 +22,7 @@ class DbDispatcherLeftOpsSpec extends AnyFreeSpec with MockitoSugar with Matcher
|
||||
|
||||
"rollbackOnLeft should not rollback on right" in {
|
||||
val conn = mock[Connection]
|
||||
DbDispatcherLeftOps
|
||||
Ops
|
||||
.rollbackOnLeft(_ => Right(""))(conn) shouldBe Right("")
|
||||
|
||||
verifyZeroInteractions(conn)
|
||||
|
@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import com.daml.platform.store.backend.StorageBackendProviderH2
|
||||
import org.scalatest.freespec.AsyncFreeSpec
|
||||
|
||||
class PersistentIdentityProviderConfigStoreH2Spec
|
||||
extends AsyncFreeSpec
|
||||
with PersistentIdentityProviderConfigStoreTests
|
||||
with StorageBackendProviderH2
|
@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import com.daml.platform.store.backend.StorageBackendProviderOracle
|
||||
import org.scalatest.freespec.AsyncFreeSpec
|
||||
|
||||
class PersistentIdentityProviderConfigStoreOracleSpec
|
||||
extends AsyncFreeSpec
|
||||
with PersistentIdentityProviderConfigStoreTests
|
||||
with StorageBackendProviderOracle
|
@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import com.daml.platform.store.backend.StorageBackendProviderPostgres
|
||||
import org.scalatest.freespec.AsyncFreeSpec
|
||||
|
||||
class PersistentIdentityProviderConfigStorePostgresSpec
|
||||
extends AsyncFreeSpec
|
||||
with PersistentIdentityProviderConfigStoreTests
|
||||
with StorageBackendProviderPostgres
|
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import com.daml.caching.CaffeineCache
|
||||
import com.daml.caching.CaffeineCache.FutureAsyncCacheLoader
|
||||
import com.daml.ledger.api.domain.{IdentityProviderConfig, IdentityProviderId}
|
||||
import com.daml.logging.LoggingContext
|
||||
import com.daml.metrics.Metrics
|
||||
import com.daml.platform.localstore.api.IdentityProviderConfigStore.Result
|
||||
import com.daml.platform.localstore.api.{IdentityProviderConfigStore, IdentityProviderConfigUpdate}
|
||||
import com.github.benmanes.caffeine.{cache => caffeine}
|
||||
|
||||
import java.time.Duration
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Success, Try}
|
||||
|
||||
class CachedIdentityProviderConfigStore(
|
||||
delegate: IdentityProviderConfigStore,
|
||||
expiryAfterWriteInSeconds: Int,
|
||||
maximumCacheSize: Int,
|
||||
metrics: Metrics,
|
||||
)(implicit val executionContext: ExecutionContext, loggingContext: LoggingContext)
|
||||
extends IdentityProviderConfigStore {
|
||||
|
||||
private val idpCache: CaffeineCache.AsyncLoadingCaffeineCache[
|
||||
IdentityProviderId.Id,
|
||||
Result[IdentityProviderConfig],
|
||||
] =
|
||||
new CaffeineCache.AsyncLoadingCaffeineCache(
|
||||
caffeine.Caffeine
|
||||
.newBuilder()
|
||||
.expireAfterWrite(Duration.ofSeconds(expiryAfterWriteInSeconds.toLong))
|
||||
.maximumSize(maximumCacheSize.toLong)
|
||||
.buildAsync(
|
||||
new FutureAsyncCacheLoader[IdentityProviderId.Id, Result[IdentityProviderConfig]](
|
||||
delegate.getIdentityProviderConfig
|
||||
)
|
||||
),
|
||||
metrics.daml.identityProviderConfigStore.cache,
|
||||
)
|
||||
|
||||
override def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[IdentityProviderConfig]] =
|
||||
delegate
|
||||
.createIdentityProviderConfig(identityProviderConfig)
|
||||
.andThen(invalidateOnSuccess(identityProviderConfig.identityProviderId))
|
||||
|
||||
override def getIdentityProviderConfig(id: IdentityProviderId.Id)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[IdentityProviderConfig]] = idpCache.get(id)
|
||||
|
||||
override def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[Unit]] =
|
||||
delegate.deleteIdentityProviderConfig(id).andThen(invalidateOnSuccess(id))
|
||||
|
||||
override def listIdentityProviderConfigs()(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[Seq[IdentityProviderConfig]]] = delegate.listIdentityProviderConfigs()
|
||||
|
||||
override def updateIdentityProviderConfig(update: IdentityProviderConfigUpdate)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[IdentityProviderConfig]] = delegate
|
||||
.updateIdentityProviderConfig(update)
|
||||
.andThen(invalidateOnSuccess(update.identityProviderId))
|
||||
|
||||
private def invalidateOnSuccess(
|
||||
id: IdentityProviderId.Id
|
||||
): PartialFunction[Try[Result[Any]], Unit] = { case Success(Right(_)) =>
|
||||
idpCache.invalidate(id)
|
||||
}
|
||||
}
|
@ -3,10 +3,8 @@
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.{CompletableFuture, Executor}
|
||||
|
||||
import com.daml.caching.CaffeineCache
|
||||
import com.daml.caching.CaffeineCache.FutureAsyncCacheLoader
|
||||
import com.daml.ledger.api.domain
|
||||
import com.daml.ledger.api.domain.User
|
||||
import com.daml.lf.data.Ref
|
||||
@ -15,11 +13,11 @@ import com.daml.logging.LoggingContext
|
||||
import com.daml.metrics.Metrics
|
||||
import com.daml.platform.localstore.api.UserManagementStore.{Result, UserInfo}
|
||||
import com.daml.platform.localstore.api.{UserManagementStore, UserUpdate}
|
||||
import com.github.benmanes.caffeine.cache.AsyncCacheLoader
|
||||
import com.github.benmanes.caffeine.{cache => caffeine}
|
||||
|
||||
import java.time.Duration
|
||||
import scala.concurrent.{ExecutionContext, Future}
|
||||
import scala.util.{Failure, Success, Try}
|
||||
import scala.util.{Success, Try}
|
||||
|
||||
class CachedUserManagementStore(
|
||||
delegate: UserManagementStore,
|
||||
@ -36,19 +34,7 @@ class CachedUserManagementStore(
|
||||
.expireAfterWrite(Duration.ofSeconds(expiryAfterWriteInSeconds.toLong))
|
||||
.maximumSize(maximumCacheSize.toLong)
|
||||
.buildAsync(
|
||||
new AsyncCacheLoader[UserId, Result[UserInfo]] {
|
||||
override def asyncLoad(
|
||||
key: UserId,
|
||||
executor: Executor,
|
||||
): CompletableFuture[Result[UserInfo]] = {
|
||||
val cf = new CompletableFuture[Result[UserInfo]]
|
||||
delegate.getUserInfo(key).onComplete {
|
||||
case Success(value) => cf.complete(value)
|
||||
case Failure(e) => cf.completeExceptionally(e)
|
||||
}
|
||||
cf
|
||||
}
|
||||
}
|
||||
new FutureAsyncCacheLoader[UserId, Result[UserInfo]](key => delegate.getUserInfo(key))
|
||||
),
|
||||
metrics.daml.userManagement.cache,
|
||||
)
|
||||
|
@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import com.daml.ledger.api.domain
|
||||
import com.daml.ledger.api.domain.{IdentityProviderConfig, IdentityProviderId}
|
||||
import com.daml.logging.LoggingContext
|
||||
import com.daml.platform.localstore.api.IdentityProviderConfigStore._
|
||||
import com.daml.platform.localstore.api.{IdentityProviderConfigStore, IdentityProviderConfigUpdate}
|
||||
|
||||
import scala.collection.concurrent.TrieMap
|
||||
import scala.concurrent.Future
|
||||
|
||||
class InMemoryIdentityProviderConfigStore(maxIdentityProviderConfigs: Int = 10)
|
||||
extends IdentityProviderConfigStore {
|
||||
private val state: TrieMap[IdentityProviderId.Id, IdentityProviderConfig] =
|
||||
TrieMap[IdentityProviderId.Id, IdentityProviderConfig]()
|
||||
|
||||
override def createIdentityProviderConfig(identityProviderConfig: domain.IdentityProviderConfig)(
|
||||
implicit loggingContext: LoggingContext
|
||||
): Future[Result[domain.IdentityProviderConfig]] = withState {
|
||||
for {
|
||||
_ <- checkIssuerDoNotExists(
|
||||
identityProviderConfig.issuer,
|
||||
identityProviderConfig.identityProviderId,
|
||||
)
|
||||
_ <- checkIdDoNotExists(identityProviderConfig.identityProviderId)
|
||||
_ <- tooManyIdentityProviderConfigs()
|
||||
_ = state.put(identityProviderConfig.identityProviderId, identityProviderConfig)
|
||||
} yield identityProviderConfig
|
||||
}
|
||||
|
||||
override def getIdentityProviderConfig(id: IdentityProviderId.Id)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[domain.IdentityProviderConfig]] = withState {
|
||||
state.get(id).toRight(IdentityProviderConfigNotFound(id))
|
||||
}
|
||||
|
||||
override def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[Unit]] = withState {
|
||||
for {
|
||||
_ <- checkIdExists(id)
|
||||
} yield {
|
||||
state.remove(id)
|
||||
()
|
||||
}
|
||||
}
|
||||
|
||||
override def listIdentityProviderConfigs()(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[Seq[domain.IdentityProviderConfig]]] = withState {
|
||||
Right(state.values.toSeq)
|
||||
}
|
||||
|
||||
override def updateIdentityProviderConfig(update: IdentityProviderConfigUpdate)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[IdentityProviderConfig]] = withState {
|
||||
val id = update.identityProviderId
|
||||
for {
|
||||
currentState <- checkIdExists(id)
|
||||
_ <- update.issuerUpdate
|
||||
.map(checkIssuerDoNotExists(_, update.identityProviderId))
|
||||
.getOrElse(Right(()))
|
||||
} yield {
|
||||
val updatedValue = currentState
|
||||
.copy(isDeactivated = update.isDeactivatedUpdate.getOrElse(currentState.isDeactivated))
|
||||
.copy(issuer = update.issuerUpdate.getOrElse(currentState.issuer))
|
||||
.copy(jwksUrl = update.jwksUrlUpdate.getOrElse(currentState.jwksUrl))
|
||||
state.put(update.identityProviderId, updatedValue)
|
||||
updatedValue
|
||||
}
|
||||
}
|
||||
|
||||
private def checkIssuerDoNotExists(
|
||||
issuer: String,
|
||||
idToIgnore: IdentityProviderId.Id,
|
||||
): Result[Unit] =
|
||||
Either.cond(
|
||||
!state.values.exists(cfg => cfg.issuer == issuer && cfg.identityProviderId != idToIgnore),
|
||||
(),
|
||||
IdentityProviderConfigWithIssuerExists(issuer),
|
||||
)
|
||||
|
||||
private def checkIdDoNotExists(id: IdentityProviderId.Id): Result[Unit] =
|
||||
Either.cond(
|
||||
!state.isDefinedAt(id),
|
||||
(),
|
||||
IdentityProviderConfigExists(id),
|
||||
)
|
||||
|
||||
private def tooManyIdentityProviderConfigs(): Result[Unit] = {
|
||||
Either.cond(
|
||||
state.size + 1 <= maxIdentityProviderConfigs,
|
||||
(),
|
||||
TooManyIdentityProviderConfigs(),
|
||||
)
|
||||
}
|
||||
|
||||
private def checkIdExists(id: IdentityProviderId.Id): Result[IdentityProviderConfig] =
|
||||
state.get(id).toRight(IdentityProviderConfigNotFound(id))
|
||||
|
||||
private def withState[T](t: => T): Future[T] =
|
||||
state.synchronized(
|
||||
Future.successful(t)
|
||||
)
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore.api
|
||||
|
||||
import com.daml.ledger.api.domain.{IdentityProviderConfig, IdentityProviderId}
|
||||
import com.daml.logging.LoggingContext
|
||||
import com.daml.platform.localstore.api.IdentityProviderConfigStore.Result
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
trait IdentityProviderConfigStore {
|
||||
|
||||
def createIdentityProviderConfig(identityProviderConfig: IdentityProviderConfig)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[IdentityProviderConfig]]
|
||||
|
||||
def getIdentityProviderConfig(id: IdentityProviderId.Id)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[IdentityProviderConfig]]
|
||||
|
||||
def deleteIdentityProviderConfig(id: IdentityProviderId.Id)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[Unit]]
|
||||
|
||||
def listIdentityProviderConfigs()(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[Seq[IdentityProviderConfig]]]
|
||||
|
||||
def updateIdentityProviderConfig(update: IdentityProviderConfigUpdate)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[IdentityProviderConfig]]
|
||||
|
||||
}
|
||||
object IdentityProviderConfigStore {
|
||||
type Result[T] = Either[Error, T]
|
||||
|
||||
sealed trait Error
|
||||
final case class IdentityProviderConfigNotFound(identityProviderId: IdentityProviderId.Id)
|
||||
extends Error
|
||||
final case class IdentityProviderConfigExists(identityProviderId: IdentityProviderId.Id)
|
||||
extends Error
|
||||
final case class IdentityProviderConfigWithIssuerExists(issuer: String) extends Error
|
||||
final case class TooManyIdentityProviderConfigs() extends Error
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore.api
|
||||
|
||||
import com.daml.ledger.api.domain.{IdentityProviderId, JwksUrl}
|
||||
|
||||
case class IdentityProviderConfigUpdate(
|
||||
identityProviderId: IdentityProviderId.Id,
|
||||
isDeactivatedUpdate: Option[Boolean] = None,
|
||||
jwksUrlUpdate: Option[JwksUrl] = None,
|
||||
issuerUpdate: Option[String] = None,
|
||||
)
|
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import com.daml.ledger.resources.TestResourceContext
|
||||
import com.daml.platform.localstore.api.IdentityProviderConfigStore
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
import org.scalatest.{Assertion, AsyncTestSuite, EitherValues, OptionValues}
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
trait IdentityProviderConfigStoreSpecBase
|
||||
extends TestResourceContext
|
||||
with Matchers
|
||||
with OptionValues
|
||||
with EitherValues { self: AsyncTestSuite =>
|
||||
|
||||
def newStore(): IdentityProviderConfigStore
|
||||
|
||||
final protected def testIt(
|
||||
f: IdentityProviderConfigStore => Future[Assertion]
|
||||
): Future[Assertion] = f(newStore())
|
||||
|
||||
}
|
@ -0,0 +1,317 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import com.daml.ledger.api.domain.{IdentityProviderConfig, IdentityProviderId, JwksUrl}
|
||||
import com.daml.lf.data.Ref
|
||||
import com.daml.logging.LoggingContext
|
||||
import com.daml.platform.localstore.api.IdentityProviderConfigStore.{
|
||||
IdentityProviderConfigExists,
|
||||
IdentityProviderConfigNotFound,
|
||||
IdentityProviderConfigWithIssuerExists,
|
||||
TooManyIdentityProviderConfigs,
|
||||
}
|
||||
import com.daml.platform.localstore.api.IdentityProviderConfigUpdate
|
||||
import org.scalatest.freespec.AsyncFreeSpec
|
||||
|
||||
import java.util.UUID
|
||||
import scala.concurrent.Future
|
||||
|
||||
trait IdentityProviderConfigStoreTests extends IdentityProviderConfigStoreSpecBase {
|
||||
self: AsyncFreeSpec =>
|
||||
implicit val lc: LoggingContext = LoggingContext.ForTesting
|
||||
|
||||
val MaxIdentityProviderConfigs = 10
|
||||
|
||||
def config(): IdentityProviderConfig =
|
||||
IdentityProviderConfig(
|
||||
identityProviderId = randomId(),
|
||||
isDeactivated = false,
|
||||
jwksUrl = JwksUrl.assertFromString("http://example.com/jwks.json"),
|
||||
issuer = UUID.randomUUID().toString,
|
||||
)
|
||||
|
||||
def randomId() = {
|
||||
val id = UUID.randomUUID().toString
|
||||
IdentityProviderId.Id(Ref.LedgerString.assertFromString(id))
|
||||
}
|
||||
|
||||
"identity provider config store" - {
|
||||
"allows to create and load unchanged an identity provider config" in {
|
||||
testIt { tested =>
|
||||
val cfg1 = config()
|
||||
for {
|
||||
res1 <- tested.createIdentityProviderConfig(cfg1)
|
||||
} yield {
|
||||
res1 shouldBe Right(cfg1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"disallow to create identity provider config with non unique id" in {
|
||||
val id = randomId()
|
||||
testIt { tested =>
|
||||
val cfg1 = config().copy(identityProviderId = id)
|
||||
val cfg2 = config().copy(identityProviderId = id)
|
||||
for {
|
||||
res1 <- tested.createIdentityProviderConfig(cfg1)
|
||||
res2 <- tested.createIdentityProviderConfig(cfg2)
|
||||
} yield {
|
||||
res1 shouldBe Right(cfg1)
|
||||
res2 shouldBe Left(IdentityProviderConfigExists(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"disallow to create identity provider config with non unique issuer" in {
|
||||
testIt { tested =>
|
||||
val cfg1 = config().copy(issuer = "issuer1")
|
||||
val cfg2 = config().copy(issuer = "issuer1")
|
||||
for {
|
||||
res1 <- tested.createIdentityProviderConfig(cfg1)
|
||||
res2 <- tested.createIdentityProviderConfig(cfg2)
|
||||
} yield {
|
||||
res1 shouldBe Right(cfg1)
|
||||
res2 shouldBe Left(IdentityProviderConfigWithIssuerExists("issuer1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s"disallow to create more than $MaxIdentityProviderConfigs configs" in {
|
||||
testIt { tested =>
|
||||
for {
|
||||
res1 <- Future.sequence(
|
||||
(1 to MaxIdentityProviderConfigs).map(_ =>
|
||||
tested.createIdentityProviderConfig(config())
|
||||
)
|
||||
)
|
||||
last = config()
|
||||
res2 <- tested.createIdentityProviderConfig(last)
|
||||
res3 <- tested.getIdentityProviderConfig(last.identityProviderId)
|
||||
} yield {
|
||||
res1.forall(_.isRight) shouldBe true
|
||||
res2 shouldBe Left(TooManyIdentityProviderConfigs())
|
||||
// check res3 has not been created
|
||||
res3 shouldBe Left(IdentityProviderConfigNotFound(last.identityProviderId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"allows to delete an identity provider config" in {
|
||||
testIt { tested =>
|
||||
val cfg = config()
|
||||
for {
|
||||
res1 <- tested.createIdentityProviderConfig(cfg)
|
||||
res2 <- tested.deleteIdentityProviderConfig(cfg.identityProviderId)
|
||||
res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
} yield {
|
||||
res1 shouldBe Right(cfg)
|
||||
res2 shouldBe Right(())
|
||||
res3 shouldBe Left(IdentityProviderConfigNotFound(cfg.identityProviderId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"allow to get identity provider config by id" in {
|
||||
testIt { tested =>
|
||||
val cfg = config()
|
||||
val id = randomId()
|
||||
for {
|
||||
res1 <- tested.createIdentityProviderConfig(cfg)
|
||||
res2 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
res3 <- tested.getIdentityProviderConfig(id)
|
||||
} yield {
|
||||
res1 shouldBe Right(cfg)
|
||||
res2 shouldBe Right(cfg)
|
||||
res3 shouldBe Left(IdentityProviderConfigNotFound(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"fail to delete non-existing identity provider config" in {
|
||||
val id = randomId()
|
||||
testIt { tested =>
|
||||
for {
|
||||
res <- tested.deleteIdentityProviderConfig(id)
|
||||
} yield {
|
||||
res shouldBe Left(IdentityProviderConfigNotFound(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"allows to update nothing" in {
|
||||
testIt { tested =>
|
||||
val cfg = config()
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg)
|
||||
res <- tested.updateIdentityProviderConfig(
|
||||
IdentityProviderConfigUpdate(
|
||||
identityProviderId = cfg.identityProviderId
|
||||
)
|
||||
)
|
||||
} yield {
|
||||
res shouldBe Right(cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"fail to update non existing config" in {
|
||||
val id = randomId()
|
||||
testIt { tested =>
|
||||
for {
|
||||
res <- tested.updateIdentityProviderConfig(
|
||||
IdentityProviderConfigUpdate(
|
||||
identityProviderId = id
|
||||
)
|
||||
)
|
||||
} yield {
|
||||
res shouldBe Left(IdentityProviderConfigNotFound(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"allows to update existing identity provider config's isDeactivated attribute" in {
|
||||
testIt { tested =>
|
||||
val cfg = config().copy(isDeactivated = false)
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg)
|
||||
res2 <- tested.updateIdentityProviderConfig(
|
||||
IdentityProviderConfigUpdate(
|
||||
identityProviderId = cfg.identityProviderId,
|
||||
isDeactivatedUpdate = Some(true),
|
||||
)
|
||||
)
|
||||
res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
} yield {
|
||||
res2 shouldBe Right(cfg.copy(isDeactivated = true))
|
||||
res3 shouldBe Right(cfg.copy(isDeactivated = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"allows to update existing identity provider config's jwksUrl attribute" in {
|
||||
testIt { tested =>
|
||||
val cfg = config().copy(jwksUrl = JwksUrl.assertFromString("http://daml.com/jwks1.json"))
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg)
|
||||
res2 <- tested.updateIdentityProviderConfig(
|
||||
IdentityProviderConfigUpdate(
|
||||
identityProviderId = cfg.identityProviderId,
|
||||
jwksUrlUpdate = Some(JwksUrl.assertFromString("http://daml.com/jwks2.json")),
|
||||
)
|
||||
)
|
||||
res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
} yield {
|
||||
val expected = cfg.copy(jwksUrl = JwksUrl.assertFromString("http://daml.com/jwks2.json"))
|
||||
res2 shouldBe Right(expected)
|
||||
res3 shouldBe Right(expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"allows to update existing identity provider config's issuer attribute" in {
|
||||
testIt { tested =>
|
||||
val cfg = config().copy(issuer = "issuer1")
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg)
|
||||
res2 <- tested.updateIdentityProviderConfig(
|
||||
IdentityProviderConfigUpdate(
|
||||
identityProviderId = cfg.identityProviderId,
|
||||
issuerUpdate = Some("issuer2"),
|
||||
)
|
||||
)
|
||||
res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
} yield {
|
||||
res2 shouldBe Right(cfg.copy(issuer = "issuer2"))
|
||||
res3 shouldBe Right(cfg.copy(issuer = "issuer2"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"allows to update existing identity provider config's issuer attribute to the same value" in {
|
||||
testIt { tested =>
|
||||
val cfg = config().copy(issuer = "issuer1")
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg)
|
||||
res2 <- tested.updateIdentityProviderConfig(
|
||||
IdentityProviderConfigUpdate(
|
||||
identityProviderId = cfg.identityProviderId,
|
||||
issuerUpdate = Some("issuer1"),
|
||||
)
|
||||
)
|
||||
res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
} yield {
|
||||
res2 shouldBe Right(cfg.copy(issuer = "issuer1"))
|
||||
res3 shouldBe Right(cfg.copy(issuer = "issuer1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"allows to update everything at the same time" in {
|
||||
testIt { tested =>
|
||||
val id = randomId()
|
||||
val cfg = IdentityProviderConfig(
|
||||
identityProviderId = id,
|
||||
isDeactivated = false,
|
||||
jwksUrl = JwksUrl.assertFromString("http://example.com/jwks.json"),
|
||||
issuer = UUID.randomUUID().toString,
|
||||
)
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg)
|
||||
res <- tested.updateIdentityProviderConfig(
|
||||
IdentityProviderConfigUpdate(
|
||||
identityProviderId = cfg.identityProviderId,
|
||||
issuerUpdate = Some("issuer2"),
|
||||
jwksUrlUpdate = Some(JwksUrl.assertFromString("http://daml.com/jwks2.json")),
|
||||
isDeactivatedUpdate = Some(true),
|
||||
)
|
||||
)
|
||||
res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
} yield {
|
||||
val expected = IdentityProviderConfig(
|
||||
identityProviderId = id,
|
||||
isDeactivated = true,
|
||||
jwksUrl = JwksUrl.assertFromString("http://daml.com/jwks2.json"),
|
||||
issuer = "issuer2",
|
||||
)
|
||||
res shouldBe Right(expected)
|
||||
res3 shouldBe Right(expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"disallow updating issuer to non-unique value" in {
|
||||
testIt { tested =>
|
||||
val cfg1 = config().copy(issuer = "issuer1")
|
||||
val cfg2 = config().copy(issuer = "issuer2")
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg1)
|
||||
_ <- tested.createIdentityProviderConfig(cfg2)
|
||||
res <- tested.updateIdentityProviderConfig(
|
||||
IdentityProviderConfigUpdate(
|
||||
identityProviderId = cfg1.identityProviderId,
|
||||
issuerUpdate = Some("issuer2"),
|
||||
)
|
||||
)
|
||||
} yield {
|
||||
res shouldBe Left(IdentityProviderConfigWithIssuerExists("issuer2"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"allow listing all configs" in {
|
||||
testIt { tested =>
|
||||
val cfg1 = config().copy(issuer = "issuer1")
|
||||
val cfg2 = config().copy(issuer = "issuer2")
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg1)
|
||||
_ <- tested.createIdentityProviderConfig(cfg2)
|
||||
res <- tested.listIdentityProviderConfigs()
|
||||
} yield {
|
||||
res.value should contain theSameElementsAs Vector(cfg1, cfg2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import com.daml.metrics.Metrics
|
||||
import com.daml.platform.localstore.api.{IdentityProviderConfigStore, IdentityProviderConfigUpdate}
|
||||
import com.daml.platform.localstore.api.IdentityProviderConfigStore.IdentityProviderConfigNotFound
|
||||
import org.mockito.{ArgumentMatchersSugar, MockitoSugar}
|
||||
import org.scalatest.freespec.AsyncFreeSpec
|
||||
|
||||
class CachedIdentityProviderConfigStoreSpec
|
||||
extends AsyncFreeSpec
|
||||
with IdentityProviderConfigStoreTests
|
||||
with MockitoSugar
|
||||
with ArgumentMatchersSugar {
|
||||
|
||||
override def newStore(): IdentityProviderConfigStore = new CachedIdentityProviderConfigStore(
|
||||
new InMemoryIdentityProviderConfigStore(),
|
||||
1,
|
||||
10,
|
||||
Metrics.ForTesting,
|
||||
)
|
||||
|
||||
private def createTested(
|
||||
delegate: IdentityProviderConfigStore
|
||||
): CachedIdentityProviderConfigStore =
|
||||
new CachedIdentityProviderConfigStore(
|
||||
delegate,
|
||||
expiryAfterWriteInSeconds = 1,
|
||||
maximumCacheSize = 10,
|
||||
Metrics.ForTesting,
|
||||
)
|
||||
|
||||
"test identity-provider-config cache result gets invalidated after new config creation" in {
|
||||
val delegate = spy(new InMemoryIdentityProviderConfigStore())
|
||||
val tested = createTested(delegate)
|
||||
val cfg = config()
|
||||
for {
|
||||
getYetNonExistent <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
_ <- tested.createIdentityProviderConfig(cfg)
|
||||
get <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
} yield {
|
||||
getYetNonExistent shouldBe Left(IdentityProviderConfigNotFound(cfg.identityProviderId))
|
||||
get.value shouldBe cfg
|
||||
}
|
||||
}
|
||||
|
||||
"test cache population" in {
|
||||
val delegate = spy(new InMemoryIdentityProviderConfigStore())
|
||||
val tested = createTested(delegate)
|
||||
val cfg = config()
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg)
|
||||
res1 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
res2 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
res4 <- tested.listIdentityProviderConfigs()
|
||||
res5 <- tested.listIdentityProviderConfigs()
|
||||
} yield {
|
||||
verify(delegate, times(1)).getIdentityProviderConfig(cfg.identityProviderId)
|
||||
verify(delegate, times(2)).listIdentityProviderConfigs()
|
||||
res1.value shouldBe cfg
|
||||
res2.value shouldBe cfg
|
||||
res3.value shouldBe cfg
|
||||
res4.value shouldBe Vector(cfg)
|
||||
res5.value shouldBe Vector(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
"test cache invalidation after every write method" in {
|
||||
val delegate = spy(new InMemoryIdentityProviderConfigStore())
|
||||
val tested = createTested(delegate)
|
||||
val cfg = config()
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg)
|
||||
res1 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
res2 <- tested.updateIdentityProviderConfig(
|
||||
IdentityProviderConfigUpdate(
|
||||
cfg.identityProviderId,
|
||||
isDeactivatedUpdate = Some(true),
|
||||
)
|
||||
)
|
||||
res3 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
res4 <- tested.deleteIdentityProviderConfig(cfg.identityProviderId)
|
||||
res5 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
} yield {
|
||||
val order = inOrder(delegate)
|
||||
order.verify(delegate, times(1)).createIdentityProviderConfig(cfg)
|
||||
order.verify(delegate, times(1)).getIdentityProviderConfig(cfg.identityProviderId)
|
||||
order
|
||||
.verify(delegate, times(1))
|
||||
.updateIdentityProviderConfig(
|
||||
IdentityProviderConfigUpdate(
|
||||
cfg.identityProviderId,
|
||||
isDeactivatedUpdate = Some(true),
|
||||
)
|
||||
)
|
||||
order.verify(delegate, times(1)).getIdentityProviderConfig(cfg.identityProviderId)
|
||||
order.verify(delegate, times(1)).deleteIdentityProviderConfig(cfg.identityProviderId)
|
||||
order.verify(delegate, times(1)).getIdentityProviderConfig(cfg.identityProviderId)
|
||||
order.verifyNoMoreInteractions()
|
||||
res1.value shouldBe cfg
|
||||
res2.value shouldBe cfg.copy(isDeactivated = true)
|
||||
res3.value shouldBe cfg.copy(isDeactivated = true)
|
||||
res4.value shouldBe ()
|
||||
res5 shouldBe Left(IdentityProviderConfigNotFound(cfg.identityProviderId))
|
||||
}
|
||||
}
|
||||
|
||||
"listing all users should not be cached" in {
|
||||
val delegate = spy(new InMemoryIdentityProviderConfigStore())
|
||||
val tested = createTested(delegate)
|
||||
val cfg1 = config()
|
||||
val cfg2 = config()
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg1)
|
||||
_ <- tested.createIdentityProviderConfig(cfg2)
|
||||
res1 <- tested.listIdentityProviderConfigs()
|
||||
res2 <- tested.listIdentityProviderConfigs()
|
||||
res3 <- tested.listIdentityProviderConfigs()
|
||||
} yield {
|
||||
verify(delegate, times(3)).listIdentityProviderConfigs()
|
||||
res1.value should contain theSameElementsAs Vector(cfg1, cfg2)
|
||||
res2.value should contain theSameElementsAs Vector(cfg1, cfg2)
|
||||
res3.value should contain theSameElementsAs Vector(cfg1, cfg2)
|
||||
}
|
||||
}
|
||||
|
||||
"cache entries expire after a set time" in {
|
||||
val delegate = spy(new InMemoryIdentityProviderConfigStore())
|
||||
val tested = createTested(delegate)
|
||||
val cfg = config()
|
||||
for {
|
||||
_ <- tested.createIdentityProviderConfig(cfg)
|
||||
res1 <- tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
res2 <- tested.listIdentityProviderConfigs()
|
||||
|
||||
res3 <- {
|
||||
Thread.sleep(2000)
|
||||
tested.getIdentityProviderConfig(cfg.identityProviderId)
|
||||
}
|
||||
res4 <- tested.listIdentityProviderConfigs()
|
||||
} yield {
|
||||
verify(delegate, times(2)).getIdentityProviderConfig(cfg.identityProviderId)
|
||||
verify(delegate, times(2)).listIdentityProviderConfigs()
|
||||
res1.value shouldBe cfg
|
||||
res2.value shouldBe Vector(cfg)
|
||||
res3.value shouldBe cfg
|
||||
res4.value shouldBe Vector(cfg)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.daml.platform.localstore
|
||||
|
||||
import org.scalatest.freespec.AsyncFreeSpec
|
||||
|
||||
class InMemoryIdentityProviderConfigStoreSpec
|
||||
extends AsyncFreeSpec
|
||||
with IdentityProviderConfigStoreTests {
|
||||
|
||||
override def newStore() = new InMemoryIdentityProviderConfigStore(MaxIdentityProviderConfigs)
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user