Skip to content

Commit a2a6dd2

Browse files
jamshaledbluhm
andauthored
BREAKING: VCHolder multitenant binding (openwallet-foundation#3391)
* VCHolder multitenant binding Signed-off-by: jamshale <jamiehalebc@gmail.com> * fix: soft binding for MT askar of VC Holder Signed-off-by: Daniel Bluhm <dbluhm@pm.me> * fix: openapi requests and responses for vc routes Signed-off-by: Daniel Bluhm <dbluhm@pm.me> * fix: example script result assertion Signed-off-by: Daniel Bluhm <dbluhm@pm.me> --------- Signed-off-by: jamshale <jamiehalebc@gmail.com> Signed-off-by: Daniel Bluhm <dbluhm@pm.me> Co-authored-by: Daniel Bluhm <dbluhm@pm.me>
1 parent 7871842 commit a2a6dd2

File tree

7 files changed

+167
-7
lines changed

7 files changed

+167
-7
lines changed

acapy_agent/askar/profile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def bind_providers(self):
123123
VCHolder,
124124
ClassProvider(
125125
"acapy_agent.storage.vc_holder.askar.AskarVCHolder",
126-
ref(self),
126+
ClassProvider.Inject(Profile),
127127
),
128128
)
129129
if (

acapy_agent/vc/routes.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ async def list_credentials_route(request: web.BaseRequest):
3434
holder = context.inject(VCHolder)
3535
try:
3636
search = holder.search_credentials()
37-
records = [record.serialize()["cred_value"] for record in await search.fetch()]
37+
records = {
38+
"results": [
39+
record.serialize()["cred_value"] for record in await search.fetch()
40+
]
41+
}
3842
return web.json_response(records, status=200)
3943
except (StorageError, StorageNotFoundError) as err:
4044
return web.json_response({"message": err.roll_up}, status=400)
@@ -133,6 +137,9 @@ async def verify_credential_route(request: web.BaseRequest):
133137

134138

135139
@docs(tags=["vc-api"], summary="Store a credential")
140+
@request_schema(web_schemas.StoreCredentialRequest())
141+
@response_schema(web_schemas.StoreCredentialResponse(), 200, description="")
142+
@tenant_authentication
136143
async def store_credential_route(request: web.BaseRequest):
137144
"""Request handler for storing a credential.
138145
@@ -153,7 +160,7 @@ async def store_credential_route(request: web.BaseRequest):
153160
options = LDProofVCOptions.deserialize(options)
154161

155162
await manager.verify_credential(vc)
156-
await manager.store_credential(vc, options, cred_id)
163+
await manager.store_credential(vc, cred_id)
157164

158165
return web.json_response({"credentialId": cred_id}, status=200)
159166

acapy_agent/vc/vc_ld/manager.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,9 +405,8 @@ async def issue(
405405
async def store_credential(
406406
self,
407407
vc: VerifiableCredential,
408-
options: LDProofVCOptions,
409408
cred_id: Optional[str] = None,
410-
) -> VerifiableCredential:
409+
) -> VCRecord:
411410
"""Store a verifiable credential."""
412411

413412
# Saving expanded type as a cred_tag
@@ -437,6 +436,8 @@ async def store_credential(
437436

438437
await vc_holder.store_credential(vc_record)
439438

439+
return vc_record
440+
440441
async def verify_credential(
441442
self, vc: VerifiableCredential
442443
) -> DocumentVerificationResult:

acapy_agent/vc/vc_ld/models/web_schemas.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
class ListCredentialsResponse(OpenAPISchema):
1313
"""Response schema for listing credentials."""
1414

15-
results = [fields.Nested(VerifiableCredentialSchema)]
15+
results = fields.List(fields.Nested(VerifiableCredentialSchema))
1616

1717

1818
class FetchCredentialResponse(OpenAPISchema):
@@ -47,6 +47,18 @@ class VerifyCredentialResponse(OpenAPISchema):
4747
results = fields.Nested(PresentationVerificationResultSchema)
4848

4949

50+
class StoreCredentialRequest(OpenAPISchema):
51+
"""Request schema for verifying an LDP VP."""
52+
53+
verifiableCredential = fields.Nested(VerifiableCredentialSchema)
54+
55+
56+
class StoreCredentialResponse(OpenAPISchema):
57+
"""Request schema for verifying an LDP VP."""
58+
59+
credentialId = fields.Str()
60+
61+
5062
class ProvePresentationRequest(OpenAPISchema):
5163
"""Request schema for proving a presentation."""
5264

acapy_agent/vc/vc_ld/tests/test_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ async def test_store(
331331
self.vc.issuer = did.did
332332
self.options.proof_type = Ed25519Signature2018.signature_type
333333
cred = await self.manager.issue(self.vc, self.options)
334-
await self.manager.store_credential(cred, self.options, TEST_UUID)
334+
await self.manager.store_credential(cred, TEST_UUID)
335335
async with self.profile.session() as session:
336336
holder = session.inject(VCHolder)
337337
record = await holder.retrieve_credential_by_id(record_id=TEST_UUID)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
services:
2+
agency:
3+
image: acapy-test
4+
ports:
5+
- "3001:3001"
6+
environment:
7+
RUST_LOG: 'aries-askar::log::target=error'
8+
command: >
9+
start
10+
--label Agency
11+
--inbound-transport http 0.0.0.0 3000
12+
--outbound-transport http
13+
--endpoint http://agency:3000
14+
--admin 0.0.0.0 3001
15+
--admin-insecure-mode
16+
--no-ledger
17+
--wallet-type askar
18+
--wallet-name alice
19+
--wallet-key insecure
20+
--auto-provision
21+
--log-level debug
22+
--debug-webhooks
23+
--multitenant
24+
--multitenant-admin
25+
--jwt-secret insecure
26+
--multitenancy-config wallet_type=single-wallet-askar key_derivation_method=RAW
27+
healthcheck:
28+
test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null
29+
start_period: 30s
30+
interval: 7s
31+
timeout: 5s
32+
retries: 5
33+
34+
example:
35+
container_name: controller
36+
build:
37+
context: ../..
38+
environment:
39+
- AGENCY=http://agency:3001
40+
volumes:
41+
- ./example.py:/usr/src/app/example.py:ro,z
42+
command: python -m example
43+
depends_on:
44+
agency:
45+
condition: service_healthy
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Test VC Holder multi-tenancy isolation."""
2+
3+
import asyncio
4+
from os import getenv
5+
6+
from acapy_controller import Controller
7+
from acapy_controller.logging import logging_to_stdout
8+
from acapy_controller.models import CreateWalletResponse
9+
from acapy_controller.protocols import DIDResult
10+
11+
AGENCY = getenv("AGENCY", "http://agency:3001")
12+
13+
14+
async def main():
15+
"""Test Controller protocols."""
16+
async with Controller(base_url=AGENCY) as agency:
17+
issuer = await agency.post(
18+
"/multitenancy/wallet",
19+
json={
20+
"label": "Issuer",
21+
"wallet_type": "askar",
22+
},
23+
response=CreateWalletResponse,
24+
)
25+
alice = await agency.post(
26+
"/multitenancy/wallet",
27+
json={
28+
"label": "Alice",
29+
"wallet_type": "askar",
30+
},
31+
response=CreateWalletResponse,
32+
)
33+
bob = await agency.post(
34+
"/multitenancy/wallet",
35+
json={
36+
"label": "Bob",
37+
"wallet_type": "askar",
38+
},
39+
response=CreateWalletResponse,
40+
)
41+
42+
async with (
43+
Controller(
44+
base_url=AGENCY, wallet_id=alice.wallet_id, subwallet_token=alice.token
45+
) as alice,
46+
Controller(
47+
base_url=AGENCY, wallet_id=bob.wallet_id, subwallet_token=bob.token
48+
) as bob,
49+
Controller(
50+
base_url=AGENCY, wallet_id=issuer.wallet_id, subwallet_token=issuer.token
51+
) as issuer,
52+
):
53+
public_did = (
54+
await issuer.post(
55+
"/wallet/did/create",
56+
json={"method": "key", "options": {"key_type": "ed25519"}},
57+
response=DIDResult,
58+
)
59+
).result
60+
assert public_did
61+
cred = await issuer.post(
62+
"/vc/credentials/issue",
63+
json={
64+
"credential": {
65+
"@context": [
66+
"https://www.w3.org/2018/credentials/v1",
67+
"https://www.w3.org/2018/credentials/examples/v1",
68+
],
69+
"id": "http://example.edu/credentials/1872",
70+
"credentialSubject": {
71+
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21"
72+
},
73+
"issuer": public_did.did,
74+
"issuanceDate": "2024-12-10T10:00:00Z",
75+
"type": ["VerifiableCredential", "AlumniCredential"],
76+
},
77+
"options": {
78+
"challenge": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
79+
"domain": "example.com",
80+
"proofPurpose": "assertionMethod",
81+
"proofType": "Ed25519Signature2018",
82+
},
83+
},
84+
)
85+
await alice.post(
86+
"/vc/credentials/store",
87+
json={"verifiableCredential": cred["verifiableCredential"]},
88+
)
89+
result = await bob.get("/vc/credentials")
90+
assert len(result["results"]) == 0
91+
92+
93+
if __name__ == "__main__":
94+
logging_to_stdout()
95+
asyncio.run(main())

0 commit comments

Comments
 (0)