Skip to content

Commit

Permalink
BREAKING: VCHolder multitenant binding (openwallet-foundation#3391)
Browse files Browse the repository at this point in the history
* VCHolder multitenant binding

Signed-off-by: jamshale <[email protected]>

* fix: soft binding for MT askar of VC Holder

Signed-off-by: Daniel Bluhm <[email protected]>

* fix: openapi requests and responses for vc routes

Signed-off-by: Daniel Bluhm <[email protected]>

* fix: example script result assertion

Signed-off-by: Daniel Bluhm <[email protected]>

---------

Signed-off-by: jamshale <[email protected]>
Signed-off-by: Daniel Bluhm <[email protected]>
Co-authored-by: Daniel Bluhm <[email protected]>
  • Loading branch information
jamshale and dbluhm authored Dec 10, 2024
1 parent 7871842 commit a2a6dd2
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 7 deletions.
2 changes: 1 addition & 1 deletion acapy_agent/askar/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def bind_providers(self):
VCHolder,
ClassProvider(
"acapy_agent.storage.vc_holder.askar.AskarVCHolder",
ref(self),
ClassProvider.Inject(Profile),
),
)
if (
Expand Down
11 changes: 9 additions & 2 deletions acapy_agent/vc/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ async def list_credentials_route(request: web.BaseRequest):
holder = context.inject(VCHolder)
try:
search = holder.search_credentials()
records = [record.serialize()["cred_value"] for record in await search.fetch()]
records = {
"results": [
record.serialize()["cred_value"] for record in await search.fetch()
]
}
return web.json_response(records, status=200)
except (StorageError, StorageNotFoundError) as err:
return web.json_response({"message": err.roll_up}, status=400)
Expand Down Expand Up @@ -133,6 +137,9 @@ async def verify_credential_route(request: web.BaseRequest):


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

await manager.verify_credential(vc)
await manager.store_credential(vc, options, cred_id)
await manager.store_credential(vc, cred_id)

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

Expand Down
5 changes: 3 additions & 2 deletions acapy_agent/vc/vc_ld/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,9 +405,8 @@ async def issue(
async def store_credential(
self,
vc: VerifiableCredential,
options: LDProofVCOptions,
cred_id: Optional[str] = None,
) -> VerifiableCredential:
) -> VCRecord:
"""Store a verifiable credential."""

# Saving expanded type as a cred_tag
Expand Down Expand Up @@ -437,6 +436,8 @@ async def store_credential(

await vc_holder.store_credential(vc_record)

return vc_record

async def verify_credential(
self, vc: VerifiableCredential
) -> DocumentVerificationResult:
Expand Down
14 changes: 13 additions & 1 deletion acapy_agent/vc/vc_ld/models/web_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
class ListCredentialsResponse(OpenAPISchema):
"""Response schema for listing credentials."""

results = [fields.Nested(VerifiableCredentialSchema)]
results = fields.List(fields.Nested(VerifiableCredentialSchema))


class FetchCredentialResponse(OpenAPISchema):
Expand Down Expand Up @@ -47,6 +47,18 @@ class VerifyCredentialResponse(OpenAPISchema):
results = fields.Nested(PresentationVerificationResultSchema)


class StoreCredentialRequest(OpenAPISchema):
"""Request schema for verifying an LDP VP."""

verifiableCredential = fields.Nested(VerifiableCredentialSchema)


class StoreCredentialResponse(OpenAPISchema):
"""Request schema for verifying an LDP VP."""

credentialId = fields.Str()


class ProvePresentationRequest(OpenAPISchema):
"""Request schema for proving a presentation."""

Expand Down
2 changes: 1 addition & 1 deletion acapy_agent/vc/vc_ld/tests/test_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ async def test_store(
self.vc.issuer = did.did
self.options.proof_type = Ed25519Signature2018.signature_type
cred = await self.manager.issue(self.vc, self.options)
await self.manager.store_credential(cred, self.options, TEST_UUID)
await self.manager.store_credential(cred, TEST_UUID)
async with self.profile.session() as session:
holder = session.inject(VCHolder)
record = await holder.retrieve_credential_by_id(record_id=TEST_UUID)
Expand Down
45 changes: 45 additions & 0 deletions scenarios/examples/vc_holder/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
services:
agency:
image: acapy-test
ports:
- "3001:3001"
environment:
RUST_LOG: 'aries-askar::log::target=error'
command: >
start
--label Agency
--inbound-transport http 0.0.0.0 3000
--outbound-transport http
--endpoint http://agency:3000
--admin 0.0.0.0 3001
--admin-insecure-mode
--no-ledger
--wallet-type askar
--wallet-name alice
--wallet-key insecure
--auto-provision
--log-level debug
--debug-webhooks
--multitenant
--multitenant-admin
--jwt-secret insecure
--multitenancy-config wallet_type=single-wallet-askar key_derivation_method=RAW
healthcheck:
test: curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001/status/live" | grep "200" > /dev/null
start_period: 30s
interval: 7s
timeout: 5s
retries: 5

example:
container_name: controller
build:
context: ../..
environment:
- AGENCY=http://agency:3001
volumes:
- ./example.py:/usr/src/app/example.py:ro,z
command: python -m example
depends_on:
agency:
condition: service_healthy
95 changes: 95 additions & 0 deletions scenarios/examples/vc_holder/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Test VC Holder multi-tenancy isolation."""

import asyncio
from os import getenv

from acapy_controller import Controller
from acapy_controller.logging import logging_to_stdout
from acapy_controller.models import CreateWalletResponse
from acapy_controller.protocols import DIDResult

AGENCY = getenv("AGENCY", "http://agency:3001")


async def main():
"""Test Controller protocols."""
async with Controller(base_url=AGENCY) as agency:
issuer = await agency.post(
"/multitenancy/wallet",
json={
"label": "Issuer",
"wallet_type": "askar",
},
response=CreateWalletResponse,
)
alice = await agency.post(
"/multitenancy/wallet",
json={
"label": "Alice",
"wallet_type": "askar",
},
response=CreateWalletResponse,
)
bob = await agency.post(
"/multitenancy/wallet",
json={
"label": "Bob",
"wallet_type": "askar",
},
response=CreateWalletResponse,
)

async with (
Controller(
base_url=AGENCY, wallet_id=alice.wallet_id, subwallet_token=alice.token
) as alice,
Controller(
base_url=AGENCY, wallet_id=bob.wallet_id, subwallet_token=bob.token
) as bob,
Controller(
base_url=AGENCY, wallet_id=issuer.wallet_id, subwallet_token=issuer.token
) as issuer,
):
public_did = (
await issuer.post(
"/wallet/did/create",
json={"method": "key", "options": {"key_type": "ed25519"}},
response=DIDResult,
)
).result
assert public_did
cred = await issuer.post(
"/vc/credentials/issue",
json={
"credential": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1",
],
"id": "http://example.edu/credentials/1872",
"credentialSubject": {
"id": "did:example:ebfeb1f712ebc6f1c276e12ec21"
},
"issuer": public_did.did,
"issuanceDate": "2024-12-10T10:00:00Z",
"type": ["VerifiableCredential", "AlumniCredential"],
},
"options": {
"challenge": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"domain": "example.com",
"proofPurpose": "assertionMethod",
"proofType": "Ed25519Signature2018",
},
},
)
await alice.post(
"/vc/credentials/store",
json={"verifiableCredential": cred["verifiableCredential"]},
)
result = await bob.get("/vc/credentials")
assert len(result["results"]) == 0


if __name__ == "__main__":
logging_to_stdout()
asyncio.run(main())

0 comments on commit a2a6dd2

Please sign in to comment.