IdentityProviderConfig persistence layer [DPP-1331] (#15634)

This commit is contained in:
Sergey Kisel 2022-11-24 13:32:42 +01:00 committed by GitHub
parent fd8439144a
commit 7760ec9fb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1659 additions and 27 deletions

View File

@ -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
}
}
}

View File

@ -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,
)
}

View File

@ -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 {}

View File

@ -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")
}

View File

@ -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)

View File

@ -1 +1 @@
a03ea7d689a0f2babb56fe2fce7cc11d853c27e587e934d8c2d51c3a9837b38e
b17769a61b71fedc597a1c55896fc5af10c97801dac4e7b9c20feebece7f39ea

View File

@ -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
---------------------------------------------------------------------------------------------------

View File

@ -0,0 +1 @@
1358474f56f8c553bf071b3790aa691c9a2e6d6f013f7898fa16349e9c879cc0

View File

@ -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
);

View File

@ -0,0 +1 @@
7cd4dbb7ff7886500a11a41dd7b248d0af9acb2ba51e81f69258a95267a08e19

View File

@ -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
);

View File

@ -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

View File

@ -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,
)
}

View File

@ -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

View File

@ -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 =

View File

@ -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;

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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",

View File

@ -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;

View File

@ -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(

View File

@ -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,
)
}

View File

@ -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,
)
}

View File

@ -19,6 +19,7 @@ trait StorageBackendSuite
with StorageBackendTestsTimestamps
with StorageBackendTestsStringInterning
with StorageBackendTestsUserManagement
with StorageBackendTestsIDPConfig
with StorageBackendTestsPartyRecord
with StorageBackendTestsMeteringParameters
with StorageBackendTestsWriteMetering

View File

@ -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))
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
}
}

View File

@ -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,
)

View File

@ -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)
)
}

View File

@ -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
}

View File

@ -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,
)

View File

@ -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())
}

View File

@ -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)
}
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}