Skip to content

Commit

Permalink
✨ Add endpoint to fix revocation registry state (#1206)
Browse files Browse the repository at this point in the history
* add revocation routes

* 🎨

* add fix-rev-reg endpoint

* 🎨

* add unit test

* add e2e test fix_rev_reg with bad id

* make line short

* update base path name

* update description

* add param to logger

* 🎨 Update query param and response descriptions

---------

Co-authored-by: ff137 <ff137@proton.me>
  • Loading branch information
cl0ete and ff137 authored Dec 4, 2024
1 parent 7dbc52c commit dae89e0
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 32 deletions.
60 changes: 58 additions & 2 deletions app/routes/revocation.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import asyncio
from typing import Optional

from aries_cloudcontroller import IssuerCredRevRecord
from aries_cloudcontroller import IssuerCredRevRecord, RevRegWalletUpdatedResult
from fastapi import APIRouter, Depends

from app.dependencies.acapy_clients import client_from_auth
from app.dependencies.auth import AcaPyAuth, acapy_auth_from_header
from app.exceptions import CloudApiException
from app.exceptions import CloudApiException, handle_acapy_call
from app.models.issuer import (
ClearPendingRevocationsRequest,
ClearPendingRevocationsResult,
Expand Down Expand Up @@ -313,3 +313,59 @@ async def get_pending_revocations(

bound_logger.debug("Successfully fetched pending revocations.")
return PendingRevocations(pending_cred_rev_ids=result)


@router.put(
"/fix-revocation-registry/{revocation_registry_id}",
summary="Fix Revocation Registry Entry State",
)
async def fix_revocation_registry_entry_state(
revocation_registry_id: str,
apply_ledger_update: bool = False,
auth: AcaPyAuth = Depends(acapy_auth_from_header),
) -> RevRegWalletUpdatedResult:
"""
Fix Revocation Registry Entry State
---
Fix the revocation registry entry state for a given revocation registry ID.
If issuer's revocation registry wallet state is out of sync with the ledger,
this endpoint can be used to fix/update the ledger state.
Path Parameters:
---
revocation_registry_id: str
The ID of the revocation registry for which to fix the state
Query Parameters:
---
apply_ledger_update: bool
Apply changes to ledger (default: False). If False, only computes the difference
between the wallet and ledger state.
Returns:
---
RevRegWalletUpdatedResult:
accum_calculated: The calculated accumulator value for any revocations not yet published to ledger
accum_fixed: The result of applying the ledger transaction to synchronize revocation state
rev_reg_delta: The delta between wallet and ledger state for this revocation registry
"""
bound_logger = logger.bind(
body={
"revocation_registry_id": revocation_registry_id,
"apply_ledger_update": apply_ledger_update,
}
)
bound_logger.debug("PUT request received: Fix revocation registry entry state")

async with client_from_auth(auth) as aries_controller:
bound_logger.debug("Fixing revocation registry entry state")
response = await handle_acapy_call(
logger=bound_logger,
acapy_call=aries_controller.revocation.update_rev_reg_revoked_state,
rev_reg_id=revocation_registry_id,
apply_ledger_update=apply_ledger_update,
)

bound_logger.debug("Successfully fixed revocation registry entry state.")
return response
83 changes: 55 additions & 28 deletions app/tests/e2e/test_revocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from shared import RichAsyncClient
from shared.models.credential_exchange import CredentialExchange

CREDENTIALS_BASE_PATH = router.prefix
REVOCATION_BASE_PATH = router.prefix
VERIFIER_BASE_PATH = verifier_router.prefix

skip_regression_test_reason = "Skip publish-revocations in regression mode"
Expand All @@ -26,7 +26,7 @@ async def test_clear_pending_revokes(
):
faber_cred_ex_id = revoke_alice_creds[0].credential_exchange_id
revocation_record_response = await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/revocation/record"
f"{REVOCATION_BASE_PATH}/revocation/record"
+ "?credential_exchange_id="
+ faber_cred_ex_id
)
Expand All @@ -35,7 +35,7 @@ async def test_clear_pending_revokes(
cred_rev_id = revocation_record_response.json()["cred_rev_id"]

clear_revoke_response = await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/clear-pending-revocations",
f"{REVOCATION_BASE_PATH}/clear-pending-revocations",
json={"revocation_registry_credential_map": {rev_reg_id: [cred_rev_id]}},
)
revocation_registry_credential_map = clear_revoke_response.json()[
Expand All @@ -48,7 +48,7 @@ async def test_clear_pending_revokes(
), "We expect at least two cred_rev_ids per rev_reg_id after revoking one"

clear_revoke_response = await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/clear-pending-revocations",
f"{REVOCATION_BASE_PATH}/clear-pending-revocations",
json={"revocation_registry_credential_map": {rev_reg_id: []}},
)
revocation_registry_credential_map = clear_revoke_response.json()[
Expand All @@ -60,7 +60,7 @@ async def test_clear_pending_revokes(
for cred in revoke_alice_creds:
rev_record = (
await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/revocation/record"
f"{REVOCATION_BASE_PATH}/revocation/record"
+ "?credential_exchange_id="
+ cred.credential_exchange_id
)
Expand All @@ -71,7 +71,7 @@ async def test_clear_pending_revokes(
# Test for cred_rev_id not pending
with pytest.raises(HTTPException) as exc:
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/clear-pending-revocations",
f"{REVOCATION_BASE_PATH}/clear-pending-revocations",
json={"revocation_registry_credential_map": {rev_reg_id: [cred_rev_id]}},
)
assert exc.value.status_code == 404
Expand All @@ -88,7 +88,7 @@ async def test_clear_pending_revokes_no_map(
):
# clear_revoke_response = (
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/clear-pending-revocations",
f"{REVOCATION_BASE_PATH}/clear-pending-revocations",
json={"revocation_registry_credential_map": {}},
)
# ).json()["revocation_registry_credential_map"]
Expand All @@ -98,7 +98,7 @@ async def test_clear_pending_revokes_no_map(
for cred in revoke_alice_creds:
rev_record = (
await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/revocation/record"
f"{REVOCATION_BASE_PATH}/revocation/record"
+ "?credential_exchange_id="
+ cred.credential_exchange_id
)
Expand All @@ -117,23 +117,23 @@ async def test_clear_pending_revokes_bad_payload(
):
with pytest.raises(HTTPException) as exc:
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/clear-pending-revocations",
f"{REVOCATION_BASE_PATH}/clear-pending-revocations",
json={"revocation_registry_credential_map": "bad"},
)

assert exc.value.status_code == 422

with pytest.raises(HTTPException) as exc:
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/clear-pending-revocations",
f"{REVOCATION_BASE_PATH}/clear-pending-revocations",
json={"revocation_registry_credential_map": {"bad": "bad"}},
)

assert exc.value.status_code == 422

with pytest.raises(HTTPException) as exc:
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/clear-pending-revocations",
f"{REVOCATION_BASE_PATH}/clear-pending-revocations",
json={
"revocation_registry_credential_map": {
"WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0": []
Expand All @@ -156,7 +156,7 @@ async def test_publish_all_revocations_for_rev_reg_id(
faber_cred_ex_id = revoke_alice_creds[0].credential_exchange_id
response = (
await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/revocation/record"
f"{REVOCATION_BASE_PATH}/revocation/record"
+ "?credential_exchange_id="
+ faber_cred_ex_id
)
Expand All @@ -165,14 +165,14 @@ async def test_publish_all_revocations_for_rev_reg_id(
rev_reg_id = response["rev_reg_id"]

await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/publish-revocations",
f"{REVOCATION_BASE_PATH}/publish-revocations",
json={"revocation_registry_credential_map": {rev_reg_id: []}},
)

for cred in revoke_alice_creds:
rev_record = (
await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/revocation/record"
f"{REVOCATION_BASE_PATH}/revocation/record"
+ "?credential_exchange_id="
+ cred.credential_exchange_id
)
Expand All @@ -191,14 +191,14 @@ async def test_publish_all_revocations_no_payload(
revoke_alice_creds: List[CredentialExchange],
):
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/publish-revocations",
f"{REVOCATION_BASE_PATH}/publish-revocations",
json={"revocation_registry_credential_map": {}},
)

for cred in revoke_alice_creds:
rev_record = (
await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/revocation/record"
f"{REVOCATION_BASE_PATH}/revocation/record"
+ "?credential_exchange_id="
+ cred.credential_exchange_id
)
Expand All @@ -219,7 +219,7 @@ async def test_publish_one_revocation(
faber_cred_ex_id = revoke_alice_creds[0].credential_exchange_id
response = (
await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/revocation/record"
f"{REVOCATION_BASE_PATH}/revocation/record"
+ "?credential_exchange_id="
+ faber_cred_ex_id
)
Expand All @@ -228,14 +228,14 @@ async def test_publish_one_revocation(
rev_reg_id = response["rev_reg_id"]
cred_rev_id = response["cred_rev_id"]
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/publish-revocations",
f"{REVOCATION_BASE_PATH}/publish-revocations",
json={"revocation_registry_credential_map": {rev_reg_id: [cred_rev_id]}},
)

for cred in revoke_alice_creds:
rev_record = (
await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/revocation/record"
f"{REVOCATION_BASE_PATH}/revocation/record"
+ "?credential_exchange_id="
+ cred.credential_exchange_id
)
Expand All @@ -249,7 +249,7 @@ async def test_publish_one_revocation(
# Test for cred_rev_id not pending
with pytest.raises(HTTPException) as exc:
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/publish-revocations",
f"{REVOCATION_BASE_PATH}/publish-revocations",
json={"revocation_registry_credential_map": {rev_reg_id: [cred_rev_id]}},
)

Expand All @@ -266,23 +266,23 @@ async def test_publish_revocations_bad_payload(
):
with pytest.raises(HTTPException) as exc:
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/publish-revocations",
f"{REVOCATION_BASE_PATH}/publish-revocations",
json={"revocation_registry_credential_map": "bad"},
)

assert exc.value.status_code == 422

with pytest.raises(HTTPException) as exc:
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/publish-revocations",
f"{REVOCATION_BASE_PATH}/publish-revocations",
json={"revocation_registry_credential_map": {"bad": "bad"}},
)

assert exc.value.status_code == 422

with pytest.raises(HTTPException) as exc:
await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/publish-revocations",
f"{REVOCATION_BASE_PATH}/publish-revocations",
json={
"revocation_registry_credential_map": {
"WgWxqztrNooG92RXvxSTWv:4:WgWxqztrNooG92RXvxSTWv:3:CL:20:tag:CL_ACCUM:0": []
Expand All @@ -304,7 +304,7 @@ async def test_get_pending_revocations(
):
faber_cred_ex_id = revoke_alice_creds[0].credential_exchange_id
revocation_record_response = await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/revocation/record"
f"{REVOCATION_BASE_PATH}/revocation/record"
+ "?credential_exchange_id="
+ faber_cred_ex_id
)
Expand All @@ -313,7 +313,7 @@ async def test_get_pending_revocations(

pending_revocations = (
await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/get-pending-revocations/{rev_reg_id}"
f"{REVOCATION_BASE_PATH}/get-pending-revocations/{rev_reg_id}"
)
).json()["pending_cred_rev_ids"]

Expand All @@ -322,13 +322,13 @@ async def test_get_pending_revocations(
) # we expect at least 3 cred_rev_ids can be more if whole module is run

await faber_client.post(
f"{CREDENTIALS_BASE_PATH}/clear-pending-revocations",
f"{REVOCATION_BASE_PATH}/clear-pending-revocations",
json={"revocation_registry_credential_map": {}},
)

pending_revocations = (
await faber_client.get(
f"{CREDENTIALS_BASE_PATH}/get-pending-revocations/{rev_reg_id}"
f"{REVOCATION_BASE_PATH}/get-pending-revocations/{rev_reg_id}"
)
).json()["pending_cred_rev_ids"]

Expand All @@ -344,6 +344,33 @@ async def test_get_pending_revocations_bad_payload(
faber_client: RichAsyncClient,
):
with pytest.raises(HTTPException) as exc:
await faber_client.get(f"{CREDENTIALS_BASE_PATH}/get-pending-revocations/bad")
await faber_client.get(f"{REVOCATION_BASE_PATH}/get-pending-revocations/bad")

assert exc.value.status_code == 422


@pytest.mark.anyio
@pytest.mark.skipif(
TestMode.regression_run in TestMode.fixture_params,
reason=skip_regression_test_reason,
)
@pytest.mark.parametrize(
"rev_reg_id, status_code",
[
(
"Ddhz428iyF5h96uLUgiuFa:4:Ddhz428iyF5h96uLUgiuFa:3:CL:8:Epic:CL_ACCUM:2e292c76-bc43-496c-a65a-297fc49c21c6",
404,
),
("bad_format", 422),
],
)
async def test_fix_rev_reg_bad_id(
faber_client: RichAsyncClient, rev_reg_id: str, status_code: int
):
with pytest.raises(HTTPException) as exc:
await faber_client.put(
f"{REVOCATION_BASE_PATH}/fix-revocation-registry/{rev_reg_id}",
params={"apply_ledger_update": False},
)

assert exc.value.status_code == status_code
Loading

0 comments on commit dae89e0

Please sign in to comment.