Skip to content

Commit 621f8f1

Browse files
authored
Merge pull request #343 from digital-asset/python-daml-2-auth-compat
python: Miscellaneous fixes to improve compatibility with Daml 2 ledgers running behind auth.
2 parents 63362f4 + 5acd44e commit 621f8f1

File tree

9 files changed

+97
-16
lines changed

9 files changed

+97
-16
lines changed

.circleci/install-daml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ function install {
88
curl -sSL https://get.daml.com/ | sh
99
link
1010
fi
11-
daml install 2.0.0
11+
daml install 2.2.0
1212
daml install 1.18.1
1313
}
1414

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
7.7.5
1+
7.7.6

_build/daml-connect/daml-connect.conf

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
[tool.poetry]
55
name = "dazl"
6-
version = "7.7.5"
6+
version = "7.7.6"
77
description = "high-level Ledger API client for Daml ledgers"
88
license = "Apache-2.0"
99
authors = ["Davin K. Tanabe <davin.tanabe@digitalasset.com>"]

python/dazl/ledger/_retry.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright (c) 2017-2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
from asyncio import sleep
4+
from datetime import datetime, timedelta
5+
from typing import Awaitable, Callable, TypeVar
6+
7+
from grpc import RpcError, StatusCode
8+
9+
T = TypeVar("T")
10+
11+
DEFAULT_TIMEOUT = timedelta(minutes=1)
12+
13+
14+
def is_retryable_exception(ex: Exception) -> bool:
15+
if not isinstance(ex, RpcError):
16+
return False
17+
18+
# TODO: Expand on these categories as documented here:
19+
# https://docs.daml.com/app-dev/grpc/error-codes.html
20+
return ex.code() == StatusCode.FAILED_PRECONDITION and ex.details().startswith(
21+
"PARTY_ALLOCATION_WITHOUT_CONNECTED_DOMAIN"
22+
)
23+
24+
25+
async def retry(
26+
fn: Callable[[], Awaitable[T]],
27+
*,
28+
retry_safe_ex: Callable[[Exception], bool] = is_retryable_exception,
29+
timeout: timedelta = DEFAULT_TIMEOUT
30+
) -> T:
31+
a, b = 0, 1
32+
start = datetime.utcnow()
33+
while True:
34+
try:
35+
return await fn()
36+
except Exception as ex:
37+
if (datetime.now() - start) <= timeout and retry_safe_ex(ex):
38+
# retry in increasing intervals according to the Fibonacci sequence;
39+
# exponential backoff generally makes us wait too long
40+
a, b = b, a + b
41+
await sleep(a)
42+
else:
43+
raise

python/dazl/ledger/config/access.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ def token(self, value: str) -> None:
247247

248248
v1_claims = claims.get(DamlLedgerApiNamespace)
249249
if "daml_ledger_api" in claims.get("scope", "").split(" "):
250-
# "scope": "daml_ledegr_api" is present, which makes it a Daml V2 token
250+
# "scope": "daml_ledger_api" is present, which makes it a Daml V2 token
251251
self._ledger_id = None
252-
self._application_name = None
252+
self._application_name = claims.get("sub")
253253
self._token_version = 2
254254

255255
elif v1_claims is not None:
@@ -266,7 +266,7 @@ def token(self, value: str) -> None:
266266
else:
267267
# we're not _entirely_ sure what kind of token it is; assume it's 2
268268
self._ledger_id = None
269-
self._application_name = None
269+
self._application_name = claims.get("sub")
270270
self._token_version = 2
271271

272272
def _set(self, *, read_as: Collection[Party], act_as: Collection[Party], admin: bool):

python/dazl/ledger/grpc/conn_aio.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from ...prim import ContractData, ContractId, Party, datetime_to_timestamp, to_parties
3434
from ...query import Filter, Queries, Query, parse_query
3535
from .._offsets import END, End, LedgerOffsetRange, from_offset_until_forever
36+
from .._retry import retry
3637
from ..api_types import (
3738
ActAs,
3839
Admin,
@@ -50,7 +51,7 @@
5051
Version,
5152
)
5253
from ..config import Config
53-
from ..config.access import PropertyBasedAccessConfig, TokenBasedAccessConfig
54+
from ..config.access import TokenBasedAccessConfig
5455
from ..errors import ProtocolWarning, _allow_cancel, _translate_exceptions
5556
from .channel import create_channel
5657
from .codec_aio import Codec
@@ -110,8 +111,10 @@ async def open(self) -> None:
110111
for right in await self.list_user_rights():
111112
if right == Admin:
112113
admin = True
113-
elif isinstance(right, (ReadAs, ActAs)):
114+
elif isinstance(right, ReadAs):
114115
read_as.append(right.party)
116+
elif isinstance(right, ActAs):
117+
act_as.append(right.party)
115118

116119
# noinspection PyProtectedMember
117120
cast(TokenBasedAccessConfig, self._config.access)._set(
@@ -512,7 +515,7 @@ def _submit_and_wait_request(
512515
# this is support for versions of Daml Connect prior to 1.9.0
513516
act_as = meta.act_as
514517
if act_as is not None:
515-
act_as_party = act_as[0]
518+
act_as_party = act_as[0] if act_as else None
516519
else:
517520
raise ValueError("current access rights do not include any act-as parties")
518521

@@ -748,7 +751,8 @@ async def allocate_party(
748751
party_id_hint=Party(identifier_hint) if identifier_hint else None,
749752
display_name=display_name,
750753
)
751-
response = await stub.AllocateParty(request)
754+
755+
response = await retry(lambda: stub.AllocateParty(request))
752756
return Codec.decode_party_info(response.party_details)
753757

754758
async def list_known_parties(self) -> Sequence[PartyInfo]:

python/tests/unit/config.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
# Copyright (c) 2017-2022 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
22
# SPDX-License-Identifier: Apache-2.0
33
import os
4+
import sys
45
from typing import Sequence, Union
56

6-
__all__ = ["daml_sdk_versions"]
7+
if sys.version_info >= (3, 8):
8+
from typing import Final
9+
else:
10+
from typing_extensions import Final
711

12+
__all__ = ["daml_sdk_versions", "known_version_1", "known_version_2", "known_versions"]
813

9-
known_versions = ("1.18.1", "2.0.0")
14+
15+
known_version_1: Final = "1.18.1"
16+
known_version_2: Final = "2.2.0"
17+
known_versions: Final = (known_version_1, known_version_2)
1018

1119

1220
def daml_sdk_versions(

python/tests/unit/test_ledger_token.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,45 @@
33
from asyncio import sleep
44

55
from dazl import connect, testing
6+
from dazl.ledger import ActAs, Admin, User
67
from dazl.ledger.config.access import DamlLedgerApiNamespace
78
import pytest
89

9-
from .config import daml_sdk_versions
10+
from .config import daml_sdk_versions, known_version_2
11+
from .dars import PostOffice
1012

1113

1214
@pytest.mark.asyncio
1315
@pytest.mark.parametrize("daml_sdk_version", daml_sdk_versions())
1416
async def test_v1_token(daml_sdk_version):
15-
with testing.sandbox(version=daml_sdk_version, use_auth=True, ledger_id="sandbox") as sandbox:
17+
async with testing.sandbox(
18+
version=daml_sdk_version, use_auth=True, ledger_id="sandbox"
19+
) as sandbox:
1620
token = sandbox.sign_token({DamlLedgerApiNamespace: {"admin": True, "ledgerId": "sandbox"}})
1721
async with connect(url=sandbox.url, oauth_token=token) as conn:
1822
# the result of this call is not particularly interesting;
1923
# we just need to make sure it doesn't crash
2024
await conn.list_package_ids()
2125
await sleep(1)
26+
27+
28+
@pytest.mark.asyncio
29+
async def test_v2_token(sandbox_v2):
30+
async with testing.sandbox(
31+
version=known_version_2, use_auth=True, ledger_id="sandbox"
32+
) as sandbox:
33+
# use an anonymous admin Daml V1 token to bootstrap users, because that's unfortunately
34+
# the only way
35+
token = sandbox.sign_token({DamlLedgerApiNamespace: {"admin": True, "ledgerId": "sandbox"}})
36+
async with connect(url=sandbox.url, oauth_token=token) as conn:
37+
p = await conn.allocate_party(identifier_hint="alice", display_name="alice")
38+
await conn.create_user(User("alice", p.party), [Admin, ActAs(p.party)])
39+
await conn.upload_package(PostOffice.read_bytes())
40+
41+
token = sandbox.sign_token({"sub": "alice", "scope": "daml_ledger_api"})
42+
async with connect(url=sandbox.url, oauth_token=token) as conn:
43+
# make sure we have admin rights
44+
await conn.allocate_party()
45+
46+
# make sure we can create contracts
47+
await conn.create("Main:PostmanRole", {"postman": p.party})

0 commit comments

Comments
 (0)