Skip to content

Commit 99c5cef

Browse files
Better support for custom party hints for non-operator users (#3367)
Signed-off-by: Pasindu Tennage <pasindu.tennage@digitalasset.com> [ci]
1 parent d6f8e91 commit 99c5cef

File tree

10 files changed

+205
-22
lines changed

10 files changed

+205
-22
lines changed

apps/app/src/main/scala/org/lfdecentralizedtrust/splice/console/ValidatorAppReference.scala

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,14 @@ abstract class ValidatorAppReference(
105105
@Help.Description(
106106
"""Onboard individual canton-amulet user with a fresh or existing party-id. Return the user's partyId."""
107107
)
108-
def onboardUser(user: String, existingPartyId: Option[PartyId] = None): PartyId = {
108+
def onboardUser(
109+
user: String,
110+
existingPartyId: Option[PartyId] = None,
111+
createIfMissing: Option[Boolean] = None,
112+
): PartyId = {
109113
consoleEnvironment.run {
110114
httpCommand(
111-
HttpValidatorAdminAppClient.OnboardUser(user, existingPartyId)
115+
HttpValidatorAdminAppClient.OnboardUser(user, existingPartyId, createIfMissing)
112116
)
113117
}
114118
}

apps/app/src/test/scala/org/lfdecentralizedtrust/splice/integration/tests/ValidatorIntegrationTest.scala

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,4 +578,82 @@ class ValidatorIntegrationTest extends IntegrationTest with WalletTestUtil {
578578
getUser.primaryParty.value.uid.identifier shouldBe validatorPartyHint
579579
}
580580

581+
"onboard user with custom party hint and check assignment/creation modes" in { implicit env =>
582+
initDso()
583+
aliceValidatorBackend.startSync()
584+
585+
val aliceValidatorParty = aliceValidatorBackend.getValidatorPartyId()
586+
val testUser1 = s"test-1-${Random.nextInt(10000)}"
587+
val testUser2 = s"test-2-${Random.nextInt(10000)}"
588+
val testUser3 = s"test-3-${Random.nextInt(10000)}"
589+
val testUser4 = s"test-4-${Random.nextInt(10000)}"
590+
val customPartyHint = s"CustomHint${Random.nextInt(10000)}"
591+
592+
def onboard(
593+
name: String,
594+
partyId: Option[PartyId] = None,
595+
createIfMissing: Option[Boolean] = None,
596+
): PartyId = {
597+
aliceValidatorBackend.onboardUser(name, partyId, createIfMissing)
598+
}
599+
600+
clue(
601+
"Assign new user to existing Validator Party"
602+
) {
603+
aliceValidatorBackend.listUsers() should not contain testUser1
604+
val assignedPartyId = onboard(
605+
name = testUser1,
606+
partyId = Some(aliceValidatorParty),
607+
)
608+
assignedPartyId shouldBe aliceValidatorParty
609+
aliceValidatorBackend.listUsers() should contain(testUser1)
610+
}
611+
612+
clue("Use 'name' as hint") {
613+
aliceValidatorBackend.listUsers() should not contain testUser2
614+
615+
val defaultPartyId = onboard(
616+
name = testUser2,
617+
createIfMissing = Some(true),
618+
)
619+
620+
val expectedHint = BaseLedgerConnection.sanitizeUserIdToPartyString(testUser2)
621+
defaultPartyId.toString.split("::").head shouldBe expectedHint
622+
aliceValidatorBackend.listUsers() should contain(testUser2)
623+
}
624+
625+
clue("Use partyId as hint") {
626+
val desiredPartyId = PartyId.tryCreate(customPartyHint, aliceValidatorParty.uid.namespace)
627+
aliceValidatorBackend.listUsers() should not contain testUser3
628+
val customPartyId = onboard(
629+
name = testUser3,
630+
partyId = Some(desiredPartyId),
631+
createIfMissing = Some(true),
632+
)
633+
634+
customPartyId.toString.split("::").head shouldBe customPartyHint
635+
customPartyId shouldBe desiredPartyId
636+
aliceValidatorBackend.listUsers() should contain(testUser3)
637+
}
638+
639+
clue("Fail when creation is disallowed but no party is provided to assign to") {
640+
loggerFactory.assertEventuallyLogsSeq(SuppressionRule.LevelAndAbove(Level.DEBUG))(
641+
intercept[com.digitalasset.canton.console.CommandFailure] {
642+
onboard(
643+
name = testUser4,
644+
createIfMissing = Some(false),
645+
)
646+
},
647+
entries => {
648+
forAtLeast(1, entries)(
649+
_.message should include(
650+
s"party_id must be provided when createPartyIfMissing is false and no existing"
651+
)
652+
)
653+
},
654+
)
655+
aliceValidatorBackend.listUsers() should not contain testUser4
656+
}
657+
}
658+
581659
}

apps/validator/src/main/openapi/validator-internal.yaml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -551,11 +551,19 @@ components:
551551
properties:
552552
name:
553553
type: string
554+
description: The name of the user to onboard.
554555
party_id:
555556
type: string
556557
description: |
557-
The party id of the user to onboard. This is optional and if not provided
558-
a fresh party id will be generated by the backend.
558+
The party id of the user to onboard.
559+
If no party_id is provided then a fresh party will be generated, using the 'name' as the Party Hint.
560+
If party_id is provided and createPartyIfMissing is false, then the party must already exist on the ledger. The existing party will be assigned to the user.
561+
If party_id is provided and createPartyIfMissing is true, then: if a party with the provided party_id exists, the user will be associated with it. Otherwise, a new party will be created using the provided party_id, and the user will be associated with that new party.
562+
createPartyIfMissing:
563+
type: boolean
564+
description: |
565+
If true, create the party if it does not already exist on the ledger.
566+
Default is 'false'.
559567
560568
OnboardUserResponse:
561569
type: object

apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/ValidatorApp.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ class ValidatorApp(
525525
.onboard(
526526
instance.walletUser.getOrElse(instance.serviceUser),
527527
Some(party),
528+
Some(false),
528529
storeWithIngestion,
529530
validatorUserName = config.ledgerApiUser,
530531
// we're initializing so AmuletRules is guaranteed to be on synchronizerId
@@ -973,6 +974,7 @@ class ValidatorApp(
973974
ValidatorUtil.onboard(
974975
endUserName = user,
975976
knownParty = Some(validatorParty),
977+
Some(false),
976978
automation,
977979
validatorUserName = config.ledgerApiUser,
978980
// we're initializing so AmuletRules is guaranteed to be on synchronizerId

apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/api/client/commands/HttpValidatorAdminAppClient.scala

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,19 @@ object HttpValidatorAdminAppClient {
9898
}
9999
}
100100

101-
case class OnboardUser(name: String, existingPartyId: Option[PartyId])
102-
extends BaseCommand[http.OnboardUserResponse, PartyId] {
101+
case class OnboardUser(
102+
name: String,
103+
existingPartyId: Option[PartyId],
104+
createIfMissing: Option[Boolean] = None,
105+
) extends BaseCommand[http.OnboardUserResponse, PartyId] {
103106

104107
def submitRequest(
105108
client: Client,
106109
headers: List[HttpHeader],
107110
): EitherT[Future, Either[Throwable, HttpResponse], http.OnboardUserResponse] =
108111
client.onboardUser(
109-
definitions.OnboardUserRequest(name, existingPartyId.map(_.toProtoPrimitive)),
112+
definitions
113+
.OnboardUserRequest(name, existingPartyId.map(_.toProtoPrimitive), createIfMissing),
110114
headers,
111115
)
112116

apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorAdminHandler.scala

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ class HttpValidatorAdminHandler(
114114
withSpan(s"$workflowId.onboardUser") { _ => span =>
115115
val name = body.name
116116
span.setAttribute("name", name)
117-
onboard(name, body.partyId.map(PartyId.tryFromProtoPrimitive)).map(p =>
118-
definitions.OnboardUserResponse(p)
117+
onboard(name, body.partyId.map(PartyId.tryFromProtoPrimitive), body.createPartyIfMissing).map(
118+
p => definitions.OnboardUserResponse(p)
119119
)
120120
}
121121
}
@@ -235,13 +235,18 @@ class HttpValidatorAdminHandler(
235235
}
236236
}
237237

238-
private def onboard(name: String, partyId: Option[PartyId])(implicit
238+
private def onboard(
239+
name: String,
240+
partyId: Option[PartyId],
241+
createPartyIfMissing: Option[Boolean],
242+
)(implicit
239243
traceContext: TraceContext
240244
): Future[String] = {
241245
ValidatorUtil
242246
.onboard(
243247
name,
244248
partyId,
249+
createPartyIfMissing,
245250
storeWithIngestion,
246251
validatorUserName,
247252
getAmuletRulesDomain,

apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/admin/http/HttpValidatorHandler.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class HttpValidatorHandler(
5050
.onboard(
5151
name,
5252
None,
53+
None,
5354
storeWithIngestion,
5455
validatorUserName,
5556
getAmuletRulesDomain,

apps/validator/src/main/scala/org/lfdecentralizedtrust/splice/validator/util/ValidatorUtil.scala

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import com.digitalasset.canton.topology.transaction.*
2525
import com.digitalasset.canton.topology.{PartyId, SynchronizerId}
2626
import com.digitalasset.canton.tracing.TraceContext
2727
import com.digitalasset.canton.util.HexString
28-
import io.grpc.Status
28+
import io.grpc.{Status, StatusRuntimeException}
2929
import org.lfdecentralizedtrust.splice.store.AppStoreWithIngestion.SpliceLedgerConnectionPriority
3030

3131
import java.util.Base64
@@ -96,6 +96,7 @@ private[validator] object ValidatorUtil {
9696
def onboard(
9797
endUserName: String,
9898
knownParty: Option[PartyId],
99+
createPartyIfMissing: Option[Boolean],
99100
storeWithIngestion: AppStoreWithIngestion[ValidatorStore],
100101
validatorUserName: String,
101102
getAmuletRulesDomain: ScanConnection.GetAmuletRulesDomain,
@@ -110,17 +111,77 @@ private[validator] object ValidatorUtil {
110111
for {
111112
userPartyId <- knownParty match {
112113
case Some(party) =>
113-
connection.createUserWithPrimaryParty(
114-
endUserName,
115-
party,
116-
Seq(),
117-
)
114+
if (createPartyIfMissing.getOrElse(false)) {
115+
for {
116+
newlyAllocatedPartyId <- connection.getOrAllocateParty(
117+
party.uid.identifier.unwrap,
118+
Seq(),
119+
participantAdminConnection,
120+
)
121+
allocatedPartyId <- connection.createUserWithPrimaryParty(
122+
endUserName,
123+
newlyAllocatedPartyId,
124+
Seq(),
125+
)
126+
} yield {
127+
logger.debug(
128+
s"Creation allowed. Allocated new party ID $allocatedPartyId for user $endUserName"
129+
)
130+
allocatedPartyId
131+
}
132+
} else {
133+
connection
134+
.createUserWithPrimaryParty(
135+
endUserName,
136+
party,
137+
Seq(),
138+
)
139+
.map(newParty => {
140+
logger.debug(
141+
s"Creation disallowed. Associating user $endUserName with existing party $newParty"
142+
)
143+
newParty
144+
})
145+
}
118146
case None =>
119-
connection.getOrAllocateParty(
120-
endUserName,
121-
Seq(),
122-
participantAdminConnection,
123-
)
147+
if (createPartyIfMissing.getOrElse(true)) {
148+
connection
149+
.getOrAllocateParty(
150+
endUserName,
151+
Seq(),
152+
participantAdminConnection,
153+
)
154+
.map(allocatedParty => {
155+
logger.debug(
156+
s"No party ID provided and creation allowed. Allocated $allocatedParty for user $endUserName"
157+
)
158+
allocatedParty
159+
})
160+
} else {
161+
logger.debug(
162+
s"No party ID provided and creation disallowed. Checking for existing party."
163+
)
164+
connection
165+
.getOptionalPrimaryParty(endUserName)
166+
.recover {
167+
case e: StatusRuntimeException if e.getStatus.getCode == Status.Code.NOT_FOUND =>
168+
None
169+
}
170+
.map {
171+
case Some(existingParty) =>
172+
logger.debug(
173+
s"No party ID provided, creation disallowed, but user $endUserName has existing party $existingParty."
174+
)
175+
existingParty
176+
177+
case None =>
178+
throw Status.INVALID_ARGUMENT
179+
.withDescription(
180+
s"party_id must be provided when createPartyIfMissing is false and no existing party for user $endUserName is found."
181+
)
182+
.asRuntimeException()
183+
}
184+
}
124185
}
125186
_ <- retryProvider.ensureThatB(
126187
RetryFor.ClientCalls,

docs/src/release_notes.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
.. release-notes:: upcoming
99

10+
- Validator
11+
- Added support for picking a custom name for new parties created when onboarding users via the `/v0/admin/users` API. See :ref:`docs <validator-users>`.
12+
1013
- API security
1114

1215
- Tightened authorization checks for all non-public API endpoints.

docs/src/validator_operator/validator_users.rst

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ with the newly allocated party. As part of validator initialization, a party is
1919
Validator Operator. The user provided during installation as the `validatorWalletUser` will be
2020
associated with this party as its primary party.
2121

22-
More users can be configured such that their primary party is that of the validator operator.
22+
In addition to above approach, the validator API endpoint `/v0/admin/users` supports two additional modes for creating user and party associations:
23+
24+
1. Association with Existing Party: Linking a user to an existing party.
25+
2. Custom Party Hint: Allocating a new party using a human-readable party hint and associating the user to this newly created party.
26+
27+
Association with Existing Party: Users can be configured such that their primary party is that of the validator operator (or any other existing party).
2328
In effect, when such users login to the wallet UI, they will be accessing the wallet of the validator
2429
operator. Note that this will be the same wallet accessed by different users, with currently
2530
no support for finer grained permissions per user.
@@ -49,6 +54,18 @@ In order to associate a user with the party of the validator operator, the follo
4954
onboarded through the API call above. If you do see the button, it means that something has gone wrong
5055
in the process above (do not click the button!).
5156

57+
Custom Party Hint: This mode allows providing a human-readable Party hint when creating a new party for a user.
58+
This is used to create parties with descriptive hints (e.g., treasury_dept::...) instead of OAuth IDs.
59+
60+
1. Follow steps 1-3 from Mode ``Association with Existing Party`` to obtain the USER and TOKEN.
61+
2. Determine the desired, full PartyID ``(Hint::Namespace)``, e.g., ``alice::f3d917....``. Use the namespace of an existing party (like the validator operator's) for the namespace part. Save the full ID in a ``PARTY_ID`` environment variable: ``export PARTY_ID=alice::f3d917...``.
62+
3. Run the following command to create the new party and associate the user:
63+
64+
.. code-block:: bash
65+
66+
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
67+
--data-raw "{\"party_id\":\"$PARTY_ID\",\"name\":\"$USER\",\"createPartyIfMissing\":true}" \
68+
https://<URL of your wallet>/api/validator/v0/admin/users
5269
5370
Disable wallet and wallet automation
5471
-----------------------------------------------

0 commit comments

Comments
 (0)