From 0c13a4f1fd19132f8a1764f85b4746aba10f7bd4 Mon Sep 17 00:00:00 2001 From: nickchapman-da <49153372+nickchapman-da@users.noreply.github.com> Date: Tue, 18 Jan 2022 12:58:51 +0000 Subject: [PATCH] Error handling for User Management exposed via daml-script (#12416) * Error handling for User Management exposed via daml-script changelog_begin changelog_end adapt ScriptTest.daml to new user-management interface adapt create-daml-app Setup.daml to new user-management interface * Add deriving Ord for UserId * change example of invalid user-id char to "%" from "." (which is no longer illegal) * recover/reify ALREADY_EXISTS from GrpcLedgerClient.createuser * fix testcase expected order of users from daml-script listUsers * adapt create-saml-app Setup.daml to changed interface of user-management * reinstate sort lost in merge * sort user in ScriptService user-management test * improve comment for error foobar hack * improve doc comment for validateUserId * use upper case as test example for invalid user-id --- .../damlc/tests/src/DA/Test/ScriptService.hs | 86 +++++----- daml-script/daml/Daml/Script.daml | 149 +++++++++++++----- .../daml/lf/engine/script/Converter.scala | 19 ++- .../daml/lf/engine/script/ScriptF.scala | 75 +++++++-- .../ledgerinteraction/GrpcLedgerClient.scala | 34 ++-- .../ledgerinteraction/IdeLedgerClient.scala | 35 ++-- .../ledgerinteraction/JsonLedgerClient.scala | 10 +- .../ScriptLedgerClient.scala | 10 +- daml-script/test/daml/ScriptTest.daml | 63 ++++---- templates/create-daml-app/daml/Setup.daml | 15 +- 10 files changed, 340 insertions(+), 156 deletions(-) diff --git a/compiler/damlc/tests/src/DA/Test/ScriptService.hs b/compiler/damlc/tests/src/DA/Test/ScriptService.hs index 1bac74e1d91b..d38cf5f2e2c4 100644 --- a/compiler/damlc/tests/src/DA/Test/ScriptService.hs +++ b/compiler/damlc/tests/src/DA/Test/ScriptService.hs @@ -932,70 +932,84 @@ main = [ "module Test where" , "import DA.Assert" , "import Daml.Script" + , "import DA.List (sort)" + , "isValidUserId : Text -> Script Bool" + , "isValidUserId name = try do _ <- validateUserId name; pure True catch InvalidUserId _ -> pure False" + , "userExists : UserId -> Script Bool" + , "userExists u = do try do _ <- getUser u; pure True catch UserNotFound _ -> pure False" + , "expectUserNotFound : Script a -> Script ()" + , "expectUserNotFound script = try do _ <- script; undefined catch UserNotFound _ -> pure ()" , "testUserManagement = do" + , " True <- isValidUserId \"good\"" + , " False <- isValidUserId \"BAD\"" + , " u1 <- validateUserId \"user1\"" + , " u2 <- validateUserId \"user2\"" + , " let user1 = User u1 None" + , " let user2 = User u2 None" + , " userName u1 === \"user1\"" + , " userName u2 === \"user2\"" , " users <- listUsers" , " users === []" - , " u1 <- createUser (User \"u1\" None) []" - , " u1 === User \"u1\" None" - , " u2 <- createUser (User \"u2\" None) []" - , " u2 === User \"u2\" None" - , " u <- getUser \"u1\"" - , " u === Some u1" - , " u <- getUser \"u2\"" - , " u === Some u2" - , " u <- getUser \"nonexistent\"" - , " u === None" + , " createUser user1 []" + , " True <- userExists u1" + , " False <- userExists u2" + , " try do _ <- createUser user1 []; undefined catch UserAlreadyExists _ -> pure ()" + , " createUser user2 []" + , " True <- userExists u1" + , " True <- userExists u2" + , " u <- getUser u1" + , " u === user1" + , " u <- getUser u2" + , " u === user2" , " users <- listUsers" - , " users === [User \"u1\" None, User \"u2\" None]" - , " deleteUser \"u1\"" + , " sort users === [user1, user2]" + , " deleteUser u1" , " users <- listUsers" - , " users === [User \"u2\" None]" - , " deleteUser \"u2\"" + , " users === [user2]" + , " deleteUser u2" , " users <- listUsers" , " users === []" + , " nonexistent <- validateUserId \"nonexistent\"" + , " expectUserNotFound (getUser nonexistent)" + , " expectUserNotFound (deleteUser nonexistent)" , " pure ()" , "testUserRightManagement = do" , " p1 <- allocateParty \"p1\"" , " p2 <- allocateParty \"p2\"" - , " u1 <- createUser (User \"u1\" None) []" - , " rights <- listUserRights \"u1\"" + , " u1 <- validateUserId \"user1\"" + , " createUser (User u1 None) []" + , " rights <- listUserRights u1" , " rights === []" - , " newRights <- grantUserRights \"u1\" [ParticipantAdmin]" + , " newRights <- grantUserRights u1 [ParticipantAdmin]" , " newRights === [ParticipantAdmin]" - , " newRights <- grantUserRights \"u1\" [ParticipantAdmin]" + , " newRights <- grantUserRights u1 [ParticipantAdmin]" , " newRights === []" - , " rights <- listUserRights \"u1\"" + , " rights <- listUserRights u1" , " rights === [ParticipantAdmin]" - , " newRights <- grantUserRights \"u1\" [CanActAs p1, CanReadAs p2]" + , " newRights <- grantUserRights u1 [CanActAs p1, CanReadAs p2]" , " newRights === [CanActAs p1, CanReadAs p2]" - , " rights <- listUserRights \"u1\"" + , " rights <- listUserRights u1" , " rights === [ParticipantAdmin, CanActAs p1, CanReadAs p2]" - , " revoked <- revokeUserRights \"u1\" [ParticipantAdmin]" + , " revoked <- revokeUserRights u1 [ParticipantAdmin]" , " revoked === [ParticipantAdmin]" - , " revoked <- revokeUserRights \"u1\" [ParticipantAdmin]" + , " revoked <- revokeUserRights u1 [ParticipantAdmin]" , " revoked === []" - , " rights <- listUserRights \"u1\"" + , " rights <- listUserRights u1" , " rights === [CanActAs p1, CanReadAs p2]" - , " revoked <- revokeUserRights \"u1\" [CanActAs p1, CanReadAs p2]" + , " revoked <- revokeUserRights u1 [CanActAs p1, CanReadAs p2]" , " revoked === [CanActAs p1, CanReadAs p2]" - , " rights <- listUserRights \"u1\"" + , " rights <- listUserRights u1" , " rights === []" - , "testUserAlreadyExists = do" - , " u1 <- createUser (User \"u1\" None) []" - , " u2 <- createUser (User \"u1\" None) []" - , " pure ()" - , "testUserNotFound = do" - , " deleteUser \"nonexistent\"" + , " nonexistent <- validateUserId \"nonexistent\"" + , " expectUserNotFound (listUserRights nonexistent)" + , " expectUserNotFound (revokeUserRights nonexistent [])" + , " expectUserNotFound (grantUserRights nonexistent [])" , " pure ()" ] expectScriptSuccess rs (vr "testUserManagement") $ \r -> matchRegex r "Active contracts: \n" expectScriptSuccess rs (vr "testUserRightManagement") $ \r -> matchRegex r "Active contracts: \n" - expectScriptFailure rs (vr "testUserAlreadyExists") $ \r -> - matchRegex r "User already exists: u1\n" - expectScriptFailure rs (vr "testUserNotFound") $ \r -> - matchRegex r "User not found: nonexistent\n" ] where scenarioConfig = SS.defaultScenarioServiceConfig {SS.cnfJvmOptions = ["-Xmx200M"]} diff --git a/daml-script/daml/Daml/Script.daml b/daml-script/daml/Daml/Script.daml index a6edb6f038d1..727ad2144999 100644 --- a/daml-script/daml/Daml/Script.daml +++ b/daml-script/daml/Daml/Script.daml @@ -52,8 +52,14 @@ module Daml.Script , AnyContractId , fromAnyContractId + , UserId + , InvalidUserId(..) + , UserAlreadyExists(..) + , UserNotFound(..) , User(..) , UserRight(..) + , userName + , validateUserId , createUser , createUserOn , getUser @@ -127,6 +133,7 @@ data ScriptF a | GetTime (GetTimePayload a) | SetTime (SetTimePayload a) | Sleep (SleepRec a) + | ValidateUserId (ValidateUserIdPayload a) | CreateUser (CreateUserPayload a) | GetUser (GetUserPayload a) | DeleteUser (DeleteUserPayload a) @@ -506,7 +513,7 @@ instance CanAssert Script where data LedgerValue = LedgerValue {} fromLedgerValue : LedgerValue -> a -fromLedgerValue = error "foobar" +fromLedgerValue = error "foobar" -- gets replaced by the identity-function in script/Runner.scala -- | HIDE A version of 'createCmd' without constraints. -- @@ -758,10 +765,17 @@ submitTreeMulti actAs readAs cmds = locations = getCallStack callStack continue = identity + +-- | HIDE Identifier for a user in the user management service +data UserId = UserId + with + userName : Text + deriving (Eq, Ord, Show) + -- | HIDE User as used in the user management service data User = User with - id : Text + userId : UserId primaryParty : Optional Party deriving (Show, Eq, Ord) @@ -772,16 +786,62 @@ data UserRight | CanReadAs Party deriving (Show, Eq) +-- | HIDE May be thrown by validateUserId +exception InvalidUserId + with + m : Text + where + message m + +mkUserId : Text -> Script (Optional Text) -> Script UserId +mkUserId name validateScript = do + validateScript >>= \case + None -> pure (UserId name) + Some msg -> throw (InvalidUserId msg) + +-- | HIDE May be thrown by createUser +exception UserAlreadyExists + with + userId : UserId + where + message (userName userId) + +checkUserAlreadyExists : UserId -> Script (Optional ()) -> Script () +checkUserAlreadyExists userId script = do + script >>= \case + None -> throw (UserAlreadyExists userId) + Some x -> pure x + +-- | HIDE May be thrown by: {get,delete}User, {list,grant,revoke}UserRights +exception UserNotFound + with + userId : UserId + where + message (userName userId) + +checkUserNotFound : UserId -> Script (Optional a) -> Script a +checkUserNotFound userId script = do + script >>= \case + None -> throw (UserNotFound userId) + Some x -> pure x + +-- | HIDE Convert the text to a user id or fail if it does not conform to the format restriction. +validateUserId : HasCallStack => Text -> Script UserId +validateUserId name = mkUserId name $ lift $ Free $ ValidateUserId ValidateUserIdPayload with + continue = pure + locations = getCallStack callStack + name + -- | HIDE Create a user with the given rights -createUser : HasCallStack => User -> [UserRight] -> Script User +createUser : HasCallStack => User -> [UserRight] -> Script () createUser user rights = createUser' user rights None -- | HIDE Create a user with the given rights on the given participant. -createUserOn : HasCallStack => User -> [UserRight] -> ParticipantName -> Script User +createUserOn : HasCallStack => User -> [UserRight] -> ParticipantName -> Script () createUserOn user rights participant = createUser' user rights (Some participant) -createUser' : HasCallStack => User -> [UserRight] -> Optional ParticipantName -> Script User -createUser' user rights participant = lift $ Free $ CreateUser CreateUserPayload with +createUser' : HasCallStack => User -> [UserRight] -> Optional ParticipantName -> Script () +createUser' user rights participant = checkUserAlreadyExists user.userId $ lift $ Free $ CreateUser CreateUserPayload with participant = fmap participantName participant continue = pure locations = getCallStack callStack @@ -789,15 +849,15 @@ createUser' user rights participant = lift $ Free $ CreateUser CreateUserPayload rights -- | HIDE Fetch a user by user id -getUser : HasCallStack => Text -> Script (Optional User) -getUser userId = getUser' userId None +getUser : HasCallStack => UserId -> Script User +getUser userId = getUser' userId None -- | HIDE Fetch a user by user id from the given participant. -getUserOn : HasCallStack => Text -> ParticipantName -> Script (Optional User) -getUserOn userId participant = getUser' userId (Some participant) +getUserOn : HasCallStack => UserId -> ParticipantName -> Script User +getUserOn userId participant = getUser' userId (Some participant) -getUser' : HasCallStack => Text -> Optional ParticipantName -> Script (Optional User) -getUser' userId participant = lift $ Free $ GetUser GetUserPayload with +getUser' : HasCallStack => UserId -> Optional ParticipantName -> Script User +getUser' userId participant = checkUserNotFound userId $ lift $ Free $ GetUser GetUserPayload with participant = fmap participantName participant continue = pure locations = getCallStack callStack @@ -818,15 +878,15 @@ listUsers' participant = lift $ Free $ ListUsers ListUsersPayload with locations = getCallStack callStack -- | HIDE Grant the user the given rights. Returns the rights that have been newly granted. -grantUserRights : HasCallStack => Text -> [UserRight] -> Script [UserRight] +grantUserRights : HasCallStack => UserId -> [UserRight] -> Script [UserRight] grantUserRights userId rights = grantUserRights' userId rights None -- | HIDE Grant the user on the given participant the given rights. Returns the rights that have been newly granted. -grantUserRightsOn : HasCallStack => Text -> [UserRight] -> ParticipantName -> Script [UserRight] +grantUserRightsOn : HasCallStack => UserId -> [UserRight] -> ParticipantName -> Script [UserRight] grantUserRightsOn userId rights participant = grantUserRights' userId rights (Some participant) -grantUserRights' : HasCallStack => Text -> [UserRight] -> Optional ParticipantName -> Script [UserRight] -grantUserRights' userId rights participant = lift $ Free $ GrantUserRights GrantUserRightsPayload with +grantUserRights' : HasCallStack => UserId -> [UserRight] -> Optional ParticipantName -> Script [UserRight] +grantUserRights' userId rights participant = checkUserNotFound userId $ lift $ Free $ GrantUserRights GrantUserRightsPayload with participant = fmap participantName participant continue = pure locations = getCallStack callStack @@ -834,14 +894,14 @@ grantUserRights' userId rights participant = lift $ Free $ GrantUserRights Grant rights -- | HIDE Revoke the rights of the given user. Returns the revoked rights. -revokeUserRights : HasCallStack => Text -> [UserRight] -> Script [UserRight] +revokeUserRights : HasCallStack => UserId -> [UserRight] -> Script [UserRight] revokeUserRights userId rights = revokeUserRights' userId rights None -revokeUserRightsOn : HasCallStack => Text -> [UserRight] -> ParticipantName -> Script [UserRight] +revokeUserRightsOn : HasCallStack => UserId -> [UserRight] -> ParticipantName -> Script [UserRight] revokeUserRightsOn userId rights participant = revokeUserRights' userId rights (Some participant) -revokeUserRights' : HasCallStack => Text -> [UserRight] -> Optional ParticipantName -> Script [UserRight] -revokeUserRights' userId rights participant = lift $ Free $ RevokeUserRights RevokeUserRightsPayload with +revokeUserRights' : HasCallStack => UserId -> [UserRight] -> Optional ParticipantName -> Script [UserRight] +revokeUserRights' userId rights participant = checkUserNotFound userId $ lift $ Free $ RevokeUserRights RevokeUserRightsPayload with participant = fmap participantName participant continue = pure locations = getCallStack callStack @@ -849,63 +909,70 @@ revokeUserRights' userId rights participant = lift $ Free $ RevokeUserRights Rev rights -- | HIDE Delete the given user. Fails if the user does not exist. -deleteUser : HasCallStack => Text -> Script () +deleteUser : HasCallStack => UserId -> Script () deleteUser userId = deleteUser' userId None -- | HIDE Delete the given user on the given participant. Fails if the user does not exist. -deleteUserOn : HasCallStack => Text -> ParticipantName -> Script () +deleteUserOn : HasCallStack => UserId -> ParticipantName -> Script () deleteUserOn userId participant = deleteUser' userId (Some participant) -deleteUser' : HasCallStack => Text -> Optional ParticipantName -> Script () -deleteUser' userId participant = lift $ Free $ DeleteUser DeleteUserPayload with +deleteUser' : HasCallStack => UserId -> Optional ParticipantName -> Script () +deleteUser' userId participant = checkUserNotFound userId $ lift $ Free $ DeleteUser DeleteUserPayload with participant = fmap participantName participant continue = pure locations = getCallStack callStack userId -- | HIDE List the rights of the given user . -listUserRights : HasCallStack => Text -> Script [UserRight] +listUserRights : HasCallStack => UserId -> Script [UserRight] listUserRights userId = listUserRights' userId None -- | HIDE List the rights of the user on the given participant. -listUserRightsOn : HasCallStack => Text -> ParticipantName -> Script [UserRight] +listUserRightsOn : HasCallStack => UserId -> ParticipantName -> Script [UserRight] listUserRightsOn userId participant = listUserRights' userId (Some participant) -listUserRights' : HasCallStack => Text -> Optional ParticipantName -> Script [UserRight] -listUserRights' userId participant = lift $ Free $ ListUserRights ListUserRightsPayload with +listUserRights' : HasCallStack => UserId -> Optional ParticipantName -> Script [UserRight] +listUserRights' userId participant = checkUserNotFound userId $ lift $ Free $ ListUserRights ListUserRightsPayload with participant = fmap participantName participant continue = pure locations = getCallStack callStack userId -- | HIDE Submit the commands with the actAs and readAs claims granted to the user. -submitUser : HasCallStack => Text -> Commands a -> Script a +submitUser : HasCallStack => UserId -> Commands a -> Script a submitUser userId cmds = submitUser' userId None cmds -- | HIDE Submit the commands with the actAs and readAs claims granted -- to the user on the given participant -submitUserOn : HasCallStack => Text -> ParticipantName -> Commands a -> Script a +submitUserOn : HasCallStack => UserId -> ParticipantName -> Commands a -> Script a submitUserOn userId participant cmds = submitUser' userId (Some participant) cmds -submitUser' : HasCallStack => Text -> Optional ParticipantName -> Commands a -> Script a +submitUser' : HasCallStack => UserId -> Optional ParticipantName -> Commands a -> Script a submitUser' userId participant cmds = do rights <- listUserRights' userId participant let actAs = [ p | CanActAs p <- rights ] let readAs = [ p | CanReadAs p <- rights ] submitMulti actAs readAs cmds +data ValidateUserIdPayload a = ValidateUserIdPayload + with + name : Text + continue : Optional Text -> a -- text indicates reason for invalid name + locations : [(Text, SrcLoc)] + deriving Functor + data CreateUserPayload a = CreateUserPayload with user: User rights: [UserRight] participant : Optional Text - continue : User -> a + continue : Optional () -> a locations : [(Text, SrcLoc)] deriving Functor data GetUserPayload a = GetUserPayload with - userId : Text + userId : UserId participant : Optional Text continue : Optional User -> a locations : [(Text, SrcLoc)] @@ -913,9 +980,9 @@ data GetUserPayload a = GetUserPayload data DeleteUserPayload a = DeleteUserPayload with - userId : Text + userId : UserId participant : Optional Text - continue : () -> a + continue : Optional () -> a locations : [(Text, SrcLoc)] deriving Functor @@ -928,26 +995,26 @@ data ListUsersPayload a = ListUsersPayload data GrantUserRightsPayload a = GrantUserRightsPayload with - userId : Text + userId : UserId rights : [UserRight] participant : Optional Text - continue : [UserRight] -> a + continue : Optional [UserRight] -> a locations : [(Text, SrcLoc)] deriving Functor data RevokeUserRightsPayload a = RevokeUserRightsPayload with - userId : Text + userId : UserId rights : [UserRight] participant : Optional Text - continue : [UserRight] -> a + continue : Optional [UserRight] -> a locations : [(Text, SrcLoc)] deriving Functor data ListUserRightsPayload a = ListUserRightsPayload with - userId : Text + userId : UserId participant : Optional Text - continue : [UserRight] -> a + continue : Optional [UserRight] -> a locations : [(Text, SrcLoc)] deriving Functor diff --git a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/Converter.scala b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/Converter.scala index 955572e6e029..0bac5f9ed052 100644 --- a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/Converter.scala +++ b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/Converter.scala @@ -667,11 +667,17 @@ object Converter { Right( record( scriptIds.damlScript("User"), - ("id", SText(user.id)), + ("id", fromUserId(scriptIds, user.id)), ("primaryParty", SOptional(user.primaryParty.map(SParty(_)))), ) ) + def fromUserId(scriptIds: ScriptIds, userId: UserId): SValue = + record( + scriptIds.damlScript("UserId"), + ("userName", SText(userId)), + ) + def toUser(v: SValue): Either[String, User] = v match { case SRecord(_, _, vals) if vals.size == 2 => @@ -683,9 +689,14 @@ object Converter { } def toUserId(v: SValue): Either[String, UserId] = - // TODO https://github.com/digital-asset/daml/issues/11997 - // Produce a sensible error for invalid user ids. - toText(v).flatMap(UserId.fromString(_)) + v match { + case SRecord(_, _, vals) if vals.size == 1 => + for { + userName <- toText(vals.get(0)) + userId <- UserId.fromString(userName) + } yield userId + case _ => Left(s"Expected UserId but got $v") + } def fromUserRight( scriptIds: ScriptIds, diff --git a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ScriptF.scala b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ScriptF.scala index 8bb8d9867abf..9e62e5b8b15f 100644 --- a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ScriptF.scala +++ b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ScriptF.scala @@ -29,7 +29,7 @@ import scalaz.syntax.traverse._ import scalaz.std.either._ import scalaz.std.list._ import scalaz.std.option._ -import com.daml.script.converter.Converter.toContractId +import com.daml.script.converter.Converter.{toContractId, toText} import scala.concurrent.{ExecutionContext, Future} @@ -412,6 +412,26 @@ object ScriptF { } } + final case class ValidateUserId( + userName: String, + stackTrace: StackTrace, + continue: SValue, + ) extends Cmd { + override def description = "ValidateUserId" + override def execute(env: Env)(implicit + ec: ExecutionContext, + mat: Materializer, + esf: ExecutionSequencerFactory, + ): Future[SExpr] = { + val errorOption = + UserId.fromString(userName) match { + case Right(_) => None // valid + case Left(message) => Some(SText(message)) // invalid; with error message + } + Future.successful(SEApp(SEValue(continue), Array(SEValue(SOptional(errorOption))))) + } + } + final case class CreateUser( user: User, rights: List[UserRight], @@ -427,9 +447,11 @@ object ScriptF { ): Future[SExpr] = for { client <- Converter.toFuture(env.clients.getParticipant(participant)) - user <- client.createUser(user, rights) - user <- Converter.toFuture(Converter.fromUser(env.scriptIds, user)) - } yield SEApp(SEValue(continue), Array(SEValue(user))) + res <- client.createUser(user, rights) + res <- Converter.toFuture( + Converter.fromOptional[Unit](res, _ => Right(SUnit)) + ) + } yield SEApp(SEValue(continue), Array(SEValue(res))) } final case class GetUser( @@ -467,8 +489,11 @@ object ScriptF { ): Future[SExpr] = for { client <- Converter.toFuture(env.clients.getParticipant(participant)) - _ <- client.deleteUser(userId) - } yield SEApp(SEValue(continue), Array(SEValue(SUnit))) + res <- client.deleteUser(userId) + res <- Converter.toFuture( + Converter.fromOptional[Unit](res, _ => Right(SUnit)) + ) + } yield SEApp(SEValue(continue), Array(SEValue(res))) } final case class ListUsers( @@ -508,9 +533,14 @@ object ScriptF { client <- Converter.toFuture(env.clients.getParticipant(participant)) rights <- client.grantUserRights(userId, rights) rights <- Converter.toFuture( - rights.to(FrontStack).traverse(Converter.fromUserRight(env.scriptIds, _)) + Converter.fromOptional[List[UserRight]]( + rights, + _.to(FrontStack) + .traverse(Converter.fromUserRight(env.scriptIds, _)) + .map(SList(_)), + ) ) - } yield SEApp(SEValue(continue), Array(SEValue(SList(rights)))) + } yield SEApp(SEValue(continue), Array(SEValue(rights))) } final case class RevokeUserRights( @@ -530,9 +560,14 @@ object ScriptF { client <- Converter.toFuture(env.clients.getParticipant(participant)) rights <- client.revokeUserRights(userId, rights) rights <- Converter.toFuture( - rights.to(FrontStack).traverse(Converter.fromUserRight(env.scriptIds, _)) + Converter.fromOptional[List[UserRight]]( + rights, + _.to(FrontStack) + .traverse(Converter.fromUserRight(env.scriptIds, _)) + .map(SList(_)), + ) ) - } yield SEApp(SEValue(continue), Array(SEValue(SList(rights)))) + } yield SEApp(SEValue(continue), Array(SEValue(rights))) } final case class ListUserRights( @@ -551,9 +586,14 @@ object ScriptF { client <- Converter.toFuture(env.clients.getParticipant(participant)) rights <- client.listUserRights(userId) rights <- Converter.toFuture( - rights.to(FrontStack).traverse(Converter.fromUserRight(env.scriptIds, _)) + Converter.fromOptional[List[UserRight]]( + rights, + _.to(FrontStack) + .traverse(Converter.fromUserRight(env.scriptIds, _)) + .map(SList(_)), + ) ) - } yield SEApp(SEValue(continue), Array(SEValue(SList(rights)))) + } yield SEApp(SEValue(continue), Array(SEValue(rights))) } // Shared between Submit, SubmitMustFail and SubmitTree @@ -790,6 +830,16 @@ object ScriptF { } + private def parseValidateUserId(ctx: Ctx, v: SValue): Either[String, ValidateUserId] = + v match { + case SRecord(_, _, JavaList(userName, continue, stackTrace)) => + for { + userName <- toText(userName) + stackTrace <- toStackTrace(ctx, Some(stackTrace)) + } yield ValidateUserId(userName, stackTrace, continue) + case _ => Left(s"Expected ValidateUserId payload but got $v") + } + private def parseCreateUser(ctx: Ctx, v: SValue): Either[String, CreateUser] = v match { case SRecord(_, _, JavaList(user, rights, participant, continue, stackTrace)) => @@ -884,6 +934,7 @@ object ScriptF { case "Sleep" => parseSleep(ctx, v) case "Catch" => parseCatch(v) case "Throw" => parseThrow(v) + case "ValidateUserId" => parseValidateUserId(ctx, v) case "CreateUser" => parseCreateUser(ctx, v) case "GetUser" => parseGetUser(ctx, v) case "DeleteUser" => parseDeleteUser(ctx, v) diff --git a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/GrpcLedgerClient.scala b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/GrpcLedgerClient.scala index 3872b7c284e1..8802e7241de3 100644 --- a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/GrpcLedgerClient.scala +++ b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/GrpcLedgerClient.scala @@ -349,8 +349,10 @@ class GrpcLedgerClient(val grpcClient: LedgerClient, val applicationId: Applicat ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[User] = - grpcClient.userManagementClient.createUser(user, rights) + ): Future[Option[Unit]] = + grpcClient.userManagementClient.createUser(user, rights).map(_ => Some(())).recover { + case e: StatusRuntimeException if e.getStatus.getCode == Status.Code.ALREADY_EXISTS => None + } override def getUser(id: UserId)(implicit ec: ExecutionContext, @@ -365,8 +367,10 @@ class GrpcLedgerClient(val grpcClient: LedgerClient, val applicationId: Applicat ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[Unit] = - grpcClient.userManagementClient.deleteUser(id) + ): Future[Option[Unit]] = + grpcClient.userManagementClient.deleteUser(id).map(Some(_)).recover { + case e: StatusRuntimeException if e.getStatus.getCode == Status.Code.NOT_FOUND => None + } override def listUsers()(implicit ec: ExecutionContext, @@ -382,8 +386,10 @@ class GrpcLedgerClient(val grpcClient: LedgerClient, val applicationId: Applicat ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] = - grpcClient.userManagementClient.grantUserRights(id, rights).map(_.toList) + ): Future[Option[List[UserRight]]] = + grpcClient.userManagementClient.grantUserRights(id, rights).map(_.toList).map(Some(_)).recover { + case e: StatusRuntimeException if e.getStatus.getCode == Status.Code.NOT_FOUND => None + } override def revokeUserRights( id: UserId, @@ -392,13 +398,21 @@ class GrpcLedgerClient(val grpcClient: LedgerClient, val applicationId: Applicat ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] = - grpcClient.userManagementClient.revokeUserRights(id, rights).map(_.toList) + ): Future[Option[List[UserRight]]] = + grpcClient.userManagementClient + .revokeUserRights(id, rights) + .map(_.toList) + .map(Some(_)) + .recover { + case e: StatusRuntimeException if e.getStatus.getCode == Status.Code.NOT_FOUND => None + } override def listUserRights(id: UserId)(implicit ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] = - grpcClient.userManagementClient.listUserRights(id).map(_.toList) + ): Future[Option[List[UserRight]]] = + grpcClient.userManagementClient.listUserRights(id).map(_.toList).map(Some(_)).recover { + case e: StatusRuntimeException if e.getStatus.getCode == Status.Code.NOT_FOUND => None + } } diff --git a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/IdeLedgerClient.scala b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/IdeLedgerClient.scala index 8c369252f08f..7067f7e71fd3 100644 --- a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/IdeLedgerClient.scala +++ b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/IdeLedgerClient.scala @@ -322,8 +322,11 @@ class IdeLedgerClient( ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[User] = - handleUserManagement(userManagementStore.createUser(user, rights.toSet)).map(_ => user) + ): Future[Option[Unit]] = + userManagementStore.createUser(user, rights.toSet) match { + case Left(scenario.Error.UserManagementError.UserExists(_)) => Future.successful(None) + case a => handleUserManagement(a).map(Some(_)) + } override def getUser(id: UserId)(implicit ec: ExecutionContext, @@ -339,8 +342,11 @@ class IdeLedgerClient( ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[Unit] = - handleUserManagement(userManagementStore.deleteUser(id)) + ): Future[Option[Unit]] = + userManagementStore.deleteUser(id) match { + case Left(scenario.Error.UserManagementError.UserNotFound(_)) => Future.successful(None) + case a => handleUserManagement(a).map(Some(_)) + } override def listUsers()(implicit ec: ExecutionContext, @@ -356,8 +362,11 @@ class IdeLedgerClient( ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] = - handleUserManagement(userManagementStore.grantRights(id, rights.toSet)).map(_.toList) + ): Future[Option[List[UserRight]]] = + userManagementStore.grantRights(id, rights.toSet) match { + case Left(scenario.Error.UserManagementError.UserNotFound(_)) => Future.successful(None) + case a => handleUserManagement(a).map(_.toList).map(Some(_)) + } override def revokeUserRights( id: UserId, @@ -366,13 +375,19 @@ class IdeLedgerClient( ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] = - handleUserManagement(userManagementStore.revokeRights(id, rights.toSet)).map(_.toList) + ): Future[Option[List[UserRight]]] = + userManagementStore.revokeRights(id, rights.toSet) match { + case Left(scenario.Error.UserManagementError.UserNotFound(_)) => Future.successful(None) + case a => handleUserManagement(a).map(_.toList).map(Some(_)) + } override def listUserRights(id: UserId)(implicit ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] = - handleUserManagement(userManagementStore.listUserRights(id)).map(_.toList) + ): Future[Option[List[UserRight]]] = + userManagementStore.listUserRights(id) match { + case Left(scenario.Error.UserManagementError.UserNotFound(_)) => Future.successful(None) + case a => handleUserManagement(a).map(_.toList).map(Some(_)) + } } diff --git a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/JsonLedgerClient.scala b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/JsonLedgerClient.scala index 9c042d989fd9..ec4bc2f6b683 100644 --- a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/JsonLedgerClient.scala +++ b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/JsonLedgerClient.scala @@ -509,7 +509,7 @@ class JsonLedgerClient( ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[User] = + ): Future[Option[Unit]] = unsupportedOn("createUser") override def getUser(id: UserId)(implicit @@ -523,7 +523,7 @@ class JsonLedgerClient( ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[Unit] = + ): Future[Option[Unit]] = unsupportedOn("deleteUser") override def listUsers()(implicit @@ -540,7 +540,7 @@ class JsonLedgerClient( ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] = + ): Future[Option[List[UserRight]]] = unsupportedOn("grantUserRights") override def revokeUserRights( @@ -550,14 +550,14 @@ class JsonLedgerClient( ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] = + ): Future[Option[List[UserRight]]] = unsupportedOn("revokeUserRights") override def listUserRights(id: UserId)(implicit ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] = + ): Future[Option[List[UserRight]]] = unsupportedOn("listUserRights") } diff --git a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/ScriptLedgerClient.scala b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/ScriptLedgerClient.scala index 321c117cf0b0..c98d32b40164 100644 --- a/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/ScriptLedgerClient.scala +++ b/daml-script/runner/src/main/scala/com/digitalasset/daml/lf/engine/script/ledgerinteraction/ScriptLedgerClient.scala @@ -140,7 +140,7 @@ trait ScriptLedgerClient { ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[User] + ): Future[Option[Unit]] def getUser(id: UserId)(implicit ec: ExecutionContext, @@ -152,7 +152,7 @@ trait ScriptLedgerClient { ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[Unit] + ): Future[Option[Unit]] def listUsers()(implicit ec: ExecutionContext, @@ -164,17 +164,17 @@ trait ScriptLedgerClient { ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] + ): Future[Option[List[UserRight]]] def revokeUserRights(id: UserId, rights: List[UserRight])(implicit ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] + ): Future[Option[List[UserRight]]] def listUserRights(id: UserId)(implicit ec: ExecutionContext, esf: ExecutionSequencerFactory, mat: Materializer, - ): Future[List[UserRight]] + ): Future[Option[List[UserRight]]] } diff --git a/daml-script/test/daml/ScriptTest.daml b/daml-script/test/daml/ScriptTest.daml index 588ac6429859..5cd7b73ca856 100644 --- a/daml-script/test/daml/ScriptTest.daml +++ b/daml-script/test/daml/ScriptTest.daml @@ -441,28 +441,33 @@ jsonMultiPartyPartySets (p1, p2) = do pure () -- User created by Sandbox by default. -participantAdmin : User -participantAdmin = User "participant_admin" None +getParticipantAdmin : () -> Script User +getParticipantAdmin () = do + participant_admin <- validateUserId "participant_admin" + pure $ User participant_admin None testUserManagement = do + participantAdmin <- getParticipantAdmin () users <- listUsers users === [participantAdmin] - u1 <- createUser (User "u1" None) [] - u1 === User "u1" None - u2 <- createUser (User "u2" None) [] - u2 === User "u2" None - u <- getUser "u1" - u === Some u1 - u <- getUser "u2" - u === Some u2 - u <- getUser "nonexistent" - u === None + u1 <- validateUserId "u1" + u2 <- validateUserId "u2" + let user1 = User u1 None + let user2 = User u2 None + createUser user1 [] + createUser user2 [] + u <- getUser u1 + u === user1 + u <- getUser u2 + u === user2 + nonexistent <- validateUserId "nonexistent" + try do _ <- getUser nonexistent; undefined catch UserNotFound _ -> pure () users <- listUsers - sort users === [participantAdmin, User "u1" None, User "u2" None] - deleteUser "u1" + sort users === [participantAdmin, user1, user2] + deleteUser u1 users <- listUsers - sort users === [participantAdmin, User "u2" None] - deleteUser "u2" + sort users === [participantAdmin, user2] + deleteUser u2 users <- listUsers users === [participantAdmin] pure () @@ -470,26 +475,28 @@ testUserManagement = do testUserRightManagement = do p1 <- allocateParty "p1" p2 <- allocateParty "p2" - u1 <- createUser (User "u1" None) [] - rights <- listUserRights "u1" + u1 <- validateUserId "u1" + let user1 = User u1 None + createUser user1 [] + rights <- listUserRights u1 rights === [] - newRights <- grantUserRights "u1" [ParticipantAdmin] + newRights <- grantUserRights u1 [ParticipantAdmin] newRights === [ParticipantAdmin] - newRights <- grantUserRights "u1" [ParticipantAdmin] + newRights <- grantUserRights u1 [ParticipantAdmin] newRights === [] - rights <- listUserRights "u1" + rights <- listUserRights u1 rights === [ParticipantAdmin] - newRights <- grantUserRights "u1" [CanActAs p1, CanReadAs p2] + newRights <- grantUserRights u1 [CanActAs p1, CanReadAs p2] newRights === [CanActAs p1, CanReadAs p2] - rights <- listUserRights "u1" + rights <- listUserRights u1 rights === [ParticipantAdmin, CanActAs p1, CanReadAs p2] - revoked <- revokeUserRights "u1" [ParticipantAdmin] + revoked <- revokeUserRights u1 [ParticipantAdmin] revoked === [ParticipantAdmin] - revoked <- revokeUserRights "u1" [ParticipantAdmin] + revoked <- revokeUserRights u1 [ParticipantAdmin] revoked === [] - rights <- listUserRights "u1" + rights <- listUserRights u1 rights === [CanActAs p1, CanReadAs p2] - revoked <- revokeUserRights "u1" [CanActAs p1, CanReadAs p2] + revoked <- revokeUserRights u1 [CanActAs p1, CanReadAs p2] revoked === [CanActAs p1, CanReadAs p2] - rights <- listUserRights "u1" + rights <- listUserRights u1 rights === [] diff --git a/templates/create-daml-app/daml/Setup.daml b/templates/create-daml-app/daml/Setup.daml index 663afc31e15b..36c741d195ee 100644 --- a/templates/create-daml-app/daml/Setup.daml +++ b/templates/create-daml-app/daml/Setup.daml @@ -7,20 +7,25 @@ import Daml.Script setup : Script () setup = do + let displayNames = ["Alice", "Bob", "Charlie"] forA_ displayNames $ \displayName -> do - let userId = toUserId displayName - u <- getUser userId - case u of - Some _ -> + userId <- validateUserId $ toUserId displayName + userExists userId >>= \case + True -> -- user already exists do nothing pure () - None -> do + False -> do let partyIdHint = toPartyIdHint displayName p <- allocatePartyWithHint displayName (PartyIdHint partyIdHint) createUser (User userId (Some p)) [CanActAs p] pure () +userExists : UserId -> Script Bool +userExists u = do + try do _ <- getUser u; pure True + catch UserNotFound _ -> pure False + toUserId : Text -> Text toUserId = T.asciiToLower