Skip to content

Commit

Permalink
[User management] Enforce 1k user rights limit [DPP-833] (digital-ass…
Browse files Browse the repository at this point in the history
…et#12558)

CHANGELOG_BEGIN
Ledger API Specification: Maximum number of user rights per user is now limited to 1000 and is added to UserManagementFeature in VersionService. getLedgerApiVersion endpoint.
CHANGELOG_END
  • Loading branch information
pbatko-da authored Jan 27, 2022
1 parent 16a13e2 commit 4ec336d
Show file tree
Hide file tree
Showing 20 changed files with 228 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ message FeaturesDescriptor {

}

// Whether the Ledger API server provides the user management service.
message UserManagementFeature {
// Whether the Ledger API server provides the user management service.
bool supported = 1;
// The maximum number of rights that can be assigned to a single user.
// Value of 0 means that no limit is being enforced.
uint32 max_rights_per_user = 2;
}
Original file line number Diff line number Diff line change
Expand Up @@ -687,7 +687,7 @@ object LedgerApiErrors extends LedgerApiErrorGroup {
case class Reject(_operation: String, userId: String)(implicit
loggingContext: ContextualizedErrorLogger
) extends LoggingTransactionErrorImpl(
cause = s"cannot ${_operation} for unknown user \"${userId}\""
cause = s"${_operation} failed for unknown user \"${userId}\""
) {
override def resources: Seq[(ErrorResource, String)] = Seq(
ErrorResource.User -> userId
Expand All @@ -706,7 +706,31 @@ object LedgerApiErrors extends LedgerApiErrorGroup {
case class Reject(_operation: String, userId: String)(implicit
loggingContext: ContextualizedErrorLogger
) extends LoggingTransactionErrorImpl(
cause = s"cannot ${_operation}, as user \"${userId}\" already exists"
cause = s"${_operation} failed, as user \"${userId}\" already exists"
) {
override def resources: Seq[(ErrorResource, String)] = Seq(
ErrorResource.User -> userId
)
}
}

@Explanation(
"""|A user can have only a limited number of user rights.
|There was an attempt to create a user with too many rights or grant too many rights to a user."""
)
@Resolution(
"""|Retry with a smaller number of rights or delete some of the already existing rights of this user.
|Contact the participant operator if the limit is too low."""
)
object TooManyUserRights
extends ErrorCode(
id = "TOO_MANY_USER_RIGHTS",
ErrorCategory.InvalidGivenCurrentSystemStateOther,
) {
case class Reject(_operation: String, userId: String)(implicit
loggingContext: ContextualizedErrorLogger
) extends LoggingTransactionErrorImpl(
cause = s"${_operation} failed, as user \"${userId}\" would have too many rights."
) {
override def resources: Seq[(ErrorResource, String)] = Seq(
ErrorResource.User -> userId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ object Assertions {
if (checkDefiniteAnswerMetadata) assertDefiniteAnswer(exception)
additionalErrorAssertions(exception)
case exception: StatusRuntimeException if participant.features.selfServiceErrorCodes =>
assertSelfServiceErrorCode(exception, selfServiceErrorCode)
optPattern.foreach(assertMatches(exception.getMessage, _))
assertSelfServiceErrorCode(exception, selfServiceErrorCode)
if (checkDefiniteAnswerMetadata) assertDefiniteAnswer(exception)
additionalErrorAssertions(exception)
case _ =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import com.daml.ledger.api.v1.experimental_features.{
CommandDeduplicationFeatures,
ExperimentalContractIds,
}
import com.daml.ledger.api.v1.version_service.GetLedgerApiVersionResponse
import com.daml.ledger.api.v1.version_service.{GetLedgerApiVersionResponse, UserManagementFeature}

final case class Features(
userManagement: Boolean,
userManagement: UserManagementFeature,
selfServiceErrorCodes: Boolean,
staticTime: Boolean,
commandDeduplicationFeatures: CommandDeduplicationFeatures,
Expand All @@ -20,7 +20,7 @@ final case class Features(

object Features {
val defaultFeatures: Features = Features(
userManagement = false,
userManagement = UserManagementFeature.defaultInstance,
selfServiceErrorCodes = false,
staticTime = false,
commandDeduplicationFeatures = CommandDeduplicationFeatures.defaultInstance,
Expand All @@ -32,7 +32,7 @@ object Features {
val experimental = features.getExperimental

Features(
userManagement = features.getUserManagement.supported,
userManagement = features.getUserManagement,
selfServiceErrorCodes = experimental.selfServiceErrorCodes.isDefined,
staticTime = experimental.getStaticTime.supported,
commandDeduplicationFeatures = experimental.getCommandDeduplication,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,91 @@ import com.daml.ledger.api.v1.admin.user_management_service.{
import com.daml.ledger.api.v1.admin.{user_management_service => proto}
import io.grpc.Status

import scala.collection.immutable.Iterable
import scala.concurrent.{ExecutionContext, Future}

final class UserManagementServiceIT extends LedgerTestSuite {

private val adminPermission =
Permission(Permission.Kind.ParticipantAdmin(Permission.ParticipantAdmin()))
private val actAsPermission1 =
Permission(Permission.Kind.CanActAs(Permission.CanActAs("acting-party-1")))
private val readAsPermission1 =
Permission(Permission.Kind.CanReadAs(Permission.CanReadAs("reading-party-1")))
private val userRightsBatch = List(
actAsPermission1,
Permission(Permission.Kind.CanActAs(Permission.CanActAs("acting-party-2"))),
readAsPermission1,
Permission(Permission.Kind.CanReadAs(Permission.CanReadAs("reading-party-2"))),
)
private val AdminUserId = "participant_admin"

test(
"UserManagementUserRightsLimit",
"Test user rights per user limit",
allocate(NoParties),
enabled = _.userManagement.maxRightsPerUser > 0,
disabledReason = "requires user management feature with user rights limit",
)(implicit ec => { case Participants(Participant(ledger)) =>
def assertTooManyUserRightsError(t: Throwable): Unit = {
assertGrpcError(
participant = ledger,
t = t,
expectedCode = Status.Code.FAILED_PRECONDITION,
selfServiceErrorCode = LedgerApiErrors.AdminServices.TooManyUserRights,
exceptionMessageSubstring = None,
)
}

def createCanActAs(id: Int) =
Permission(Permission.Kind.CanActAs(Permission.CanActAs(s"acting-party-$id")))

val user1 = User(UUID.randomUUID.toString, "")
val user2 = User(UUID.randomUUID.toString, "")

val maxRightsPerUser = ledger.features.userManagement.maxRightsPerUser
val permissionsMaxAndOne = (1 to (maxRightsPerUser + 1)).map(createCanActAs)
val permissionOne = permissionsMaxAndOne.head
val permissionsMax = permissionsMaxAndOne.tail

for {
// cannot create user with #limit+1 rights
create1 <- ledger.userManagement
.createUser(CreateUserRequest(Some(user1), permissionsMaxAndOne))
.mustFail(
"creating user with too many rights"
)
// can create user with #limit rights
create2 <- ledger.userManagement.createUser(CreateUserRequest(Some(user1), permissionsMax))
// fails adding one more right
grant1 <- ledger.userManagement
.grantUserRights(GrantUserRightsRequest(user1.id, rights = Seq(permissionOne)))
.mustFail(
"granting more rights exceeds max number of user rights per user"
)
// rights already added are intact
rights1 <- ledger.userManagement.listUserRights(ListUserRightsRequest(user1.id))
// can create other users with #limit rights
create3 <- ledger.userManagement.createUser(CreateUserRequest(Some(user2), permissionsMax))
// cleanup
_ <- ledger.userManagement.deleteUser(DeleteUserRequest(user1.id))
_ <- ledger.userManagement.deleteUser(DeleteUserRequest(user2.id))

} yield {
assertTooManyUserRightsError(create1)
assertEquals(create2, user1)
assertTooManyUserRightsError(grant1)
assertEquals(rights1.rights.size, permissionsMaxAndOne.tail.size)
assertSameElements(rights1.rights, permissionsMaxAndOne.tail)
assertEquals(create3, user2)
}
})

test(
"UserManagementCreateUserInvalidArguments",
"Test argument validation for UserManagement#CreateUser",
allocate(NoParties),
enabled = _.userManagement,
enabled = _.userManagement.supported,
disabledReason = "requires user management feature",
)(implicit ec => { case Participants(Participant(ledger)) =>
val userId = UUID.randomUUID.toString
Expand Down Expand Up @@ -93,7 +169,7 @@ final class UserManagementServiceIT extends LedgerTestSuite {
"UserManagementGetUserInvalidArguments",
"Test argument validation for UserManagement#GetUser",
allocate(NoParties),
enabled = _.userManagement,
enabled = _.userManagement.supported,
disabledReason = "requires user management feature",
)(implicit ec => { case Participants(Participant(ledger)) =>
def getAndCheck(problem: String, userId: String, expectedErrorCode: ErrorCode): Future[Unit] =
Expand All @@ -109,20 +185,6 @@ final class UserManagementServiceIT extends LedgerTestSuite {
} yield ()
})

private val adminPermission =
Permission(Permission.Kind.ParticipantAdmin(Permission.ParticipantAdmin()))
private val actAsPermission1 =
Permission(Permission.Kind.CanActAs(Permission.CanActAs("acting-party-1")))
private val readAsPermission1 =
Permission(Permission.Kind.CanReadAs(Permission.CanReadAs("reading-party-1")))
private val userRightsBatch = List(
actAsPermission1,
Permission(Permission.Kind.CanActAs(Permission.CanActAs("acting-party-2"))),
readAsPermission1,
Permission(Permission.Kind.CanReadAs(Permission.CanReadAs("reading-party-2"))),
)
private val AdminUserId = "participant_admin"

userManagementTest(
"TestAdminExists",
"Ensure admin user exists",
Expand Down Expand Up @@ -224,6 +286,7 @@ final class UserManagementServiceIT extends LedgerTestSuite {
res5 <- ledger.userManagement.listUsers(ListUsersRequest())
} yield {
def filterUsers(users: Iterable[User]) = users.filter(u => u.id == userId1 || u.id == userId2)

assertSameElements(filterUsers(res1.users), Seq(user1))
assertEquals(res2, user2)
assertSameElements(
Expand Down Expand Up @@ -344,7 +407,7 @@ final class UserManagementServiceIT extends LedgerTestSuite {
shortIdentifier = shortIdentifier,
description = description,
allocate(NoParties),
enabled = _.userManagement,
enabled = _.userManagement.supported,
disabledReason = "requires user management feature",
)(implicit ec => { case Participants(Participant(ledger)) =>
body(ec)(ledger)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.daml.logging.{ContextualizedLogger, LoggingContext}
import com.daml.platform.api.grpc.GrpcApiService
import com.daml.platform.apiserver.LedgerFeatures
import com.daml.platform.server.api.validation.ErrorFactories
import com.daml.platform.usermanagement.UserManagementConfig
import io.grpc.ServerServiceDefinition

import scala.concurrent.{ExecutionContext, Future}
Expand Down Expand Up @@ -54,7 +55,12 @@ private[apiserver] final class ApiVersionService private (

private val featuresDescriptor =
FeaturesDescriptor.of(
userManagement = Some(UserManagementFeature(supported = enableUserManagement)),
userManagement = Some(
UserManagementFeature(
supported = enableUserManagement,
maxRightsPerUser = if (enableUserManagement) UserManagementConfig.MaxRightsPerUser else 0,
)
),
experimental = Some(
ExperimentalFeatures.of(
selfServiceErrorCodes =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ private[apiserver] final class ApiUserManagementService(
user = user,
rights = pRights,
)
.flatMap(handleResult("create user"))
.flatMap(handleResult("creating user"))
.map(_ => request.user.get)
}

Expand All @@ -68,7 +68,7 @@ private[apiserver] final class ApiUserManagementService(
)(userId =>
userManagementService
.getUser(userId)
.flatMap(handleResult("get user"))
.flatMap(handleResult("getting user"))
.map(toProtoUser)
)

Expand All @@ -78,14 +78,14 @@ private[apiserver] final class ApiUserManagementService(
)(userId =>
userManagementService
.deleteUser(userId)
.flatMap(handleResult("delete user"))
.flatMap(handleResult("deleting user"))
.map(_ => proto.DeleteUserResponse())
)

override def listUsers(request: proto.ListUsersRequest): Future[proto.ListUsersResponse] =
userManagementService
.listUsers()
.flatMap(handleResult("list users"))
.flatMap(handleResult("listing users"))
.map(
_.map(toProtoUser)
)
Expand Down Expand Up @@ -154,6 +154,11 @@ private[apiserver] final class ApiUserManagementService(
LedgerApiErrors.AdminServices.UserAlreadyExists.Reject(operation, id.toString).asGrpcError
)

case Left(UserManagementStore.TooManyUserRights(id)) =>
Future.failed(
LedgerApiErrors.AdminServices.TooManyUserRights.Reject(operation, id: String).asGrpcError
)

case scala.util.Right(t) =>
Future.successful(t)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,8 @@ trait UserManagementStorageBackend {

def getUserRights(internalId: Int)(connection: Connection): Set[UserRight]

def countUserRights(internalId: Int)(connection: Connection): Int

}

object UserManagementStorageBackend {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ object UserManagementStorageBackendTemplate extends UserManagementStorageBackend
updatedRowCount == 1
}

override def countUserRights(internalId: Int)(connection: Connection): Int = {
SQL"SELECT count(*) AS user_rights_count from participant_user_rights WHERE user_internal_id = ${internalId}"
.as(SqlParser.int("user_rights_count").single)(connection)
}

private def makeUserRight(value: Int, partyRaw: Option[String]): UserRight = {
val partyO = dbStringToPartyString(partyRaw)
(value, partyO) match {
Expand Down
Loading

0 comments on commit 4ec336d

Please sign in to comment.