Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING: VCHolder multitenant binding #3391

Merged
merged 4 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention here was to enable the controller to set tags and pass them through options if they want to build a custom tag querying integration. This can be revisited at a later time, and we would likely provide the tags directly to the store_credential function instead of the options object.

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())
Loading