diff --git a/ledger-service/http-json/src/it/scala/http/HttpServiceIntegrationTestUserManagement.scala b/ledger-service/http-json/src/it/scala/http/HttpServiceIntegrationTestUserManagement.scala index 4fa19330d464..d809d9bdae27 100644 --- a/ledger-service/http-json/src/it/scala/http/HttpServiceIntegrationTestUserManagement.scala +++ b/ledger-service/http-json/src/it/scala/http/HttpServiceIntegrationTestUserManagement.scala @@ -274,6 +274,155 @@ class HttpServiceIntegrationTestUserManagementNoAuth } yield users.map(_.userId) should contain allElementsOf usernames } + "getting information about a specific user should be possible via the user endpoint" in withHttpServiceAndClient( + participantAdminJwt + ) { (uri, _, _, _, _) => + import spray.json._ + import spray.json.DefaultJsonProtocol._ + val alice = getUniqueParty("Alice") + val createUserRequest = domain.CreateUserRequest( + getUniqueUserName("nice.user"), + Some(alice.unwrap), + List(alice), + List.empty, + isAdmin = true, + ) + for { + (status1, output1) <- postRequest( + uri.withPath(Uri.Path("/v1/user/create")), + createUserRequest.toJson, + headers = authorizationHeader(participantAdminJwt), + ) + _ <- { + status1 shouldBe StatusCodes.OK + getResult(output1).convertTo[Boolean] shouldBe true + } + (status2, output2) <- postRequest( + uri.withPath(Uri.Path(s"/v1/user")), + domain.GetUserRequest(createUserRequest.userId).toJson, + headers = authorizationHeader(participantAdminJwt), + ) + } yield { + status2 shouldBe StatusCodes.OK + getResult(output2).convertTo[UserDetails] shouldBe UserDetails( + createUserRequest.userId, + createUserRequest.primaryParty, + ) + } + } + + "getting information about the current user should be possible via the user endpoint" in withHttpServiceAndClient( + participantAdminJwt + ) { (uri, _, _, _, _) => + import spray.json._ + import spray.json.DefaultJsonProtocol._ + val alice = getUniqueParty("Alice") + val createUserRequest = domain.CreateUserRequest( + getUniqueUserName("nice.user"), + Some(alice.unwrap), + List(alice), + List.empty, + isAdmin = true, + ) + for { + (status1, output1) <- postRequest( + uri.withPath(Uri.Path("/v1/user/create")), + createUserRequest.toJson, + headers = authorizationHeader(participantAdminJwt), + ) + _ <- { + status1 shouldBe StatusCodes.OK + getResult(output1).convertTo[Boolean] shouldBe true + } + (status2, output2) <- getRequest( + uri.withPath(Uri.Path(s"/v1/user")), + headers = headersWithUserAuth(createUserRequest.userId, admin = true), + ) + } yield { + status2 shouldBe StatusCodes.OK + getResult(output2).convertTo[UserDetails] shouldBe UserDetails( + createUserRequest.userId, + createUserRequest.primaryParty, + ) + } + } + + "deleting a specific user should be possible via the user/delete endpoint" in withHttpServiceAndClient( + participantAdminJwt + ) { (uri, _, _, _, _) => + import spray.json._ + import spray.json.DefaultJsonProtocol._ + val alice = getUniqueParty("Alice") + val createUserRequest = domain.CreateUserRequest( + getUniqueUserName("nice.user"), + Some(alice.unwrap), + List(alice), + List.empty, + isAdmin = true, + ) + for { + (status1, output1) <- postRequest( + uri.withPath(Uri.Path("/v1/user/create")), + createUserRequest.toJson, + headers = authorizationHeader(participantAdminJwt), + ) + _ <- { + status1 shouldBe StatusCodes.OK + getResult(output1).convertTo[Boolean] shouldBe true + } + (status2, _) <- postRequest( + uri.withPath(Uri.Path(s"/v1/user/delete")), + domain.DeleteUserRequest(createUserRequest.userId).toJson, + headers = authorizationHeader(participantAdminJwt), + ) + _ = status2 shouldBe StatusCodes.OK + (status3, output3) <- getRequest( + uri.withPath(Uri.Path("/v1/users")), + headers = authorizationHeader(participantAdminJwt), + ) + } yield { + status3 shouldBe StatusCodes.OK + getResult(output3).convertTo[List[UserDetails]] should not contain createUserRequest.userId + } + } + + "deleting the current user should be possible via the user/delete endpoint" in withHttpServiceAndClient( + participantAdminJwt + ) { (uri, _, _, _, _) => + import spray.json._ + import spray.json.DefaultJsonProtocol._ + val alice = getUniqueParty("Alice") + val createUserRequest = domain.CreateUserRequest( + getUniqueUserName("nice.user"), + Some(alice.unwrap), + List(alice), + List.empty, + isAdmin = true, + ) + for { + (status1, output1) <- postRequest( + uri.withPath(Uri.Path("/v1/user/create")), + createUserRequest.toJson, + headers = authorizationHeader(participantAdminJwt), + ) + _ <- { + status1 shouldBe StatusCodes.OK + getResult(output1).convertTo[Boolean] shouldBe true + } + (status2, _) <- getRequest( + uri.withPath(Uri.Path(s"/v1/user/delete")), + headers = headersWithUserAuth(createUserRequest.userId), + ) + _ = status2 shouldBe StatusCodes.OK + (status3, output3) <- getRequest( + uri.withPath(Uri.Path("/v1/users")), + headers = authorizationHeader(participantAdminJwt), + ) + } yield { + status3 shouldBe StatusCodes.OK + getResult(output3).convertTo[List[UserDetails]] should not contain createUserRequest.userId + } + } } class HttpServiceIntegrationTestUserManagement diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala index c5f7b79ba8d4..9614d0c780e9 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/Endpoints.scala @@ -219,7 +219,9 @@ class Endpoints( ), path("query") & withTimer(queryMatchingTimer) apply toRoute(query(req)), path("fetch") & withFetchTimer apply toRoute(fetch(req)), - path("user" / "create") & withFetchTimer apply toRoute(allocateUser(req)), + path("user") apply toRoute(getUser(req)), + path("user" / "create") apply toRoute(allocateUser(req)), + path("user" / "delete") apply toRoute(deleteUser(req)), path("parties") & withFetchTimer apply toRoute(parties(req)), path("parties" / "allocate") & withTimer( allocatePartyTimer @@ -229,9 +231,12 @@ class Endpoints( get apply concat( path("query") & withTimer(queryAllTimer) apply toRoute(retrieveAll(req)), - path("user") & withFetchTimer apply toRoute(getUser(req)), - path("user" / "rights") & withFetchTimer apply toRoute(listUserRights(req)), - path("users") & withFetchTimer apply toRoute(listUsers(req)), + path("user") apply toRoute(getAuthenticatedUser(req)), + path("user" / "delete") apply toRoute(deleteAuthenticatedUser(req)), + path("user" / "rights") apply toRoute( + listAuthenticatedUserRights(req) + ), + path("users") apply toRoute(listUsers(req)), path("parties") & withTimer(getPartyTimer) apply toRoute(allParties(req)), path("packages") apply toRoute(listPackages(req)), @@ -475,6 +480,31 @@ class Endpoints( proxyWithoutCommand((jwt, _) => partiesService.allParties(jwt))(req) .flatMap(pd => either(pd map (domain.OkResponse(_)))) + def deleteUser(req: HttpRequest)(implicit + lc: LoggingContextOf[InstanceUUID with RequestID] + ): ET[domain.SyncResponse[Boolean]] = + proxyWithCommandET { (jwt, deleteUserRequest: domain.DeleteUserRequest) => + import scalaz.syntax.std.either._ + import com.daml.lf.data.Ref + for { + userId <- either( + Ref.UserId.fromString(deleteUserRequest.userId).disjunction.leftMap(InvalidUserInput) + ): ET[ + Ref.UserId + ] + _ <- EitherT.rightT(userManagementClient.deleteUser(userId, Some(jwt.value))) + } yield domain.OkResponse(true): domain.SyncResponse[Boolean] + }(req) + + def deleteAuthenticatedUser(req: HttpRequest)(implicit + lc: LoggingContextOf[InstanceUUID with RequestID] + ): ET[domain.SyncResponse[Boolean]] = + for { + jwt <- eitherT(input(req)).bimap(identity[Error], _._1) + userId <- decodeAndParseUserIdFromToken(jwt, decodeJwt).leftMap(identity[Error]) + _ <- EitherT.rightT(userManagementClient.deleteUser(userId, Some(jwt.value))) + } yield domain.OkResponse(true) + def listUsers(req: HttpRequest)(implicit lc: LoggingContextOf[InstanceUUID with RequestID] ): ET[domain.SyncResponse[List[domain.UserDetails]]] = @@ -485,7 +515,7 @@ class Endpoints( ) } yield domain.OkResponse(users.map(domain.UserDetails.fromUser).toList) - def listUserRights(req: HttpRequest)(implicit + def listAuthenticatedUserRights(req: HttpRequest)(implicit lc: LoggingContextOf[InstanceUUID with RequestID] ): ET[domain.SyncResponse[domain.UserRights]] = for { @@ -498,6 +528,24 @@ class Endpoints( def getUser(req: HttpRequest)(implicit lc: LoggingContextOf[InstanceUUID with RequestID] + ): ET[domain.SyncResponse[domain.UserDetails]] = + proxyWithCommandET { (jwt, getUserRequest: domain.GetUserRequest) => + import scalaz.syntax.std.either._ + import com.daml.lf.data.Ref + for { + userId <- either( + Ref.UserId.fromString(getUserRequest.userId).disjunction.leftMap(InvalidUserInput) + ): ET[ + Ref.UserId + ] + user <- EitherT.rightT(userManagementClient.getUser(userId, Some(jwt.value))) + } yield domain.OkResponse( + domain.UserDetails(user.id, user.primaryParty) + ): domain.SyncResponse[domain.UserDetails] + }(req) + + def getAuthenticatedUser(req: HttpRequest)(implicit + lc: LoggingContextOf[InstanceUUID with RequestID] ): ET[domain.SyncResponse[domain.UserDetails]] = for { jwt <- eitherT(input(req)).bimap(identity[Error], _._1) @@ -845,6 +893,12 @@ class Endpoints( a <- either(SprayJson.decode[A](reqBody).liftErr(InvalidUserInput)): ET[A] b <- eitherT(handleFutureEitherFailure(fn(jwt, a))): ET[R] } yield b + + private def proxyWithCommandET[A: JsonReader, R]( + fn: (Jwt, A) => ET[R] + )(req: HttpRequest)(implicit + lc: LoggingContextOf[InstanceUUID with RequestID] + ): ET[R] = proxyWithCommand((jwt, a: A) => fn(jwt, a).run)(req) } object Endpoints { diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/domain.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/domain.scala index 2822bc346ce4..520fbb37386b 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/domain.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/domain.scala @@ -169,6 +169,10 @@ object domain extends com.daml.fetchcontracts.domain.Aliases { isAdmin: Boolean, ) + final case class GetUserRequest(userId: String) + + final case class DeleteUserRequest(userId: String) + final case class AllocatePartyRequest(identifierHint: Option[Party], displayName: Option[String]) final case class CommandMeta( diff --git a/ledger-service/http-json/src/main/scala/com/digitalasset/http/json/JsonProtocol.scala b/ledger-service/http-json/src/main/scala/com/digitalasset/http/json/JsonProtocol.scala index 25eb11f21302..b9998c611b98 100644 --- a/ledger-service/http-json/src/main/scala/com/digitalasset/http/json/JsonProtocol.scala +++ b/ledger-service/http-json/src/main/scala/com/digitalasset/http/json/JsonProtocol.scala @@ -82,6 +82,12 @@ object JsonProtocol extends JsonProtocolLow { implicit val CreateUserRequest: JsonFormat[domain.CreateUserRequest] = jsonFormat5(domain.CreateUserRequest) + implicit val GetUserRequest: JsonFormat[domain.GetUserRequest] = + jsonFormat1(domain.GetUserRequest) + + implicit val DeleteUserRequest: JsonFormat[domain.DeleteUserRequest] = + jsonFormat1(domain.DeleteUserRequest) + implicit val AllocatePartyRequest: JsonFormat[domain.AllocatePartyRequest] = jsonFormat2(domain.AllocatePartyRequest)