Implement IDP ID check transactionally within persistence layer [DPP-1386] (#16134)

This commit is contained in:
Sergey Kisel 2023-01-26 11:21:18 +01:00 committed by GitHub
parent 232774a3bf
commit efcc6620ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 453 additions and 192 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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