Skip to content

Commit

Permalink
Add Ledger API scala client for user management (#12000)
Browse files Browse the repository at this point in the history
scala bindings: add client for user management API

CHANGELOG_BEGIN
- [Scala bindings] add methods to call the user management API
CHANGELOG_END
  • Loading branch information
meiersi-da authored Dec 7, 2021
1 parent 444b3ac commit ef4ae93
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ sealed abstract class IdString {
/** Identifiers for contracts */
type ContractIdString <: String

/** Identifiers for participant node users */
type UserId <: String

val HexString: HexStringModule[HexString]
val Name: StringModule[Name]
val PackageName: ConcatenableStringModule[PackageName, HexString]
Expand All @@ -107,6 +110,7 @@ sealed abstract class IdString {
val ParticipantId: StringModule[ParticipantId]
val LedgerString: ConcatenableStringModule[LedgerString, HexString]
val ContractIdString: StringModule[ContractIdString]
val UserId: StringModule[UserId]
}

object IdString {
Expand Down Expand Up @@ -333,4 +337,17 @@ private[data] final class IdStringImpl extends IdString {
override val ContractIdString: StringModule[ContractIdString] =
new MatchingStringModule("Daml-LF Contract ID", """#[\w._:\-#/ ]{0,254}""")

/** Identifiers for participant node users consist of ASCII digits and lower-case alphabetic characters,
* hyphens, underscores, and dots, and satisfy the following rules:
* 1. The characters `-._` never follow each other.
* 2. The characters `-._` neither occur at the start nor at the end of the user name.
* Thus 'john.doe1' is a valid user-name, while 'john..doe1', 'john.-doe1', and '-john.doe' are not.
*/
override type UserId = String
override val UserId: StringModule[UserId] =
new MatchingStringModule(
"User ID",
"""[\p{Lower}\d]([\p{Lower}\d]|[-._][\p{Lower}\d]){0,62}""",
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ object Ref {
type ApplicationId = LedgerString
val ApplicationId: LedgerString.type = LedgerString

/** Identifiers for participant node users, which act as clients to the Ledger API.
*
* The concept of participant node users has been introduced after the concept
* of client applications, which is why the code contains both of them.
* The [[UserId]] identifiers for participant node users are a strict subset of the
* [[ApplicationId]] identifiers for Ledger API client applications.
* The Ledger API and its backing code use [[UserId]] where possible
* and [[ApplicationId]] otherwise.
*/
type UserId = IdString.UserId
val UserId: IdString.UserId.type = IdString.UserId

/** Identifiers used to correlate a command submission with its result. */
type CommandId = LedgerString
val CommandId: LedgerString.type = LedgerString
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,37 @@ class RefTest extends AnyFreeSpec with Matchers with EitherValues {
}
}

"UserId" - {
"accept valid user names" in {
UserId.fromString("a") shouldBe a[Right[_, _]]
UserId.fromString("john.doe") shouldBe a[Right[_, _]]
UserId.fromString("john-doe") shouldBe a[Right[_, _]]
UserId.fromString("john_doe") shouldBe a[Right[_, _]]
UserId.fromString("jo1hn.d200oe") shouldBe a[Right[_, _]]
UserId.fromString("john-do1.e") shouldBe a[Right[_, _]]
UserId.fromString("123e4567-e89b-12d3-a456-426614174000") shouldBe a[Right[_, _]]
}

"reject invalid user names" in {
UserId.fromString("john|doe") shouldBe a[Left[_, _]]
UserId.fromString("john#doe") shouldBe a[Left[_, _]]
UserId.fromString("john@doe") shouldBe a[Left[_, _]]
UserId.fromString("john..doe") shouldBe a[Left[_, _]]
UserId.fromString("john.-doe") shouldBe a[Left[_, _]]
UserId.fromString("john--doe") shouldBe a[Left[_, _]]
UserId.fromString("johndoe.") shouldBe a[Left[_, _]]
UserId.fromString("johndoe-") shouldBe a[Left[_, _]]
UserId.fromString("-johndoe") shouldBe a[Left[_, _]]
}

"reject too long strings" in {
val negativeTestCase = "a" * 63
val positiveTestCase1 = "a" * 64
UserId.fromString(negativeTestCase) shouldBe a[Right[_, _]]
UserId.fromString(positiveTestCase1) shouldBe a[Left[_, _]]
}
}

private def testOrdered[X <: Ordered[X]](name: String, elems: Iterable[X]) =
s"$name#compare" - {
"agrees with equality" in {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package com.daml.ledger.client.services.admin

import com.daml.ledger.api.domain
import com.daml.ledger.api.domain.{UserId, User, UserRight}
import com.daml.ledger.api.v1.admin.user_management_service.UserManagementServiceGrpc.UserManagementServiceStub
import com.daml.ledger.api.v1.admin.{user_management_service => proto}
import com.daml.ledger.client.LedgerClient
import com.daml.lf.data.Ref
import com.daml.lf.data.Ref.Party

import scala.concurrent.{ExecutionContext, Future}

final class UserManagementClient(service: UserManagementServiceStub)(implicit
ec: ExecutionContext
) {
import UserManagementClient._

def createUser(
user: User,
initialRights: Seq[UserRight] = List.empty,
token: Option[String] = None,
): Future[User] = {
val request = proto.CreateUserRequest(
Some(UserManagementClient.toProtoUser(user)),
initialRights.view.map(toProtoRight).toList,
)
LedgerClient
.stub(service, token)
.createUser(request)
.map(fromProtoUser)
}

def getUser(userId: UserId, token: Option[String] = None): Future[User] =
LedgerClient
.stub(service, token)
.getUser(proto.GetUserRequest(userId.toString))
.map(fromProtoUser)

/** Retrieve the User information for the user authenticated by the token(s) on the call . */
def getAuthenticatedUser(token: Option[String] = None): Future[User] =
LedgerClient
.stub(service, token)
.getUser(proto.GetUserRequest())
.map(fromProtoUser)

def deleteUser(userId: UserId, token: Option[String] = None): Future[Unit] =
LedgerClient
.stub(service, token)
.deleteUser(proto.DeleteUserRequest(userId.toString))
.map(_ => ())

def listUsers(token: Option[String] = None): Future[Vector[User]] =
LedgerClient
.stub(service, token)
.listUsers(proto.ListUsersRequest())
.map(_.users.view.map(fromProtoUser).toVector)

def grantUserRights(
userId: UserId,
rights: Seq[UserRight],
token: Option[String] = None,
): Future[Vector[UserRight]] =
LedgerClient
.stub(service, token)
.grantUserRights(proto.GrantUserRightsRequest(userId.toString, rights.map(toProtoRight)))
.map(_.newlyGrantedRights.view.collect(fromProtoRight.unlift).toVector)

def revokeUserRights(
userId: UserId,
rights: Seq[UserRight],
token: Option[String] = None,
): Future[Vector[UserRight]] =
LedgerClient
.stub(service, token)
.revokeUserRights(proto.RevokeUserRightsRequest(userId.toString, rights.map(toProtoRight)))
.map(_.newlyRevokedRights.view.collect(fromProtoRight.unlift).toVector)

/** List the rights of the given user.
* Unknown rights are ignored.
*/
def listUserRights(userId: UserId, token: Option[String] = None): Future[Vector[UserRight]] =
LedgerClient
.stub(service, token)
.listUserRights(proto.ListUserRightsRequest(userId.toString))
.map(_.rights.view.collect(fromProtoRight.unlift).toVector)

/** Retrieve the rights of the user authenticated by the token(s) on the call .
* Unknown rights are ignored.
*/
def listAuthenticatedUserRights(token: Option[String] = None): Future[Vector[UserRight]] =
LedgerClient
.stub(service, token)
.listUserRights(proto.ListUserRightsRequest())
.map(_.rights.view.collect(fromProtoRight.unlift).toVector)
}

object UserManagementClient {
private def fromProtoUser(user: proto.User): User =
User(
UserId(Ref.UserId.assertFromString(user.id)),
Option.unless(user.primaryParty.isEmpty)(Party.assertFromString(user.primaryParty)),
)

private def toProtoUser(user: User): proto.User =
proto.User(user.id.toString, user.primaryParty.fold("")(_.toString))

private val toProtoRight: domain.UserRight => proto.Right = {
case domain.UserRight.ParticipantAdmin =>
proto.Right(proto.Right.Kind.ParticipantAdmin(proto.Right.ParticipantAdmin()))
case domain.UserRight.CanActAs(party) =>
proto.Right(proto.Right.Kind.CanActAs(proto.Right.CanActAs(party)))
case domain.UserRight.CanReadAs(party) =>
proto.Right(proto.Right.Kind.CanReadAs(proto.Right.CanReadAs(party)))
}

private val fromProtoRight: proto.Right => Option[domain.UserRight] = {
case proto.Right(_: proto.Right.Kind.ParticipantAdmin) =>
Some(domain.UserRight.ParticipantAdmin)
case proto.Right(proto.Right.Kind.CanActAs(x)) =>
// Note: assertFromString is OK here, as the server should deliver valid party identifiers.
Some(domain.UserRight.CanActAs(Ref.Party.assertFromString(x.party)))
case proto.Right(proto.Right.Kind.CanReadAs(x)) =>
Some(domain.UserRight.CanReadAs(Ref.Party.assertFromString(x.party)))
case proto.Right(proto.Right.Kind.Empty) =>
None // The server sent a right of a kind that this client doesn't know about.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,18 @@ object domain {

sealed trait ApplicationIdTag

/** Identifiers for applications connecting to the Ledger API.
*/
type ApplicationId = Ref.ApplicationId @@ ApplicationIdTag
val ApplicationId: Tag.TagOf[ApplicationIdTag] = Tag.of[ApplicationIdTag]

/** Identifiers for participant node users interacting with the Ledger API.
*
* These are a subset of [[ApplicationId]]. See [[Ref.UserId]] for details.
*/
type UserId = Ref.UserId @@ ApplicationIdTag
val UserId: Tag.TagOf[ApplicationIdTag] = Tag.of[ApplicationIdTag]

sealed trait SubmissionIdTag

type SubmissionId = Ref.SubmissionId @@ SubmissionIdTag
Expand Down Expand Up @@ -392,4 +401,16 @@ object domain {
implicit def `tagged value to LoggingValue`[T: ToLoggingValue, Tag]: ToLoggingValue[T @@ Tag] =
value => value.unwrap
}

final case class User(
id: UserId,
primaryParty: Option[Ref.Party],
)

sealed trait UserRight
object UserRight {
final case object ParticipantAdmin extends UserRight
final case class CanActAs(party: Ref.Party) extends UserRight
final case class CanReadAs(party: Ref.Party) extends UserRight
}
}

0 comments on commit ef4ae93

Please sign in to comment.