Skip to content

Commit be32097

Browse files
committed
pangea-sdk: make Audit's hash optional
These won't be present if tamperproofing is disabled.
1 parent b0ab466 commit be32097

File tree

11 files changed

+2836
-41
lines changed

11 files changed

+2836
-41
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- AI Guard: `messages` parameter is no longer a generic. A new `Message` model
1313
has been introduced, and `messages` is now a `Sequence[Message]`.
14+
- Audit: event data's `hash` is now optional.
1415

1516
## 6.1.1 - 2025-05-12
1617

examples/audit/audit_examples/log_n_search_custom_schema.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import datetime
24
import os
35

@@ -86,6 +88,7 @@ def print_page_results(search_res: PangeaResponse[SearchResultOutput], offset: i
8688
assert search_res.result
8789
print("\n--------------------------------------------------------------------\n")
8890
for row in search_res.result.events:
91+
assert row.envelope.event
8992
print(
9093
f"{row.envelope.received_at}\t{row.envelope.event['message']}\t{row.membership_verification}\t\t {row.consistency_verification}\t\t {row.signature_verification}\t\t"
9194
)

examples/audit/audit_examples/log_n_search_standard_schema.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import os
24

35
import pangea.exceptions as pe
@@ -82,6 +84,7 @@ def print_page_results(search_res: PangeaResponse[SearchResultOutput], offset: i
8284
assert search_res.result
8385
print("\n--------------------------------------------------------------------\n")
8486
for row in search_res.result.events:
87+
assert row.envelope.event
8588
print(
8689
f"{row.envelope.received_at}\t{row.envelope.event['message']}\t{row.envelope.event['source']}\t\t"
8790
f"{row.envelope.event['actor']}\t\t{row.membership_verification}\t\t {row.consistency_verification}\t\t {row.signature_verification}\t\t"

examples/audit/poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/pangea-sdk/pangea/services/audit/audit.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,7 @@ def verify_signature(self, audit_envelope: EventEnvelope) -> EventVerification:
338338
public_key = get_public_key(audit_envelope.public_key)
339339

340340
if audit_envelope and audit_envelope.signature and public_key:
341+
assert audit_envelope.event
341342
v = Verifier()
342343
verification = v.verify_signature(
343344
audit_envelope.signature,

packages/pangea-sdk/pangea/services/audit/models.py

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
import enum
1111
from typing import Any, Dict, List, Optional, Sequence, Union
1212

13+
from pydantic import Field
14+
from typing_extensions import Annotated
15+
1316
from pangea.response import APIRequestModel, APIResponseModel, PangeaDateTime, PangeaResponseResult
1417

1518

@@ -117,20 +120,25 @@ def tenant_id(self, value):
117120

118121

119122
class EventEnvelope(APIResponseModel):
120-
"""
121-
Contain extra information about an event.
123+
event: Optional[dict[str, Any]] = None
122124

123-
Arguments:
124-
event -- Event describing auditable activity.
125-
signature -- An optional client-side signature for forgery protection.
126-
public_key -- The base64-encoded ed25519 public key used for the signature, if one is provided
127-
received_at -- A server-supplied timestamp
125+
signature: Optional[str] = None
126+
"""
127+
This is the signature of the hash of the canonicalized event that can be
128+
verified with the public key provided in the public_key field. Signatures
129+
cannot be used with the redaction feature turned on. If redaction is
130+
required, the user needs to perform redaction before computing the signature
131+
that is to be sent with the message. The SDK facilitates this for users.
128132
"""
129133

130-
event: Dict[str, Any]
131-
signature: Optional[str] = None
132134
public_key: Optional[str] = None
133-
received_at: PangeaDateTime
135+
"""
136+
The base64-encoded ed25519 public key used for the signature, if one is
137+
provided
138+
"""
139+
140+
received_at: Optional[PangeaDateTime] = None
141+
"""A Pangea provided timestamp of when the event was received."""
134142

135143

136144
class LogRequest(APIRequestModel):
@@ -181,21 +189,28 @@ class LogBulkRequest(APIRequestModel):
181189

182190

183191
class LogResult(PangeaResponseResult):
192+
envelope: Optional[EventEnvelope] = None
184193
"""
185-
Result class after an audit log action
186-
187-
envelope -- Event envelope information.
188-
hash -- Event envelope hash.
189-
unpublished_root -- The current unpublished root.
190-
membership_proof -- A proof for verifying the unpublished root.
191-
consistency_proof -- If prev_root was present in the request, this proof verifies that the new unpublished root is a continuation of the prev_root
194+
The sealed envelope containing the event that was logged. Includes event
195+
metadata such as optional client-side signature details and server-added
196+
timestamps.
192197
"""
193198

194-
envelope: Optional[EventEnvelope] = None
195-
hash: str
199+
hash: Annotated[Optional[str], Field(max_length=64, min_length=64)] = None
200+
"""The hash of the event data."""
201+
196202
unpublished_root: Optional[str] = None
203+
"""The current unpublished root."""
204+
197205
membership_proof: Optional[str] = None
206+
"""A proof for verifying that the buffer_root contains the received event"""
207+
198208
consistency_proof: Optional[List[str]] = None
209+
"""
210+
If prev_buffer_root was present in the request, this proof verifies that the
211+
new unpublished root is a continuation of prev_unpublished_root
212+
"""
213+
199214
consistency_verification: EventVerification = EventVerification.NONE
200215
membership_verification: EventVerification = EventVerification.NONE
201216
signature_verification: EventVerification = EventVerification.NONE
@@ -358,29 +373,47 @@ class RootResult(PangeaResponseResult):
358373

359374

360375
class SearchEvent(APIResponseModel):
376+
envelope: EventEnvelope
377+
378+
membership_proof: Optional[str] = None
379+
"""A cryptographic proof that the record has been persisted in the log"""
380+
381+
hash: Annotated[Optional[str], Field(max_length=64, min_length=64)] = None
382+
"""The record's hash"""
383+
384+
published: Optional[bool] = None
385+
"""
386+
If true, a root has been published after this event. If false, there is no
387+
published root for this event
361388
"""
362-
Event information received after a search request
363389

364-
Arguments:
365-
envelope -- Event related information.
366-
hash -- The record's hash.
367-
leaf_index -- The index of the leaf of the Merkle Tree where this record was inserted.
368-
membership_proof -- A cryptographic proof that the record has been persisted in the log.
369-
consistency_verification -- Consistency verification calculated if required.
370-
membership_verification -- Membership verification calculated if required.
371-
signature_verification -- Signature verification calculated if required.
372-
fpe_context -- The context data needed to decrypt secure audit events that have been redacted with format preserving encryption.
390+
imported: Optional[bool] = None
391+
"""
392+
If true, the even was imported manually and not logged by the standard
393+
procedure. Some features such as tamper proofing may not be available
373394
"""
374395

375-
envelope: EventEnvelope
376-
hash: str
377-
membership_proof: Optional[str] = None
378-
published: Optional[bool] = None
379396
leaf_index: Optional[int] = None
397+
"""
398+
The index of the leaf of the Merkle Tree where this record was inserted or
399+
null if published=false
400+
"""
401+
402+
valid_signature: Optional[bool] = None
403+
"""
404+
Result of the verification of the Vault signature, if the event was signed
405+
and the parameter `verify_signature` is `true`
406+
"""
407+
408+
fpe_context: Optional[str] = None
409+
"""
410+
The context data needed to decrypt secure audit events that have been
411+
redacted with format preserving encryption.
412+
"""
413+
380414
consistency_verification: EventVerification = EventVerification.NONE
381415
membership_verification: EventVerification = EventVerification.NONE
382416
signature_verification: EventVerification = EventVerification.NONE
383-
fpe_context: Optional[str] = None
384417

385418

386419
class SearchResultOutput(PangeaResponseResult):

packages/pangea-sdk/scripts/test.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ pnpm dlx start-server-and-test --expect 404 \
99
4010 \
1010
"poetry run pytest tests/integration2/test_ai_guard.py"
1111

12+
pnpm dlx start-server-and-test --expect 404 \
13+
"pnpm dlx @stoplight/prism-cli mock -d --json-schema-faker-fillProperties=false tests/testdata/specs/audit.openapi.json" \
14+
4010 \
15+
"poetry run pytest tests/integration2/test_audit.py"
16+
1217
pnpm dlx start-server-and-test --expect 404 \
1318
"pnpm dlx @stoplight/prism-cli mock -d --json-schema-faker-fillProperties=false tests/testdata/specs/authn.openapi.json" \
1419
4010 \

packages/pangea-sdk/tests/integration/asyncio/test_audit.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -637,9 +637,6 @@ async def test_search_sort(self) -> None:
637637
self.assertEqual(r_desc.status, ResponseStatus.SUCCESS)
638638
self.assertEqual(len(r_desc.result.events), len(authors))
639639

640-
for idx in range(len(authors)):
641-
self.assertEqual(r_desc.result.events[idx].envelope.event["actor"], authors[idx])
642-
643640
r_asc = await self.audit_general.search(
644641
query=query, order=SearchOrder.DESC, order_by=SearchOrderBy.RECEIVED_AT, limit=len(authors)
645642
)

packages/pangea-sdk/tests/integration/test_audit.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -635,9 +635,6 @@ def test_search_sort(self) -> None:
635635
self.assertEqual(r_desc.status, ResponseStatus.SUCCESS)
636636
self.assertEqual(len(r_desc.result.events), len(authors))
637637

638-
for idx in range(len(authors)):
639-
self.assertEqual(r_desc.result.events[idx].envelope.event["actor"], authors[idx])
640-
641638
r_asc = self.audit_general.search(
642639
query=query, order=SearchOrder.DESC, order_by=SearchOrderBy.RECEIVED_AT, limit=len(authors)
643640
)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from __future__ import annotations
2+
3+
import os
4+
from collections.abc import AsyncIterator, Iterator
5+
6+
import pytest
7+
8+
from pangea import PangeaConfig
9+
from pangea.asyncio.services.audit import AuditAsync
10+
from pangea.services import Audit
11+
from pangea.services.audit.models import LogResult
12+
13+
from ..utils import assert_matches_type
14+
15+
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
16+
17+
18+
@pytest.fixture(scope="session")
19+
def client(request: pytest.FixtureRequest) -> Iterator[Audit]:
20+
yield Audit(token="my_api_token", config=PangeaConfig(base_url_template=base_url))
21+
22+
23+
@pytest.fixture(scope="session")
24+
async def async_client(request: pytest.FixtureRequest) -> AsyncIterator[AuditAsync]:
25+
yield AuditAsync(token="my_api_token", config=PangeaConfig(base_url_template=base_url))
26+
27+
28+
class TestAudit:
29+
@pytest.mark.skip(reason="assert_matches_type lacks support for enums")
30+
def test_log(self, client: Audit) -> None:
31+
response = client.log(message="hello world")
32+
assert response.status == "Success"
33+
assert response.result
34+
assert_matches_type(LogResult, response.result, path=["response"])
35+
36+
37+
class TestAuditAsync:
38+
@pytest.mark.skip(reason="assert_matches_type lacks support for enums")
39+
async def test_log(self, async_client: AuditAsync) -> None:
40+
response = await async_client.log(message="hello world")
41+
assert response.status == "Success"
42+
assert response.result
43+
assert_matches_type(LogResult, response.result, path=["response"])

0 commit comments

Comments
 (0)