mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 09:17:43 +03:00
Implement IDP ID check transactionally within persistence layer [DPP-1386] (#16134)
This commit is contained in:
parent
232774a3bf
commit
efcc6620ed
@ -462,14 +462,18 @@ class IdeLedgerClient(
|
||||
esf: ExecutionSequencerFactory,
|
||||
mat: Materializer,
|
||||
): Future[Option[User]] =
|
||||
userManagementStore.getUser(id)(LoggingContext.empty).map(_.toOption)
|
||||
userManagementStore
|
||||
.getUser(id, IdentityProviderId.Default)(LoggingContext.empty)
|
||||
.map(_.toOption)
|
||||
|
||||
override def deleteUser(id: UserId)(implicit
|
||||
ec: ExecutionContext,
|
||||
esf: ExecutionSequencerFactory,
|
||||
mat: Materializer,
|
||||
): Future[Option[Unit]] =
|
||||
userManagementStore.deleteUser(id)(LoggingContext.empty).map(_.toOption)
|
||||
userManagementStore
|
||||
.deleteUser(id, IdentityProviderId.Default)(LoggingContext.empty)
|
||||
.map(_.toOption)
|
||||
|
||||
override def listAllUsers()(implicit
|
||||
ec: ExecutionContext,
|
||||
@ -487,7 +491,7 @@ class IdeLedgerClient(
|
||||
mat: Materializer,
|
||||
): Future[Option[List[UserRight]]] =
|
||||
userManagementStore
|
||||
.grantRights(id, rights.toSet)(LoggingContext.empty)
|
||||
.grantRights(id, rights.toSet, IdentityProviderId.Default)(LoggingContext.empty)
|
||||
.map(_.toOption.map(_.toList))
|
||||
|
||||
override def revokeUserRights(
|
||||
@ -499,7 +503,7 @@ class IdeLedgerClient(
|
||||
mat: Materializer,
|
||||
): Future[Option[List[UserRight]]] =
|
||||
userManagementStore
|
||||
.revokeRights(id, rights.toSet)(LoggingContext.empty)
|
||||
.revokeRights(id, rights.toSet, IdentityProviderId.Default)(LoggingContext.empty)
|
||||
.map(_.toOption.map(_.toList))
|
||||
|
||||
override def listUserRights(id: UserId)(implicit
|
||||
@ -507,5 +511,7 @@ class IdeLedgerClient(
|
||||
esf: ExecutionSequencerFactory,
|
||||
mat: Materializer,
|
||||
): Future[Option[List[UserRight]]] =
|
||||
userManagementStore.listUserRights(id)(LoggingContext.empty).map(_.toOption.map(_.toList))
|
||||
userManagementStore
|
||||
.listUserRights(id, IdentityProviderId.Default)(LoggingContext.empty)
|
||||
.map(_.toOption.map(_.toList))
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ private[auth] final class UserRightsChangeAsyncChecker(
|
||||
userClaimsMismatchCallback: () => Unit
|
||||
)(implicit loggingContext: LoggingContext): () => Unit = {
|
||||
val delay = userRightsCheckIntervalInSeconds.seconds
|
||||
val identityProviderId = originalClaims.identityProviderId
|
||||
val userId = originalClaims.applicationId.fold[Ref.UserId](
|
||||
throw new RuntimeException(
|
||||
"Claims were resolved from a user but userId (applicationId) is missing in the claims."
|
||||
@ -49,8 +50,8 @@ private[auth] final class UserRightsChangeAsyncChecker(
|
||||
val userState
|
||||
: Future[Either[UserManagementStore.Error, (domain.User, Set[domain.UserRight])]] =
|
||||
for {
|
||||
userRightsResult <- userManagementStore.listUserRights(userId)
|
||||
userResult <- userManagementStore.getUser(userId)
|
||||
userRightsResult <- userManagementStore.listUserRights(userId, identityProviderId)
|
||||
userResult <- userManagementStore.getUser(userId, identityProviderId)
|
||||
} yield {
|
||||
for {
|
||||
userRights <- userRightsResult
|
||||
|
@ -93,12 +93,12 @@ final class AuthorizationInterceptor(
|
||||
for {
|
||||
userManagementStore <- getUserManagementStore(userManagementStoreO)
|
||||
userId <- getUserId(userIdStr)
|
||||
user <- verifyUserIsActive(userManagementStore, userId)
|
||||
user <- verifyUserIsActive(userManagementStore, userId, identityProviderId)
|
||||
_ <- verifyUserIsWithinIdentityProvider(
|
||||
identityProviderId,
|
||||
user,
|
||||
)
|
||||
userRightsResult <- userManagementStore.listUserRights(userId)
|
||||
userRightsResult <- userManagementStore.listUserRights(userId, identityProviderId)
|
||||
claimsSet <- userRightsResult match {
|
||||
case Left(msg) =>
|
||||
Future.failed(
|
||||
@ -144,9 +144,10 @@ final class AuthorizationInterceptor(
|
||||
private def verifyUserIsActive(
|
||||
userManagementStore: UserManagementStore,
|
||||
userId: UserId,
|
||||
identityProviderId: IdentityProviderId,
|
||||
): Future[User] =
|
||||
for {
|
||||
userResult <- userManagementStore.getUser(id = userId)
|
||||
userResult <- userManagementStore.getUser(id = userId, identityProviderId)
|
||||
value <- userResult match {
|
||||
case Left(msg) =>
|
||||
Future.failed(
|
||||
|
@ -106,12 +106,11 @@ private[apiserver] final class ApiUserManagementService(
|
||||
for {
|
||||
_ <- identityProviderExistsOrError(user.identityProviderId)
|
||||
authorizedUserContext <- authorizedUserContextF
|
||||
_ <- verifyPartyExistInIdp(
|
||||
_ <- verifyPartiesExistInIdp(
|
||||
pRights,
|
||||
user.identityProviderId,
|
||||
authorizedUserContext.isParticipantAdmin,
|
||||
)
|
||||
_ <- verifyUserDoesNotExist(user.identityProviderId, user.id, "creating user")
|
||||
result <- userManagementStore
|
||||
.createUser(
|
||||
user = user,
|
||||
@ -165,12 +164,11 @@ private[apiserver] final class ApiUserManagementService(
|
||||
userUpdate <- handleUpdatePathResult(user.id, UserUpdateMapper.toUpdate(user, fieldMask))
|
||||
_ <- identityProviderExistsOrError(user.identityProviderId)
|
||||
authorizedUserContext <- authorizedUserContextF
|
||||
_ <- verifyPartyExistInIdp(
|
||||
_ <- verifyPartiesExistInIdp(
|
||||
Set(),
|
||||
user.identityProviderId,
|
||||
authorizedUserContext.isParticipantAdmin,
|
||||
)
|
||||
_ <- verifyUserExistsAndInIdp(user.identityProviderId, user.id, "updating user")
|
||||
_ <-
|
||||
if (
|
||||
authorizedUserContext.userId
|
||||
@ -232,8 +230,8 @@ private[apiserver] final class ApiUserManagementService(
|
||||
)
|
||||
} yield (userId, identityProviderId)
|
||||
} { case (userId, identityProviderId) =>
|
||||
verifyUserExistsAndInIdp(identityProviderId, userId, "getting user")
|
||||
.flatMap { _ => userManagementStore.getUser(userId) }
|
||||
userManagementStore
|
||||
.getUser(userId, identityProviderId)
|
||||
.flatMap(handleResult("getting user"))
|
||||
.map(u => GetUserResponse(Some(toProtoUser(u))))
|
||||
}
|
||||
@ -249,8 +247,8 @@ private[apiserver] final class ApiUserManagementService(
|
||||
)
|
||||
} yield (userId, identityProviderId)
|
||||
} { case (userId, identityProviderId) =>
|
||||
verifyUserExistsAndInIdp(identityProviderId, userId, "deleting user")
|
||||
.flatMap { _ => userManagementStore.deleteUser(userId) }
|
||||
userManagementStore
|
||||
.deleteUser(userId, identityProviderId)
|
||||
.flatMap(handleResult("deleting user"))
|
||||
.map(_ => proto.DeleteUserResponse())
|
||||
}
|
||||
@ -308,9 +306,8 @@ private[apiserver] final class ApiUserManagementService(
|
||||
} yield (userId, rights, identityProviderId)
|
||||
) { case (userId, rights, identityProviderId) =>
|
||||
for {
|
||||
_ <- verifyUserExistsAndInIdp(identityProviderId, userId, "grant user rights")
|
||||
authorizedUserContext <- authorizedUserContextF
|
||||
_ <- verifyPartyExistInIdp(
|
||||
_ <- verifyPartiesExistInIdp(
|
||||
rights,
|
||||
identityProviderId,
|
||||
authorizedUserContext.isParticipantAdmin,
|
||||
@ -319,6 +316,7 @@ private[apiserver] final class ApiUserManagementService(
|
||||
.grantRights(
|
||||
id = userId,
|
||||
rights = rights,
|
||||
identityProviderId = identityProviderId,
|
||||
)
|
||||
handledResult <- handleResult("grant user rights")(result)
|
||||
} yield proto.GrantUserRightsResponse(handledResult.view.map(toProtoRight).toList)
|
||||
@ -343,16 +341,16 @@ private[apiserver] final class ApiUserManagementService(
|
||||
) { case (userId, rights, identityProviderId) =>
|
||||
for {
|
||||
authorizedUserContext <- authorizedUserContextF
|
||||
_ <- verifyPartyExistInIdp(
|
||||
_ <- verifyPartiesExistInIdp(
|
||||
rights,
|
||||
identityProviderId,
|
||||
authorizedUserContext.isParticipantAdmin,
|
||||
)
|
||||
_ <- verifyUserExistsAndInIdp(identityProviderId, userId, "revoke user rights")
|
||||
result <- userManagementStore
|
||||
.revokeRights(
|
||||
id = userId,
|
||||
rights = rights,
|
||||
identityProviderId = identityProviderId,
|
||||
)
|
||||
handledResult <- handleResult("revoke user rights")(result)
|
||||
} yield proto.RevokeUserRightsResponse(handledResult.view.map(toProtoRight).toList)
|
||||
@ -371,8 +369,8 @@ private[apiserver] final class ApiUserManagementService(
|
||||
)
|
||||
} yield (userId, identityProviderId)
|
||||
} { case (userId, identityProviderId) =>
|
||||
verifyUserExistsAndInIdp(identityProviderId, userId, "list user rights")
|
||||
.flatMap { _ => userManagementStore.listUserRights(userId) }
|
||||
userManagementStore
|
||||
.listUserRights(userId, identityProviderId)
|
||||
.flatMap(handleResult("list user rights"))
|
||||
.map(_.view.map(toProtoRight).toList)
|
||||
.map(proto.ListUserRightsResponse(_))
|
||||
@ -390,7 +388,7 @@ private[apiserver] final class ApiUserManagementService(
|
||||
Future.successful(t)
|
||||
}
|
||||
|
||||
private def verifyPartyExistInIdp(
|
||||
private def verifyPartiesExistInIdp(
|
||||
rights: Set[UserRight],
|
||||
identityProviderId: IdentityProviderId,
|
||||
isParticipantAdmin: Boolean,
|
||||
@ -431,59 +429,14 @@ private[apiserver] final class ApiUserManagementService(
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user either doesn't exist or if it does - it belongs to the requested Identity Provider
|
||||
// Alternatively, identity_provider_id could be part of the compound unique key within user database.
|
||||
// It was considered as complication for the implementation and for now is simplified.
|
||||
private def verifyUserDoesNotExist(
|
||||
identityProviderId: IdentityProviderId,
|
||||
userId: Ref.UserId,
|
||||
operation: String,
|
||||
): Future[Unit] = {
|
||||
userManagementStore.getUser(userId).flatMap {
|
||||
case Right(user) if user.identityProviderId != identityProviderId =>
|
||||
Future.failed(
|
||||
LedgerApiErrors.AuthorizationChecks.PermissionDenied
|
||||
.Reject(s"User ${user.id} belongs to another Identity Provider")
|
||||
.asGrpcError
|
||||
)
|
||||
case Right(_) =>
|
||||
Future.failed(
|
||||
LedgerApiErrors.Admin.UserManagement.UserAlreadyExists
|
||||
.Reject(operation, userId.toString)
|
||||
.asGrpcError
|
||||
)
|
||||
case _ =>
|
||||
Future.unit
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user exists and belongs to the requested Identity Provider
|
||||
// There is a possible race condition here, as user might be deleted and reintroduced within another IDP
|
||||
// In order to fix this, proper atomicity is to be considered, by implementing this check in the persistence layer
|
||||
private def verifyUserExistsAndInIdp(
|
||||
identityProviderId: IdentityProviderId,
|
||||
userId: Ref.UserId,
|
||||
operation: String,
|
||||
): Future[Unit] = {
|
||||
userManagementStore.getUser(userId).flatMap {
|
||||
case Right(user) if user.identityProviderId != identityProviderId =>
|
||||
Future.failed(
|
||||
LedgerApiErrors.AuthorizationChecks.PermissionDenied
|
||||
.Reject(s"User ${user.id} belongs to another Identity Provider")
|
||||
.asGrpcError
|
||||
)
|
||||
case Left(UserManagementStore.UserNotFound(id)) =>
|
||||
Future.failed(
|
||||
LedgerApiErrors.Admin.UserManagement.UserNotFound
|
||||
.Reject(operation, id.toString)
|
||||
.asGrpcError
|
||||
)
|
||||
case _ => Future.unit
|
||||
}
|
||||
}
|
||||
|
||||
private def handleResult[T](operation: String)(result: UserManagementStore.Result[T]): Future[T] =
|
||||
result match {
|
||||
case Left(UserManagementStore.PermissionDenied(id)) =>
|
||||
Future.failed(
|
||||
LedgerApiErrors.AuthorizationChecks.PermissionDenied
|
||||
.Reject(s"User $id belongs to another Identity Provider")
|
||||
.asGrpcError
|
||||
)
|
||||
case Left(UserManagementStore.UserNotFound(id)) =>
|
||||
Future.failed(
|
||||
LedgerApiErrors.Admin.UserManagement.UserNotFound
|
||||
|
@ -97,11 +97,11 @@ class PersistentUserManagementStore(
|
||||
|
||||
private val logger = ContextualizedLogger.get(getClass)
|
||||
|
||||
override def getUserInfo(id: UserId)(implicit
|
||||
override def getUserInfo(id: UserId, identityProviderId: IdentityProviderId)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[UserInfo]] = {
|
||||
inTransaction(_.getUserInfo) { implicit connection =>
|
||||
withUser(id) { dbUser =>
|
||||
withUser(id, identityProviderId) { dbUser =>
|
||||
val rights = backend.getUserRights(internalId = dbUser.internalId)(connection)
|
||||
val annotations = backend.getUserAnnotations(internalId = dbUser.internalId)(connection)
|
||||
val domainUser = toDomainUser(dbUser, annotations)
|
||||
@ -115,7 +115,7 @@ class PersistentUserManagementStore(
|
||||
rights: Set[domain.UserRight],
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[User]] = {
|
||||
inTransaction(_.createUser) { implicit connection: Connection =>
|
||||
withoutUser(user.id) {
|
||||
withoutUser(user.id, user.identityProviderId) {
|
||||
val now = epochMicroseconds()
|
||||
if (
|
||||
!ResourceAnnotationValidation
|
||||
@ -165,7 +165,7 @@ class PersistentUserManagementStore(
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[User]] = {
|
||||
inTransaction(_.updateUser) { implicit connection =>
|
||||
for {
|
||||
_ <- withUser(id = userUpdate.id) { dbUser =>
|
||||
_ <- withUser(id = userUpdate.id, userUpdate.identityProviderId) { dbUser =>
|
||||
val now = epochMicroseconds()
|
||||
// Step 1: Update resource version
|
||||
// NOTE: We starts by writing to the 'resource_version' attribute
|
||||
@ -227,7 +227,10 @@ class PersistentUserManagementStore(
|
||||
)(connection)
|
||||
}
|
||||
}
|
||||
domainUser <- withUser(id = userUpdate.id) { dbUserAfterUpdates =>
|
||||
domainUser <- withUser(
|
||||
id = userUpdate.id,
|
||||
identityProviderId = userUpdate.identityProviderId,
|
||||
) { dbUserAfterUpdates =>
|
||||
val annotations =
|
||||
backend.getUserAnnotations(internalId = dbUserAfterUpdates.internalId)(connection)
|
||||
toDomainUser(dbUser = dbUserAfterUpdates, annotations = annotations)
|
||||
@ -237,13 +240,15 @@ class PersistentUserManagementStore(
|
||||
}
|
||||
|
||||
override def deleteUser(
|
||||
id: UserId
|
||||
id: UserId,
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[Unit]] = {
|
||||
inTransaction(_.deleteUser) { implicit connection =>
|
||||
if (!backend.deleteUser(id = id)(connection)) {
|
||||
Left(UserNotFound(userId = id))
|
||||
} else {
|
||||
Right(())
|
||||
withUser(id, identityProviderId) { _ =>
|
||||
backend.deleteUser(id = id)(connection)
|
||||
}.flatMap {
|
||||
case true => Right(())
|
||||
case false => Left(UserNotFound(userId = id))
|
||||
}
|
||||
}.map(tapSuccess { _ =>
|
||||
logger.info(s"Deleted user with id: ${id}")
|
||||
@ -253,9 +258,10 @@ class PersistentUserManagementStore(
|
||||
override def grantRights(
|
||||
id: UserId,
|
||||
rights: Set[domain.UserRight],
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[Set[domain.UserRight]]] = {
|
||||
inTransaction(_.grantRights) { implicit connection =>
|
||||
withUser(id = id) { user =>
|
||||
withUser(id = id, identityProviderId) { user =>
|
||||
val now = epochMicroseconds()
|
||||
val addedRights = rights.filter { right =>
|
||||
if (!backend.userRightExists(internalId = user.internalId, right = right)(connection)) {
|
||||
@ -285,9 +291,10 @@ class PersistentUserManagementStore(
|
||||
override def revokeRights(
|
||||
id: UserId,
|
||||
rights: Set[domain.UserRight],
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[Set[domain.UserRight]]] = {
|
||||
inTransaction(_.revokeRights) { implicit connection =>
|
||||
withUser(id = id) { user =>
|
||||
withUser(id = id, identityProviderId) { user =>
|
||||
val revokedRights = rights.filter { right =>
|
||||
backend.deleteUserRight(internalId = user.internalId, right = right)(connection)
|
||||
}
|
||||
@ -367,21 +374,28 @@ class PersistentUserManagementStore(
|
||||
}
|
||||
|
||||
private def withUser[T](
|
||||
id: Ref.UserId
|
||||
id: Ref.UserId,
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(
|
||||
f: UserManagementStorageBackend.DbUserWithId => T
|
||||
)(implicit connection: Connection): Result[T] = {
|
||||
backend.getUser(id = id)(connection) match {
|
||||
case Some(user) => Right(f(user))
|
||||
case Some(user) if user.payload.identityProviderId == identityProviderId.toDb =>
|
||||
Right(f(user))
|
||||
case Some(_) => Left(PermissionDenied(userId = id))
|
||||
case None => Left(UserNotFound(userId = id))
|
||||
}
|
||||
}
|
||||
|
||||
private def withoutUser[T](
|
||||
id: Ref.UserId
|
||||
id: Ref.UserId,
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(t: => T)(implicit connection: Connection): Result[T] = {
|
||||
backend.getUser(id = id)(connection) match {
|
||||
case Some(user) => Left(UserExists(userId = user.payload.id))
|
||||
case Some(user) if user.payload.identityProviderId != identityProviderId.toDb =>
|
||||
Left(PermissionDenied(userId = id))
|
||||
case Some(user) =>
|
||||
Left(UserExists(userId = user.payload.id))
|
||||
case None => Right(t)
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import com.daml.lf.data.Ref
|
||||
import com.daml.lf.data.Ref.UserId
|
||||
import com.daml.logging.LoggingContext
|
||||
import com.daml.metrics.Metrics
|
||||
import com.daml.platform.localstore.CachedUserManagementStore.CacheKey
|
||||
import com.daml.platform.localstore.api.UserManagementStore.{Result, UserInfo}
|
||||
import com.daml.platform.localstore.api.{UserManagementStore, UserUpdate}
|
||||
import com.github.benmanes.caffeine.{cache => caffeine}
|
||||
@ -27,22 +28,24 @@ class CachedUserManagementStore(
|
||||
)(implicit val executionContext: ExecutionContext, loggingContext: LoggingContext)
|
||||
extends UserManagementStore {
|
||||
|
||||
private val cache: CaffeineCache.AsyncLoadingCaffeineCache[Ref.UserId, Result[UserInfo]] =
|
||||
private val cache: CaffeineCache.AsyncLoadingCaffeineCache[CacheKey, Result[UserInfo]] =
|
||||
new CaffeineCache.AsyncLoadingCaffeineCache(
|
||||
caffeine.Caffeine
|
||||
.newBuilder()
|
||||
.expireAfterWrite(Duration.ofSeconds(expiryAfterWriteInSeconds.toLong))
|
||||
.maximumSize(maximumCacheSize.toLong)
|
||||
.buildAsync(
|
||||
new FutureAsyncCacheLoader[UserId, Result[UserInfo]](key => delegate.getUserInfo(key))
|
||||
new FutureAsyncCacheLoader[CacheKey, Result[UserInfo]](key =>
|
||||
delegate.getUserInfo(key.id, key.identityProviderId)
|
||||
)
|
||||
),
|
||||
metrics.daml.userManagement.cache,
|
||||
)
|
||||
|
||||
override def getUserInfo(id: UserId)(implicit
|
||||
override def getUserInfo(id: UserId, identityProviderId: IdentityProviderId)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[UserManagementStore.UserInfo]] = {
|
||||
cache.get(id)
|
||||
cache.get(CacheKey(id, identityProviderId))
|
||||
}
|
||||
|
||||
override def createUser(user: User, rights: Set[domain.UserRight])(implicit
|
||||
@ -50,40 +53,43 @@ class CachedUserManagementStore(
|
||||
): Future[Result[User]] =
|
||||
delegate
|
||||
.createUser(user, rights)
|
||||
.andThen(invalidateOnSuccess(user.id))
|
||||
.andThen(invalidateOnSuccess(CacheKey(user.id, user.identityProviderId)))
|
||||
|
||||
override def updateUser(
|
||||
userUpdate: UserUpdate
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[User]] = {
|
||||
delegate
|
||||
.updateUser(userUpdate)
|
||||
.andThen(invalidateOnSuccess(userUpdate.id))
|
||||
.andThen(invalidateOnSuccess(CacheKey(userUpdate.id, userUpdate.identityProviderId)))
|
||||
}
|
||||
|
||||
override def deleteUser(
|
||||
id: UserId
|
||||
id: UserId,
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[Unit]] = {
|
||||
delegate
|
||||
.deleteUser(id)
|
||||
.andThen(invalidateOnSuccess(id))
|
||||
.deleteUser(id, identityProviderId)
|
||||
.andThen(invalidateOnSuccess(CacheKey(id, identityProviderId)))
|
||||
}
|
||||
|
||||
override def grantRights(
|
||||
id: UserId,
|
||||
rights: Set[domain.UserRight],
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[Set[domain.UserRight]]] = {
|
||||
delegate
|
||||
.grantRights(id, rights)
|
||||
.andThen(invalidateOnSuccess(id))
|
||||
.grantRights(id, rights, identityProviderId)
|
||||
.andThen(invalidateOnSuccess(CacheKey(id, identityProviderId)))
|
||||
}
|
||||
|
||||
override def revokeRights(
|
||||
id: UserId,
|
||||
rights: Set[domain.UserRight],
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[Set[domain.UserRight]]] = {
|
||||
delegate
|
||||
.revokeRights(id, rights)
|
||||
.andThen(invalidateOnSuccess(id))
|
||||
.revokeRights(id, rights, identityProviderId)
|
||||
.andThen(invalidateOnSuccess(CacheKey(id, identityProviderId)))
|
||||
}
|
||||
|
||||
override def listUsers(
|
||||
@ -95,8 +101,12 @@ class CachedUserManagementStore(
|
||||
): Future[Result[UserManagementStore.UsersPage]] =
|
||||
delegate.listUsers(fromExcl, maxResults, identityProviderId)
|
||||
|
||||
private def invalidateOnSuccess(id: UserId): PartialFunction[Try[Result[Any]], Unit] = {
|
||||
case Success(Right(_)) => cache.invalidate(id)
|
||||
private def invalidateOnSuccess(key: CacheKey): PartialFunction[Try[Result[Any]], Unit] = {
|
||||
case Success(Right(_)) => cache.invalidate(key)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object CachedUserManagementStore {
|
||||
case class CacheKey(id: UserId, identityProviderId: IdentityProviderId)
|
||||
}
|
||||
|
@ -27,15 +27,15 @@ class InMemoryUserManagementStore(createAdmin: Boolean = true) extends UserManag
|
||||
state.put(AdminUser.user.id, AdminUser)
|
||||
}
|
||||
|
||||
override def getUserInfo(id: UserId)(implicit
|
||||
override def getUserInfo(id: UserId, identityProviderId: IdentityProviderId)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[UserManagementStore.UserInfo]] =
|
||||
withUser(id)(info => Right(toDomainUserInfo(info)))
|
||||
withUser(id, identityProviderId)(info => Right(toDomainUserInfo(info)))
|
||||
|
||||
override def createUser(user: User, rights: Set[UserRight])(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[User]] =
|
||||
withoutUser(user.id) {
|
||||
withoutUser(user.id, user.identityProviderId) {
|
||||
for {
|
||||
_ <- validateAnnotationsSize(user.metadata.annotations, user.id)
|
||||
} yield {
|
||||
@ -57,7 +57,7 @@ class InMemoryUserManagementStore(createAdmin: Boolean = true) extends UserManag
|
||||
override def updateUser(
|
||||
userUpdate: UserUpdate
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[User]] = {
|
||||
withUser(userUpdate.id) { userInfo =>
|
||||
withUser(userUpdate.id, userUpdate.identityProviderId) { userInfo =>
|
||||
val updatedPrimaryParty = userUpdate.primaryPartyUpdateO.getOrElse(userInfo.user.primaryParty)
|
||||
val updatedIsDeactivated =
|
||||
userUpdate.isDeactivatedUpdateO.getOrElse(userInfo.user.isDeactivated)
|
||||
@ -100,9 +100,10 @@ class InMemoryUserManagementStore(createAdmin: Boolean = true) extends UserManag
|
||||
}
|
||||
|
||||
override def deleteUser(
|
||||
id: Ref.UserId
|
||||
id: Ref.UserId,
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[Unit]] =
|
||||
withUser(id) { _ =>
|
||||
withUser(id, identityProviderId) { _ =>
|
||||
state.remove(id)
|
||||
Right(())
|
||||
}
|
||||
@ -110,8 +111,9 @@ class InMemoryUserManagementStore(createAdmin: Boolean = true) extends UserManag
|
||||
override def grantRights(
|
||||
id: Ref.UserId,
|
||||
granted: Set[UserRight],
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[Set[UserRight]]] =
|
||||
withUser(id) { userInfo =>
|
||||
withUser(id, identityProviderId) { userInfo =>
|
||||
val newlyGranted = granted.diff(userInfo.rights) // faster than filter
|
||||
// we're not doing concurrent updates -- assert as backstop and a reminder to handle the collision case in the future
|
||||
assert(
|
||||
@ -123,8 +125,9 @@ class InMemoryUserManagementStore(createAdmin: Boolean = true) extends UserManag
|
||||
override def revokeRights(
|
||||
id: Ref.UserId,
|
||||
revoked: Set[UserRight],
|
||||
identityProviderId: IdentityProviderId,
|
||||
)(implicit loggingContext: LoggingContext): Future[Result[Set[UserRight]]] =
|
||||
withUser(id) { userInfo =>
|
||||
withUser(id, identityProviderId) { userInfo =>
|
||||
val effectivelyRevoked = revoked.intersect(userInfo.rights) // faster than filter
|
||||
// we're not doing concurrent updates -- assert as backstop and a reminder to handle the collision case in the future
|
||||
assert(
|
||||
@ -163,17 +166,26 @@ class InMemoryUserManagementStore(createAdmin: Boolean = true) extends UserManag
|
||||
Future.successful(t)
|
||||
)
|
||||
|
||||
private def withUser[T](id: Ref.UserId)(f: InMemUserInfo => Result[T]): Future[Result[T]] =
|
||||
private def withUser[T](id: Ref.UserId, identityProviderId: IdentityProviderId)(
|
||||
f: InMemUserInfo => Result[T]
|
||||
): Future[Result[T]] =
|
||||
withState(
|
||||
state.get(id) match {
|
||||
case Some(user) => f(user)
|
||||
case None => Left(UserNotFound(id))
|
||||
case Some(user) if user.user.identityProviderId == identityProviderId => f(user)
|
||||
case Some(_) =>
|
||||
Left(PermissionDenied(id))
|
||||
case None =>
|
||||
Left(UserNotFound(id))
|
||||
}
|
||||
)
|
||||
|
||||
private def withoutUser[T](id: Ref.UserId)(t: => Result[T]): Future[Result[T]] =
|
||||
private def withoutUser[T](id: Ref.UserId, identityProviderId: IdentityProviderId)(
|
||||
t: => Result[T]
|
||||
): Future[Result[T]] =
|
||||
withState(
|
||||
state.get(id) match {
|
||||
case Some(user) if user.user.identityProviderId != identityProviderId =>
|
||||
Left(PermissionDenied(id))
|
||||
case Some(_) => Left(UserExists(id))
|
||||
case None => t
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ trait UserManagementStore {
|
||||
|
||||
// read access
|
||||
|
||||
def getUserInfo(id: Ref.UserId)(implicit
|
||||
def getUserInfo(id: Ref.UserId, identityProviderId: IdentityProviderId)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[UserInfo]]
|
||||
|
||||
@ -61,28 +61,30 @@ trait UserManagementStore {
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[User]]
|
||||
|
||||
def deleteUser(id: Ref.UserId)(implicit loggingContext: LoggingContext): Future[Result[Unit]]
|
||||
|
||||
def grantRights(id: Ref.UserId, rights: Set[UserRight])(implicit
|
||||
def deleteUser(id: Ref.UserId, identityProviderId: IdentityProviderId)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[Unit]]
|
||||
|
||||
def grantRights(id: Ref.UserId, rights: Set[UserRight], identityProviderId: IdentityProviderId)(
|
||||
implicit loggingContext: LoggingContext
|
||||
): Future[Result[Set[UserRight]]]
|
||||
|
||||
def revokeRights(id: Ref.UserId, rights: Set[UserRight])(implicit
|
||||
loggingContext: LoggingContext
|
||||
def revokeRights(id: Ref.UserId, rights: Set[UserRight], identityProviderId: IdentityProviderId)(
|
||||
implicit loggingContext: LoggingContext
|
||||
): Future[Result[Set[UserRight]]]
|
||||
|
||||
// read helpers
|
||||
|
||||
final def getUser(id: Ref.UserId)(implicit
|
||||
final def getUser(id: Ref.UserId, identityProviderId: IdentityProviderId)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[User]] = {
|
||||
getUserInfo(id).map(_.map(_.user))(ExecutionContext.parasitic)
|
||||
getUserInfo(id, identityProviderId).map(_.map(_.user))(ExecutionContext.parasitic)
|
||||
}
|
||||
|
||||
final def listUserRights(id: Ref.UserId)(implicit
|
||||
final def listUserRights(id: Ref.UserId, identityProviderId: IdentityProviderId)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): Future[Result[Set[UserRight]]] = {
|
||||
getUserInfo(id).map(_.map(_.rights))(ExecutionContext.parasitic)
|
||||
getUserInfo(id, identityProviderId).map(_.map(_.rights))(ExecutionContext.parasitic)
|
||||
}
|
||||
|
||||
}
|
||||
@ -105,4 +107,5 @@ object UserManagementStore {
|
||||
final case class TooManyUserRights(userId: Ref.UserId) extends Error
|
||||
final case class ConcurrentUserUpdate(userId: Ref.UserId) extends Error
|
||||
final case class MaxAnnotationsSizeExceeded(userId: Ref.UserId) extends Error
|
||||
final case class PermissionDenied(userId: Ref.UserId) extends Error
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import com.daml.lf.data.Ref.{LedgerString, Party, UserId}
|
||||
import com.daml.logging.LoggingContext
|
||||
import com.daml.platform.localstore.api.UserManagementStore.{
|
||||
MaxAnnotationsSizeExceeded,
|
||||
PermissionDenied,
|
||||
UserExists,
|
||||
UserNotFound,
|
||||
UsersPage,
|
||||
@ -41,6 +42,8 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
|
||||
val persistedIdentityProviderId =
|
||||
IdentityProviderId.Id(LedgerString.assertFromString("idp1"))
|
||||
private val idpId =
|
||||
IdentityProviderId.Default
|
||||
val idp1 = IdentityProviderConfig(
|
||||
identityProviderId = persistedIdentityProviderId,
|
||||
isDeactivated = false,
|
||||
@ -53,7 +56,7 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
primaryParty: Option[Ref.Party] = None,
|
||||
isDeactivated: Boolean = false,
|
||||
annotations: Map[String, String] = Map.empty,
|
||||
identityProviderId: IdentityProviderId = IdentityProviderId.Default,
|
||||
identityProviderId: IdentityProviderId = idpId,
|
||||
): User = User(
|
||||
id = name,
|
||||
primaryParty = primaryParty,
|
||||
@ -141,12 +144,28 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
}
|
||||
}
|
||||
|
||||
"deny permission re-creating an existing user within another IDP" in {
|
||||
testIt { tested =>
|
||||
val user = newUser("user1")
|
||||
for {
|
||||
res1 <- tested.createUser(user, Set.empty)
|
||||
res2 <- tested.createUser(
|
||||
user.copy(identityProviderId = persistedIdentityProviderId),
|
||||
Set.empty,
|
||||
)
|
||||
} yield {
|
||||
res1 shouldBe Right(createdUser("user1"))
|
||||
res2 shouldBe Left(PermissionDenied(user.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"find a freshly created user" in {
|
||||
testIt { tested =>
|
||||
val user = newUser("user1")
|
||||
for {
|
||||
res1 <- tested.createUser(user, Set.empty)
|
||||
user1 <- tested.getUser(user.id)
|
||||
user1 <- tested.getUser(user.id, idpId)
|
||||
} yield {
|
||||
res1 shouldBe Right(createdUser("user1"))
|
||||
user1 shouldBe res1
|
||||
@ -154,11 +173,24 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
}
|
||||
}
|
||||
|
||||
"deny to find a freshly created user within another IDP" in {
|
||||
testIt { tested =>
|
||||
val user = newUser("user1")
|
||||
for {
|
||||
res1 <- tested.createUser(user, Set.empty)
|
||||
user1 <- tested.getUser(user.id, persistedIdentityProviderId)
|
||||
} yield {
|
||||
res1 shouldBe Right(createdUser("user1"))
|
||||
user1 shouldBe Left(PermissionDenied(user.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"not find a non-existent user" in {
|
||||
testIt { tested =>
|
||||
val userId: Ref.UserId = "user1"
|
||||
for {
|
||||
user1 <- tested.getUser(userId)
|
||||
user1 <- tested.getUser(userId, idpId)
|
||||
} yield {
|
||||
user1 shouldBe Left(UserNotFound(userId))
|
||||
}
|
||||
@ -169,9 +201,9 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
val user = newUser("user1")
|
||||
for {
|
||||
res1 <- tested.createUser(user, Set.empty)
|
||||
user1 <- tested.getUser("user1")
|
||||
res2 <- tested.deleteUser("user1")
|
||||
user2 <- tested.getUser("user1")
|
||||
user1 <- tested.getUser("user1", idpId)
|
||||
res2 <- tested.deleteUser("user1", idpId)
|
||||
user2 <- tested.getUser("user1", idpId)
|
||||
} yield {
|
||||
res1 shouldBe Right(createdUser("user1"))
|
||||
user1 shouldBe res1
|
||||
@ -180,12 +212,24 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
}
|
||||
}
|
||||
}
|
||||
"deny to delete user within another IDP" in {
|
||||
testIt { tested =>
|
||||
val user = newUser("user1")
|
||||
for {
|
||||
res1 <- tested.createUser(user, Set.empty)
|
||||
user1 <- tested.getUser("user1", persistedIdentityProviderId)
|
||||
} yield {
|
||||
res1 shouldBe Right(createdUser("user1"))
|
||||
user1 shouldBe Left(PermissionDenied(user.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
"allow recreating a deleted user" in {
|
||||
testIt { tested =>
|
||||
val user = newUser("user1")
|
||||
for {
|
||||
res1 <- tested.createUser(user, Set.empty)
|
||||
res2 <- tested.deleteUser(user.id)
|
||||
res2 <- tested.deleteUser(user.id, idpId)
|
||||
res3 <- tested.createUser(user, Set.empty)
|
||||
} yield {
|
||||
res1 shouldBe Right(createdUser("user1"))
|
||||
@ -198,7 +242,7 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
"fail to delete a non-existent user" in {
|
||||
testIt { tested =>
|
||||
for {
|
||||
res1 <- tested.deleteUser("user1")
|
||||
res1 <- tested.deleteUser("user1", idpId)
|
||||
} yield {
|
||||
res1 shouldBe Left(UserNotFound("user1"))
|
||||
}
|
||||
@ -247,7 +291,7 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
maxResults = 10000,
|
||||
identityProviderId = IdentityProviderId.Default,
|
||||
)
|
||||
res3 <- tested.deleteUser("user1")
|
||||
res3 <- tested.deleteUser("user1", idpId)
|
||||
users2 <- tested.listUsers(
|
||||
fromExcl = None,
|
||||
maxResults = 10000,
|
||||
@ -323,12 +367,12 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
testIt { tested =>
|
||||
for {
|
||||
res1 <- tested.createUser(newUser("user1"), Set.empty)
|
||||
rights1 <- tested.listUserRights("user1")
|
||||
rights1 <- tested.listUserRights("user1", idpId)
|
||||
user2 <- tested.createUser(
|
||||
newUser("user2"),
|
||||
Set(ParticipantAdmin, CanActAs("party1"), CanReadAs("party2")),
|
||||
)
|
||||
rights2 <- tested.listUserRights("user2")
|
||||
rights2 <- tested.listUserRights("user2", idpId)
|
||||
} yield {
|
||||
res1 shouldBe Right(createdUser("user1"))
|
||||
rights1 shouldBe Right(Set.empty)
|
||||
@ -339,10 +383,20 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
}
|
||||
}
|
||||
}
|
||||
"listUserRights should deny for user in another IDP" in {
|
||||
testIt { tested =>
|
||||
for {
|
||||
_ <- tested.createUser(newUser("user1"), Set.empty)
|
||||
rights1 <- tested.listUserRights("user1", persistedIdentityProviderId)
|
||||
} yield {
|
||||
rights1 shouldBe Left(PermissionDenied("user1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
"listUserRights should fail on non-existent user" in {
|
||||
testIt { tested =>
|
||||
for {
|
||||
rights1 <- tested.listUserRights("user1")
|
||||
rights1 <- tested.listUserRights("user1", idpId)
|
||||
} yield {
|
||||
rights1 shouldBe Left(UserNotFound("user1"))
|
||||
}
|
||||
@ -352,13 +406,14 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
testIt { tested =>
|
||||
for {
|
||||
res1 <- tested.createUser(newUser("user1"), Set.empty)
|
||||
rights1 <- tested.grantRights("user1", Set(ParticipantAdmin))
|
||||
rights2 <- tested.grantRights("user1", Set(ParticipantAdmin))
|
||||
rights1 <- tested.grantRights("user1", Set(ParticipantAdmin), idpId)
|
||||
rights2 <- tested.grantRights("user1", Set(ParticipantAdmin), idpId)
|
||||
rights3 <- tested.grantRights(
|
||||
"user1",
|
||||
Set(CanActAs("party1"), CanReadAs("party2")),
|
||||
idpId,
|
||||
)
|
||||
rights4 <- tested.listUserRights("user1")
|
||||
rights4 <- tested.listUserRights("user1", idpId)
|
||||
} yield {
|
||||
res1 shouldBe Right(createdUser("user1"))
|
||||
rights1 shouldBe Right(Set(ParticipantAdmin))
|
||||
@ -375,13 +430,23 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
"grantRights should fail on non-existent user" in {
|
||||
testIt { tested =>
|
||||
for {
|
||||
rights1 <- tested.grantRights("user1", Set.empty)
|
||||
rights1 <- tested.grantRights("user1", Set.empty, idpId)
|
||||
} yield {
|
||||
rights1 shouldBe Left(UserNotFound("user1"))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
"grantRights should deny for user in another IDP" in {
|
||||
testIt { tested =>
|
||||
for {
|
||||
_ <- tested.createUser(newUser("user1"), Set.empty)
|
||||
rights1 <- tested.grantRights("user1", Set.empty, persistedIdentityProviderId)
|
||||
} yield {
|
||||
rights1 shouldBe Left(PermissionDenied("user1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
"revokeRights should revoke rights" in {
|
||||
testIt { tested =>
|
||||
for {
|
||||
@ -389,15 +454,16 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
newUser("user1"),
|
||||
Set(ParticipantAdmin, CanActAs("party1"), CanReadAs("party2")),
|
||||
)
|
||||
rights1 <- tested.listUserRights("user1")
|
||||
rights2 <- tested.revokeRights("user1", Set(ParticipantAdmin))
|
||||
rights3 <- tested.revokeRights("user1", Set(ParticipantAdmin))
|
||||
rights4 <- tested.listUserRights("user1")
|
||||
rights1 <- tested.listUserRights("user1", idpId)
|
||||
rights2 <- tested.revokeRights("user1", Set(ParticipantAdmin), idpId)
|
||||
rights3 <- tested.revokeRights("user1", Set(ParticipantAdmin), idpId)
|
||||
rights4 <- tested.listUserRights("user1", idpId)
|
||||
rights5 <- tested.revokeRights(
|
||||
"user1",
|
||||
Set(CanActAs("party1"), CanReadAs("party2")),
|
||||
idpId,
|
||||
)
|
||||
rights6 <- tested.listUserRights("user1")
|
||||
rights6 <- tested.listUserRights("user1", idpId)
|
||||
} yield {
|
||||
res1 shouldBe Right(createdUser("user1"))
|
||||
rights1 shouldBe Right(
|
||||
@ -416,12 +482,22 @@ trait UserStoreTests extends UserStoreSpecBase { self: AsyncFreeSpec =>
|
||||
"revokeRights should fail on non-existent user" in {
|
||||
testIt { tested =>
|
||||
for {
|
||||
rights1 <- tested.revokeRights("user1", Set.empty)
|
||||
rights1 <- tested.revokeRights("user1", Set.empty, idpId)
|
||||
} yield {
|
||||
rights1 shouldBe Left(UserNotFound("user1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
"revokeRights should deny for user in another IDP" in {
|
||||
testIt { tested =>
|
||||
for {
|
||||
_ <- tested.createUser(newUser("user1"), Set.empty)
|
||||
rights1 <- tested.revokeRights("user1", Set.empty, persistedIdentityProviderId)
|
||||
} yield {
|
||||
rights1 shouldBe Left(PermissionDenied("user1"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"updating" - {
|
||||
|
@ -55,14 +55,15 @@ class CachedUserManagementStoreSpec
|
||||
private val userInfo = UserInfo(user, rights)
|
||||
private val createdUserInfo = UserInfo(createdUser1, rights)
|
||||
private val filter: IdentityProviderId = IdentityProviderId.Default
|
||||
private val idpId = IdentityProviderId.Default
|
||||
|
||||
"test user-not-found cache result gets invalidated after user creation" in {
|
||||
val delegate = spy(new InMemoryUserManagementStore())
|
||||
val tested = createTested(delegate)
|
||||
for {
|
||||
getYetNonExistent <- tested.getUserInfo(userInfo.user.id)
|
||||
getYetNonExistent <- tested.getUserInfo(userInfo.user.id, idpId)
|
||||
_ <- tested.createUser(userInfo.user, userInfo.rights)
|
||||
get <- tested.getUserInfo(user.id)
|
||||
get <- tested.getUserInfo(user.id, idpId)
|
||||
} yield {
|
||||
getYetNonExistent shouldBe Left(UserNotFound(createdUserInfo.user.id))
|
||||
get shouldBe Right(createdUserInfo)
|
||||
@ -75,13 +76,13 @@ class CachedUserManagementStoreSpec
|
||||
|
||||
for {
|
||||
_ <- tested.createUser(userInfo.user, userInfo.rights)
|
||||
get1 <- tested.getUserInfo(user.id)
|
||||
get2 <- tested.getUserInfo(user.id)
|
||||
getUser <- tested.getUser(user.id)
|
||||
listRights <- tested.listUserRights(user.id)
|
||||
get1 <- tested.getUserInfo(user.id, idpId)
|
||||
get2 <- tested.getUserInfo(user.id, idpId)
|
||||
getUser <- tested.getUser(user.id, idpId)
|
||||
listRights <- tested.listUserRights(user.id, idpId)
|
||||
} yield {
|
||||
verify(delegate, times(1)).createUser(userInfo.user, userInfo.rights)
|
||||
verify(delegate, times(1)).getUserInfo(userInfo.user.id)
|
||||
verify(delegate, times(1)).getUserInfo(userInfo.user.id, idpId)
|
||||
verifyNoMoreInteractions(delegate)
|
||||
get1 shouldBe Right(createdUserInfo)
|
||||
get2 shouldBe Right(createdUserInfo)
|
||||
@ -98,11 +99,11 @@ class CachedUserManagementStoreSpec
|
||||
|
||||
for {
|
||||
_ <- tested.createUser(userInfo.user, userInfo.rights)
|
||||
get1 <- tested.getUserInfo(user.id)
|
||||
_ <- tested.grantRights(user.id, Set(right1))
|
||||
get2 <- tested.getUserInfo(user.id)
|
||||
_ <- tested.revokeRights(user.id, Set(right3))
|
||||
get3 <- tested.getUserInfo(user.id)
|
||||
get1 <- tested.getUserInfo(user.id, idpId)
|
||||
_ <- tested.grantRights(user.id, Set(right1), idpId)
|
||||
get2 <- tested.getUserInfo(user.id, idpId)
|
||||
_ <- tested.revokeRights(user.id, Set(right3), idpId)
|
||||
get3 <- tested.getUserInfo(user.id, idpId)
|
||||
_ <- tested.updateUser(
|
||||
UserUpdate(
|
||||
id = user.id,
|
||||
@ -111,26 +112,26 @@ class CachedUserManagementStoreSpec
|
||||
metadataUpdate = ObjectMetaUpdate.empty,
|
||||
)
|
||||
)
|
||||
get4 <- tested.getUserInfo(user.id)
|
||||
_ <- tested.deleteUser(user.id)
|
||||
get5 <- tested.getUserInfo(user.id)
|
||||
get4 <- tested.getUserInfo(user.id, idpId)
|
||||
_ <- tested.deleteUser(user.id, idpId)
|
||||
get5 <- tested.getUserInfo(user.id, idpId)
|
||||
|
||||
} yield {
|
||||
val order = inOrder(delegate)
|
||||
order.verify(delegate, times(1)).createUser(user, userInfo.rights)
|
||||
order.verify(delegate, times(1)).getUserInfo(user.id)
|
||||
order.verify(delegate, times(1)).getUserInfo(user.id, idpId)
|
||||
order
|
||||
.verify(delegate, times(1))
|
||||
.grantRights(eqTo(user.id), any[Set[UserRight]])(any[LoggingContext])
|
||||
order.verify(delegate, times(1)).getUserInfo(userInfo.user.id)
|
||||
.grantRights(eqTo(user.id), any[Set[UserRight]], eqTo(idpId))(any[LoggingContext])
|
||||
order.verify(delegate, times(1)).getUserInfo(userInfo.user.id, idpId)
|
||||
order
|
||||
.verify(delegate, times(1))
|
||||
.revokeRights(eqTo(user.id), any[Set[UserRight]])(any[LoggingContext])
|
||||
order.verify(delegate, times(1)).getUserInfo(userInfo.user.id)
|
||||
.revokeRights(eqTo(user.id), any[Set[UserRight]], eqTo(idpId))(any[LoggingContext])
|
||||
order.verify(delegate, times(1)).getUserInfo(userInfo.user.id, idpId)
|
||||
order.verify(delegate, times(1)).updateUser(any[UserUpdate])(any[LoggingContext])
|
||||
order.verify(delegate, times(1)).getUserInfo(userInfo.user.id)
|
||||
order.verify(delegate, times(1)).deleteUser(userInfo.user.id)
|
||||
order.verify(delegate, times(1)).getUserInfo(userInfo.user.id)
|
||||
order.verify(delegate, times(1)).getUserInfo(userInfo.user.id, idpId)
|
||||
order.verify(delegate, times(1)).deleteUser(userInfo.user.id, idpId)
|
||||
order.verify(delegate, times(1)).getUserInfo(userInfo.user.id, idpId)
|
||||
order.verifyNoMoreInteractions()
|
||||
get1 shouldBe Right(createdUserInfo)
|
||||
get2 shouldBe Right(createdUserInfo)
|
||||
@ -175,17 +176,19 @@ class CachedUserManagementStoreSpec
|
||||
|
||||
for {
|
||||
create1 <- tested.createUser(user, rights)
|
||||
get1 <- tested.getUserInfo(user.id)
|
||||
get2 <- tested.getUserInfo(user.id)
|
||||
get1 <- tested.getUserInfo(user.id, idpId)
|
||||
get2 <- tested.getUserInfo(user.id, idpId)
|
||||
get3 <- {
|
||||
Thread.sleep(2000); tested.getUserInfo(user.id)
|
||||
Thread.sleep(2000); tested.getUserInfo(user.id, idpId)
|
||||
}
|
||||
} yield {
|
||||
val order = inOrder(delegate)
|
||||
order
|
||||
.verify(delegate, times(1))
|
||||
.createUser(any[User], any[Set[UserRight]])(any[LoggingContext])
|
||||
order.verify(delegate, times(2)).getUserInfo(any[Ref.UserId])(any[LoggingContext])
|
||||
order
|
||||
.verify(delegate, times(2))
|
||||
.getUserInfo(any[Ref.UserId], eqTo(idpId))(any[LoggingContext])
|
||||
order.verifyNoMoreInteractions()
|
||||
create1 shouldBe Right(createdUser1)
|
||||
get1 shouldBe Right(createdUserInfo)
|
||||
|
@ -5,6 +5,9 @@ package com.daml.platform.sandbox.auth
|
||||
|
||||
import scala.concurrent.Future
|
||||
import com.daml.ledger.api.v1.admin.{user_management_service => ums}
|
||||
import com.daml.test.evidence.scalatest.ScalaTestSupport.Implicits._
|
||||
|
||||
import java.util.UUID
|
||||
|
||||
final class CreateUserAuthIT
|
||||
extends AdminOrIDPAdminServiceCallAuthTests
|
||||
@ -18,4 +21,29 @@ final class CreateUserAuthIT
|
||||
permission: ums.Right,
|
||||
): Future[Any] =
|
||||
createFreshUser(context.token, context.identityProviderId, scala.Seq(permission))
|
||||
|
||||
it should "deny calls if user is created already within another IDP" taggedAs adminSecurityAsset
|
||||
.setAttack(
|
||||
attackPermissionDenied(threat = "Present an existing userId but foreign Identity Provider")
|
||||
) in {
|
||||
expectPermissionDenied {
|
||||
val userId = "fresh-user-" + UUID.randomUUID().toString
|
||||
for {
|
||||
idpConfigResponse1 <- createConfig(canReadAsAdminStandardJWT)
|
||||
idpConfigresponse2 <- createConfig(canReadAsAdminStandardJWT)
|
||||
_ <- createFreshUser(
|
||||
userId,
|
||||
canReadAsAdmin.token,
|
||||
toIdentityProviderId(idpConfigResponse1),
|
||||
Seq.empty,
|
||||
)
|
||||
_ <- createFreshUser(
|
||||
userId,
|
||||
canReadAsAdmin.token,
|
||||
toIdentityProviderId(idpConfigresponse2),
|
||||
Seq.empty,
|
||||
)
|
||||
} yield ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ package com.daml.platform.sandbox.auth
|
||||
|
||||
import com.daml.ledger.api.v1.admin.user_management_service.DeleteUserRequest
|
||||
|
||||
import java.util.UUID
|
||||
import scala.concurrent.Future
|
||||
import com.daml.test.evidence.scalatest.ScalaTestSupport.Implicits._
|
||||
|
||||
final class DeleteUserAuthIT extends AdminOrIDPAdminServiceCallAuthTests with UserManagementAuth {
|
||||
|
||||
@ -20,4 +22,28 @@ final class DeleteUserAuthIT extends AdminOrIDPAdminServiceCallAuthTests with Us
|
||||
)
|
||||
} yield ()
|
||||
|
||||
it should "deny calls if user is created already within another IDP" taggedAs adminSecurityAsset
|
||||
.setAttack(
|
||||
attackPermissionDenied(threat = "Present an existing userId but foreign Identity Provider")
|
||||
) in {
|
||||
expectPermissionDenied {
|
||||
val userId = "fresh-user-" + UUID.randomUUID().toString
|
||||
for {
|
||||
idpConfigresponse1 <- createConfig(canReadAsAdminStandardJWT)
|
||||
idpConfigresponse2 <- createConfig(canReadAsAdminStandardJWT)
|
||||
_ <- createFreshUser(
|
||||
userId,
|
||||
canReadAsAdmin.token,
|
||||
toIdentityProviderId(idpConfigresponse1),
|
||||
Seq.empty,
|
||||
)
|
||||
_ <- stub(canReadAsAdmin.token).deleteUser(
|
||||
DeleteUserRequest(
|
||||
userId = userId,
|
||||
identityProviderId = toIdentityProviderId(idpConfigresponse2),
|
||||
)
|
||||
)
|
||||
} yield ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,25 +7,29 @@ import java.util.UUID
|
||||
|
||||
import com.daml.ledger.api.v1.admin.user_management_service._
|
||||
import io.grpc.{Status, StatusRuntimeException}
|
||||
|
||||
import com.daml.test.evidence.scalatest.ScalaTestSupport.Implicits._
|
||||
import scala.concurrent.Future
|
||||
|
||||
class GetUserWithGivenUserIdAuthIT extends AdminOrIDPAdminServiceCallAuthTests {
|
||||
class GetUserWithGivenUserIdAuthIT
|
||||
extends AdminOrIDPAdminServiceCallAuthTests
|
||||
with UserManagementAuth {
|
||||
override def serviceCallName: String = "UserManagementService#GetUser(given-user-id)"
|
||||
|
||||
// admin and idp admin users are allowed to specify a user-id other than their own for which to retrieve a user
|
||||
override def serviceCall(context: ServiceCallContext): Future[Any] = {
|
||||
import context._
|
||||
val testId = UUID.randomUUID().toString
|
||||
|
||||
def getUser(userId: String): Future[User] =
|
||||
stub(UserManagementServiceGrpc.stub(channel), token)
|
||||
.getUser(GetUserRequest(userId, identityProviderId = identityProviderId))
|
||||
stub(UserManagementServiceGrpc.stub(channel), context.token)
|
||||
.getUser(GetUserRequest(userId, identityProviderId = context.identityProviderId))
|
||||
.map(_.user.get)
|
||||
|
||||
for {
|
||||
// create a normal users
|
||||
(alice, _) <- createUserByAdmin(testId + "-alice", identityProviderId = identityProviderId)
|
||||
(alice, _) <- createUserByAdmin(
|
||||
testId + "-alice",
|
||||
identityProviderId = context.identityProviderId,
|
||||
)
|
||||
|
||||
_ <- getUser(alice.id)
|
||||
|
||||
@ -41,4 +45,28 @@ class GetUserWithGivenUserIdAuthIT extends AdminOrIDPAdminServiceCallAuthTests {
|
||||
})
|
||||
} yield ()
|
||||
}
|
||||
|
||||
it should "deny calls if user is created already within another IDP" taggedAs adminSecurityAsset
|
||||
.setAttack(
|
||||
attackPermissionDenied(threat = "Present an existing userId but foreign Identity Provider")
|
||||
) in {
|
||||
expectPermissionDenied {
|
||||
val userId = "fresh-user-" + UUID.randomUUID().toString
|
||||
for {
|
||||
idpConfigresponse1 <- createConfig(canReadAsAdminStandardJWT)
|
||||
idpConfigresponse2 <- createConfig(canReadAsAdminStandardJWT)
|
||||
_ <- createFreshUser(
|
||||
userId,
|
||||
canReadAsAdmin.token,
|
||||
toIdentityProviderId(idpConfigresponse1),
|
||||
Seq.empty,
|
||||
)
|
||||
_ <- stub(UserManagementServiceGrpc.stub(channel), canReadAsAdmin.token)
|
||||
.getUser(
|
||||
GetUserRequest(userId, identityProviderId = toIdentityProviderId(idpConfigresponse2))
|
||||
)
|
||||
.map(_.user.get)
|
||||
} yield ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ package com.daml.platform.sandbox.auth
|
||||
|
||||
import com.daml.ledger.api.v1.admin.{user_management_service => ums}
|
||||
import com.daml.ledger.api.v1.admin.user_management_service.GrantUserRightsRequest
|
||||
import com.daml.test.evidence.scalatest.ScalaTestSupport.Implicits._
|
||||
|
||||
import java.util.UUID
|
||||
import scala.concurrent.Future
|
||||
|
||||
final class GrantUserRightsAuthIT
|
||||
@ -31,4 +33,29 @@ final class GrantUserRightsAuthIT
|
||||
)
|
||||
} yield {}
|
||||
|
||||
it should "deny calls if user is created already within another IDP" taggedAs adminSecurityAsset
|
||||
.setAttack(
|
||||
attackPermissionDenied(threat = "Present an existing userId but foreign Identity Provider")
|
||||
) in {
|
||||
expectPermissionDenied {
|
||||
val userId = "fresh-user-" + UUID.randomUUID().toString
|
||||
for {
|
||||
idpConfigresponse1 <- createConfig(canReadAsAdminStandardJWT)
|
||||
idpConfigresponse2 <- createConfig(canReadAsAdminStandardJWT)
|
||||
_ <- createFreshUser(
|
||||
userId,
|
||||
canReadAsAdmin.token,
|
||||
toIdentityProviderId(idpConfigresponse1),
|
||||
Seq.empty,
|
||||
)
|
||||
_ <- stub(canReadAsAdmin.token).grantUserRights(
|
||||
GrantUserRightsRequest(
|
||||
userId = userId,
|
||||
rights = scala.Seq(idpAdminPermission),
|
||||
identityProviderId = toIdentityProviderId(idpConfigresponse2),
|
||||
)
|
||||
)
|
||||
} yield ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,9 @@ import com.daml.ledger.api.v1.admin.user_management_service.{
|
||||
RevokeUserRightsRequest,
|
||||
}
|
||||
import com.daml.ledger.api.v1.admin.{user_management_service => ums}
|
||||
import com.daml.test.evidence.scalatest.ScalaTestSupport.Implicits._
|
||||
|
||||
import java.util.UUID
|
||||
import scala.concurrent.Future
|
||||
|
||||
final class RevokeUserRightsAuthIT
|
||||
@ -41,4 +43,37 @@ final class RevokeUserRightsAuthIT
|
||||
)
|
||||
} yield {}
|
||||
|
||||
it should "deny calls if user is created already within another IDP" taggedAs adminSecurityAsset
|
||||
.setAttack(
|
||||
attackPermissionDenied(threat = "Present an existing userId but foreign Identity Provider")
|
||||
) in {
|
||||
expectPermissionDenied {
|
||||
val userId = "fresh-user-" + UUID.randomUUID().toString
|
||||
for {
|
||||
idpConfigresponse1 <- createConfig(canReadAsAdminStandardJWT)
|
||||
idpConfigresponse2 <- createConfig(canReadAsAdminStandardJWT)
|
||||
_ <- createFreshUser(
|
||||
userId,
|
||||
canReadAsAdmin.token,
|
||||
toIdentityProviderId(idpConfigresponse1),
|
||||
Seq.empty,
|
||||
)
|
||||
_ <- stub(canReadAsAdminStandardJWT.token).grantUserRights(
|
||||
GrantUserRightsRequest(
|
||||
userId = userId,
|
||||
rights = scala.Seq(idpAdminPermission),
|
||||
identityProviderId = toIdentityProviderId(idpConfigresponse1),
|
||||
)
|
||||
)
|
||||
_ <- stub(canReadAsAdminStandardJWT.token).revokeUserRights(
|
||||
RevokeUserRightsRequest(
|
||||
userId = userId,
|
||||
rights = scala.Seq(idpAdminPermission),
|
||||
identityProviderId = toIdentityProviderId(idpConfigresponse2),
|
||||
)
|
||||
)
|
||||
} yield ()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ package com.daml.platform.sandbox.auth
|
||||
|
||||
import com.daml.ledger.api.v1.admin.user_management_service.UpdateUserRequest
|
||||
import com.google.protobuf.field_mask.FieldMask
|
||||
import com.daml.test.evidence.scalatest.ScalaTestSupport.Implicits._
|
||||
|
||||
import java.util.UUID
|
||||
import scala.concurrent.Future
|
||||
|
||||
final class UpdateUserAuthIT extends AdminOrIDPAdminServiceCallAuthTests with UserManagementAuth {
|
||||
@ -23,4 +25,31 @@ final class UpdateUserAuthIT extends AdminOrIDPAdminServiceCallAuthTests with Us
|
||||
)
|
||||
} yield ()
|
||||
|
||||
it should "deny calls if user is created already within another IDP" taggedAs adminSecurityAsset
|
||||
.setAttack(
|
||||
attackPermissionDenied(threat = "Present an existing userId but foreign Identity Provider")
|
||||
) in {
|
||||
expectPermissionDenied {
|
||||
val userId = "fresh-user-" + UUID.randomUUID().toString
|
||||
for {
|
||||
idpConfigresponse1 <- createConfig(canReadAsAdminStandardJWT)
|
||||
idpConfigresponse2 <- createConfig(canReadAsAdminStandardJWT)
|
||||
createUserResponse <- createFreshUser(
|
||||
userId,
|
||||
canReadAsAdmin.token,
|
||||
toIdentityProviderId(idpConfigresponse1),
|
||||
Seq.empty,
|
||||
)
|
||||
_ <- stub(canReadAsAdmin.token).updateUser(
|
||||
UpdateUserRequest(
|
||||
user = createUserResponse.user.map(user =>
|
||||
user.copy(identityProviderId = toIdentityProviderId(idpConfigresponse2))
|
||||
),
|
||||
updateMask = Some(FieldMask(scala.Seq("is_deactivated"))),
|
||||
)
|
||||
)
|
||||
} yield ()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ trait AdminOrIDPAdminServiceCallAuthTests
|
||||
identityProviderConfig = response.identityProviderConfig
|
||||
.getOrElse(sys.error("Failed to create idp config"))
|
||||
tokenIssuer = Some(identityProviderConfig.issuer)
|
||||
_ <- serviceCallWithIDPUser(idpAdminRights, identityProviderId(response), tokenIssuer)
|
||||
_ <- serviceCallWithIDPUser(idpAdminRights, toIdentityProviderId(response), tokenIssuer)
|
||||
} yield ()
|
||||
}
|
||||
}
|
||||
@ -56,7 +56,7 @@ trait AdminOrIDPAdminServiceCallAuthTests
|
||||
expectUnauthenticated {
|
||||
for {
|
||||
response <- createConfig(canReadAsAdminStandardJWT)
|
||||
_ <- serviceCallWithIDPUser(idpAdminRights, identityProviderId(response), None)
|
||||
_ <- serviceCallWithIDPUser(idpAdminRights, toIdentityProviderId(response), None)
|
||||
} yield ()
|
||||
}
|
||||
}
|
||||
@ -76,7 +76,7 @@ trait AdminOrIDPAdminServiceCallAuthTests
|
||||
response1 <- createConfig(canReadAsAdminStandardJWT, identityProviderConfig)
|
||||
(_, context) <- createUserByAdmin(
|
||||
userId = UUID.randomUUID().toString,
|
||||
identityProviderId = identityProviderId(response1),
|
||||
identityProviderId = toIdentityProviderId(response1),
|
||||
tokenIssuer = Some(tokenIssuer),
|
||||
rights = idpAdminRights.map(proto.Right(_)),
|
||||
)
|
||||
@ -100,7 +100,7 @@ trait AdminOrIDPAdminServiceCallAuthTests
|
||||
identityProviderConfig = response.identityProviderConfig
|
||||
.getOrElse(sys.error("Failed to create idp config"))
|
||||
tokenIssuer = Some(identityProviderConfig.issuer)
|
||||
_ <- serviceCallWithIDPUser(Vector(), identityProviderId(response), tokenIssuer)
|
||||
_ <- serviceCallWithIDPUser(Vector(), toIdentityProviderId(response), tokenIssuer)
|
||||
} yield ()
|
||||
}
|
||||
}
|
||||
@ -116,11 +116,11 @@ trait AdminOrIDPAdminServiceCallAuthTests
|
||||
tokenIssuer1 = Some(identityProviderConfig1.issuer)
|
||||
_ <- createUserByAdmin(
|
||||
userId = UUID.randomUUID().toString,
|
||||
identityProviderId = identityProviderId(response1),
|
||||
identityProviderId = toIdentityProviderId(response1),
|
||||
tokenIssuer = tokenIssuer1,
|
||||
rights = idpAdminRights.map(proto.Right(_)),
|
||||
).flatMap { case (_, context) =>
|
||||
serviceCall(context.copy(identityProviderId = identityProviderId(response2)))
|
||||
serviceCall(context.copy(identityProviderId = toIdentityProviderId(response2)))
|
||||
}
|
||||
} yield ()
|
||||
}
|
||||
@ -169,12 +169,12 @@ trait AdminOrIDPAdminServiceCallAuthTests
|
||||
identityProviderConfig = response.identityProviderConfig
|
||||
.getOrElse(sys.error("Failed to create idp config"))
|
||||
tokenIssuer = Some(identityProviderConfig.issuer)
|
||||
_ <- serviceCallWithIDPUser(idpAdminRights, identityProviderId(response), tokenIssuer)
|
||||
_ <- serviceCallWithIDPUser(idpAdminRights, toIdentityProviderId(response), tokenIssuer)
|
||||
} yield ()
|
||||
}
|
||||
}
|
||||
|
||||
private def identityProviderId(response: CreateIdentityProviderConfigResponse): String = {
|
||||
def toIdentityProviderId(response: CreateIdentityProviderConfigResponse): String = {
|
||||
val identityProviderConfig = response.identityProviderConfig
|
||||
.getOrElse(sys.error("Failed to create idp config"))
|
||||
identityProviderConfig.identityProviderId
|
||||
|
@ -27,6 +27,15 @@ trait UserManagementAuth {
|
||||
rights: scala.Seq[Right] = scala.Seq.empty,
|
||||
): Future[CreateUserResponse] = {
|
||||
val userId = "fresh-user-" + UUID.randomUUID().toString
|
||||
createFreshUser(userId, token, identityProviderId, rights)
|
||||
}
|
||||
|
||||
def createFreshUser(
|
||||
userId: String,
|
||||
token: Option[String],
|
||||
identityProviderId: String,
|
||||
rights: scala.Seq[Right],
|
||||
): Future[CreateUserResponse] = {
|
||||
val req = CreateUserRequest(
|
||||
user = Some(User(userId, identityProviderId = identityProviderId)),
|
||||
rights = rights,
|
||||
|
Loading…
Reference in New Issue
Block a user