diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..b82d7f28 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +# Docker reads .dockerignore from the build-context root only. The service +# Dockerfiles reference .//... paths, so the context is this +# directory and per-service .dockerignore files have no effect. + +# Generic +.git/ +**/.gitignore +**/Dockerfile + +# JVM projects +**/.gradle/ +**/.idea/ +**/.kotlin/ +**/build/ + + +# Python projects +**/.venv/ +**/__pycache__/ +**/*.pyc +**/.pytest_cache/ +**/*.egg-info/ + +# Node projects +**/node_modules/ diff --git a/.github/workflows/build-vla-manager-api.yml b/.github/workflows/build-vla-manager-api.yml new file mode 100644 index 00000000..71d1e345 --- /dev/null +++ b/.github/workflows/build-vla-manager-api.yml @@ -0,0 +1,60 @@ +--- +name: Build and Test VLA Manager API + + +on: + push: + paths: + - vla-manager-api/src/** + - vla-manager-api/tests/** + - vla-manager-api/pyproject.toml + - vla-manager-api/uv.lock + - .github/workflows/build-vla-manager-api.yml + pull_request: + paths: + - vla-manager-api/src/** + - vla-manager-api/tests/** + - vla-manager-api/pyproject.toml + - vla-manager-api/uv.lock + - .github/workflows/build-vla-manager-api.yml + + +permissions: + contents: read + + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + + +defaults: + run: + working-directory: ./vla-manager-api + + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v7 + - name: Install uv + uses: astral-sh/setup-uv@v9.0.0 + with: + enable-cache: true + working-directory: ./vla-manager-api + - name: Install dependencies + run: uv sync --locked + - name: Check formatting + run: uv run --no-sync ruff format --check src tests + - name: Lint + run: uv run --no-sync ruff check src tests + - name: Run Pytest + run: uv run --no-sync pytest --junitxml=junit.xml + - name: Upload test artefacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: pytest-results + path: vla-manager-api/junit.xml diff --git a/.gitignore b/.gitignore index 180562b3..aa88ca50 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ # commitlint node_modules/ +__pycache__/ +*.pyc +.DS_Store +*.egg-info/ diff --git a/README.md b/README.md index 385c0324..0cbb052f 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,6 @@ At the moment, DVA has several subcomponents: * **API server** in [`dva-api/`](dva-api/) – gateway / entry point / connector integration * **Processing** in [`dva-processing/`](dva-processing/) – evaluates veracity requirements * **RabbitMQ** from [`rabbitmq`](https://hub.docker.com/_/rabbitmq) – message queue facilitating communication between the API server and the processing module -* **ACA-Py Controller** in [`dva-acapy-controller/`](dva-acapy-controller/) – contains SSI/VC-related logic -* **ACA-Py Agent** from [`ghcr.io/hyperledger/aries-cloudagent-python`](https://github.com/orgs/hyperledger/packages/container/package/aries-cloudagent-python) – an SSI cloud agent (~ wallet) * **PostgreSQL** from [`postgres`](https://hub.docker.com/_/postgres) – stores application state, events, and _logs_ * **Dashboard** in [`dva-dashboard/`](dva-dashboard/) – frontend application that displays database contents on a dashboard interface * **VLA Manager** in [`vla-manager`](vla-manager/) – frontend application for VLAs management (creation, display, etc) @@ -28,15 +26,13 @@ You can use the following commands to build / pull the images (from the reposito docker buildx build -t dva-api:latest -f dva-api/Dockerfile ./ docker buildx build -t dva-processing:latest -f dva-processing/Dockerfile ./ docker pull rabbitmq:4-management-alpine -docker buildx build -t dva-aca-py-controller:latest -f dva-acapy-controller/Dockerfile ./ -docker pull ghcr.io/hyperledger/aries-cloudagent-python:py3.9-0.12.6 docker pull postgres:17-alpine docker buildx build -t dva-dashboard:latest ./dva-dashboard/ docker buildx build -t vla-manager:latest ./vla-manager/ ``` > [!NOTE] -> Check the currently used RabbitMQ, PostgreSQL, and ACA-Py versions in [`test-env/common-services.yml`](test-env/common-services.yml). +> Check the currently used RabbitMQ and PostgreSQL versions in [`test-env/common-services.yml`](test-env/common-services.yml). You normally do not have to do this however as you should be using Docker Compose to set up your DVA instance (see the [_test environment_](test-env/)). @@ -88,7 +84,7 @@ Example requests to test functionality manually (non-exhaustive): | `/vla/from-templates` | `POST` | *nothing* | a [VLA request using templates](test-env/test-data/vla-request/request-from-templates.json) | `201 CREATED` and [an ID](test-env/example-outputs/id.json) | | `/vla/{id}` | `GET` | `id`: a VLA ID (eg `570b22e0-2e90-4e02-8c7b-1d6d274629f3`) | *empty* | `200 OK` and the VLA template ([example](test-env/example-outputs/vla-id-get.json)) | | `/attestation` | `POST` | *nothing* | an [AoV request](test-env/test-data/aov/timestamp-in-range/request-good.json) | `200 OK` and [an ID](test-env/example-outputs/id.json) | -| `/attestation/verify` | `POST` | *nothing* | an [AoV verification request](test-env/test-data/aov/verif-req.json) | `200 OK` and an AoV JSON object from ACA-Py | +| `/attestation/verify` | `POST` | *nothing* | an [AoV verification request](test-env/test-data/aov/verif-req.json) | `200 OK` and an AoV JSON object | > [!NOTE] > AoV requests (`POST /attestation`) are processed asynchronously. @@ -172,44 +168,6 @@ In the test output, you should see all PyTest tests passing. ``` -### DVA ACA-Py Controller Module ([`dva-acapy-controller`](dva-acapy-controller/)) - -> [!NOTE] -> A nice way to run the tests from the repository root using Docker without having to touch your local environment: -> ```console -> docker run --rm -it -v ./dva-acapy-controller:/app ghcr.io/astral-sh/uv:debian-slim uv --directory /app/ run pytest -> ``` - -#### Setup test environment - -Find a way to run [uv](https://docs.astral.sh/uv) → [installation instructions](https://docs.astral.sh/uv/getting-started/installation/) - -#### Run tests - -Set your working directory to `dva-acapy-controller/` and execute: -```console -uv run pytest -``` - -#### Expected results - -In the test output, you should see all PyTest tests passing. - -
- Example output segment (click to open) - - ``` - ============================================================ test session starts ============================================================ - platform linux -- Python 3.12.8, pytest-8.4.1, pluggy-1.6.0 - rootdir: /projects/uni/edge/data-veracity/dva-processing - configfile: pyproject.toml - plugins: anyio-4.9.0 - collected 3 items - - tests/test_controller.py .. [100%] - ``` -
- ## Component-level testing diff --git a/docs/spec/components.yaml b/docs/spec/components.yaml new file mode 100644 index 00000000..d60181de --- /dev/null +++ b/docs/spec/components.yaml @@ -0,0 +1,88 @@ +--- +# Schemas shared between more than one DVA component. Referenced from +# the per-service specs as, eg, +# `$ref: './components.yaml#/schemas/EvaluationResult'`. +# +# This file is not an OpenAPI document of its own; it is only a $ref +# target, hence the bare top-level `schemas` key. +schemas: + QualityEngine: + description: >- + Identifier of a veracity requirement evaluation engine invoked by + the processing service. + type: string + enum: [SCHEMA, GREAT_EXPECTATIONS, JQ] + examples: [SCHEMA] + + EvaluationResult: + description: >- + The result of a single veracity requirement’s evaluation, as + produced by the processing service. Relayed verbatim by the DVA + API and embedded in the AoV issued by the VC Manager. + type: object + required: [engine, timestamp, success] + properties: + engine: + description: The quality engine used to obtain this result. + $ref: '#/schemas/QualityEngine' + timestamp: + description: ISO-8601 timestamp at which the evaluation ran. + type: string + format: date-time + examples: ['2025-03-16T03:25:00Z'] + success: + description: Whether the evaluated check passed. + type: boolean + examples: [true] + details: + description: >- + Human-readable success detail; absent on failure. + type: string + error: + description: >- + Human-readable failure detail; absent on success. + type: string + + VerifiableCredential: + description: >- + A W3C Verifiable Credential in its JSON-LD form. + type: object + required: + - '@context' + - id + - type + - issuer + - validFrom + - credentialSubject + properties: + '@context': + type: array + items: + type: string + examples: + - [https://w3.org] + id: + type: string + format: uri + type: + type: array + items: + type: string + issuer: + type: object + required: [id] + properties: + id: + type: string + format: uri + validFrom: + type: string + format: date-time + credentialSubject: + type: object + required: [id] + properties: + id: + type: string + format: uri + additionalProperties: true diff --git a/docs/spec/dva-api.yaml b/docs/spec/dva-api.yaml new file mode 100644 index 00000000..8bcb4214 --- /dev/null +++ b/docs/spec/dva-api.yaml @@ -0,0 +1,485 @@ +--- +openapi: 3.1.1 + + +info: + title: Data Veracity Assurance Gateway + version: 0.1.0 + description: >- + This is the API specification of the + [data veracity assurance building block (DVA)](https://github.com/Prometheus-X-association/data-veracity). + + + Operations tagged `Future` are part of the intended contract but are + not served by the current implementation yet; see the note on each. + contact: + email: bpeter@edu.bme.hu + license: + name: Apache-2.0 + identifier: Apache-2.0 + + +# The service performs no authentication or authorisation of its own; an +# empty root-level requirement says so explicitly. +security: [] + + +servers: + - url: http://localhost:9091 + description: Test environment – Provider side + - url: http://localhost:9092 + description: Test environment – Consumer side + - url: http://localhost:9090 + description: Local development server on default port + - url: '{server}' + description: Custom + variables: + server: + default: http://localhost:9090 + description: Custom server URL + + +tags: + - name: AoV + description: Attestation generation and verification + - name: Info + description: Retrieve information about the running instance + - name: Future + description: >- + Endpoints that are not available yet but will be implemented in + the future + + +paths: + /attestation: + post: + tags: [AoV] + summary: Request an Attestation of Veracity (AoV) + description: >- + Create a new **Attestation of Veracity** for a given data + exchange. The associated VLA is obtained by ID from the VLA + Manager and its veracity requirements are evaluated by the + processing service. Only if *every* requirement passes is an + AoV verifiable credential issued by the VC Manager and returned + by this endpoint as a compact JWS. + + + The request is handled synchronously, and both successful and + failed attempts are logged; see `GET /info/requests`. + operationId: requestAov + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AttestationRequest' + examples: + simple: + summary: An exchange carrying a small JSON payload + value: + exchangeID: 5287a608-36c1-40f1-8430-5eaad60c5eca + contractID: a37532aa-5e41-4a27-a6c8-a7b4089779a8 + vlaID: 6c92b868-49d2-4bcc-a5e7-bacb0f5b858a + data: + foo: bar + baz: [qux, quux] + responses: + '200': + $ref: '#/components/responses/AoVGenerated' + '404': + $ref: '#/components/responses/VLANotFound' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '422': + $ref: '#/components/responses/MalformedRequest' + '500': + $ref: '#/components/responses/InternalError' + '502': + $ref: '#/components/responses/UpstreamError' + + + /attestation/verify: + post: + tags: [AoV] + summary: Verify an Attestation of Veracity + description: >- + Verify an **Attestation of Veracity**. The AoV is forwarded to + the VC Manager, which checks the JWS both cryptographically and + content-wise; this endpoint relays that verdict. + + + A `200 OK` only means that the verification itself ran; consult + `verified` in the body for the outcome. + operationId: verifyAov + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AttestationVerificationRequest' + examples: + simple: + summary: A compact JWS as returned by `POST /attestation` + value: + jws: >- + eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa29iQ2c4Y2tUZU1Hekg4RWZYYnVKU2RRNng3UXZnZGVMNkZqQjdDeng1V1VZV0FteSJ9..pQYZ8ViPzZbnY3RJZUE3Gp_b2GXG3oFnu1Px5r2to-sZGNDv5Cj8Qp5sJvbE_3gwec6GjNmNJZpK7ve1r7UtCw + responses: + '200': + $ref: '#/components/responses/AoVVerified' + '415': + $ref: '#/components/responses/UnsupportedMediaType' + '422': + $ref: '#/components/responses/MalformedRequest' + '500': + $ref: '#/components/responses/InternalError' + '502': + $ref: '#/components/responses/UpstreamError' + + + /info/requests: + get: + tags: [Info] + summary: List attestation requests received by this instance + description: >- + Returns a log entry for every request made to `/attestation`, + including the ones that failed. + operationId: getRequests + responses: + '200': + $ref: '#/components/responses/RequestLogs' + '500': + $ref: '#/components/responses/InternalError' + + + /info/verifications: + get: + tags: [Info, Future] + summary: List AoV verifications done by this instance + description: >- + Returns a list of all AoVs that were verified by this instance + using `/attestation/verify`. + + + **Not implemented yet.** Verification requests are not logged + at present, so this path is unrouted and answers `404` like any + other unknown path. + operationId: getVerifications + x-implementation-status: planned + responses: + '200': + $ref: '#/components/responses/VerificationLogs' + '500': + $ref: '#/components/responses/InternalError' + + + /info/credentials: + get: + tags: [Info, Future] + summary: List VCs held at this instance + description: >- + Returns a list of all verifiable credentials held locally, + as reported by the ACA-Py agent. + + + **Not implemented yet.** This path is unrouted and answers + `404` like any other unknown path. + operationId: getCredentials + x-implementation-status: planned + responses: + '200': + $ref: '#/components/responses/Credentials' + '500': + $ref: '#/components/responses/InternalError' + '502': + $ref: '#/components/responses/UpstreamError' + + +components: + responses: + AoVGenerated: + description: >- + The attestation request was processed + (the evaluation itself may still have failed, in which case no + JWS is returned). + content: + application/json: + schema: + $ref: '#/components/schemas/AttestationResponse' + AoVVerified: + description: >- + AoV successfully verified + (the verification process itself was successful but the AoV itself + might still be invalid). + content: + application/json: + schema: + $ref: '#/components/schemas/AttestationVerificationResponse' + VLANotFound: + description: The referenced VLA was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + UnsupportedMediaType: + description: >- + Body was sent with a content type other than `application/json`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + MalformedRequest: + description: >- + Body is not valid JSON, is missing required fields, or carries + fields the endpoint does not accept. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + InternalError: + description: >- + An unexpected error occurred while handling the request. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + UpstreamError: + description: >- + An upstream service (eg VLA Manager, processing, VC Manager) + was unreachable or returned an error or an unusable response. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + RequestLogs: + description: List of logged AoV generation requests. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RequestLog' + VerificationLogs: + description: List of logged AoV verification requests. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/VerificationLog' + Credentials: + description: The verifiable credentials held by this instance. + content: + application/json: + schema: + type: array + items: + $ref: './components.yaml#/schemas/VerifiableCredential' + + + schemas: + AttestationRequest: + description: >- + A request to generate an attestation for a given data exchange. + type: object + required: [exchangeID, contractID, vlaID, data] + additionalProperties: false + properties: + id: + description: >- + Client-supplied identifier. Accepted for backwards + compatibility but ignored; the log entry gets a + server-generated ID. + type: string + exchangeID: + description: Identifies the data exchange. + type: string + format: uuid + examples: [1d7e4db0-145d-4f69-8774-9e1b36a4c813] + contractID: + description: Identifies the relevant contract. + type: string + format: uuid + examples: [317b2ccb-d0b0-4499-bcac-c347e8192512] + vlaID: + description: Identifies the relevant VLA with the requirements. + type: string + format: uuid + examples: [63e2b688-c538-4b10-9276-97ee138d0c5c] + data: + description: >- + The data being passed. Any JSON value – object, array or + scalar. + examples: + - {foo: bar, baz: [qux, quux]} + AttestationResponse: + description: AoV generation response containing an AoV as a JWS. + type: object + required: [evaluationPassing, evaluationResults] + properties: + jws: + description: >- + An AoV VC as a compact JWS string. Absent when the + evaluation did not pass, as no credential is issued in that + case. + type: string + evaluationPassing: + description: >- + Whether every veracity requirement’s evaluation passed. + type: boolean + examples: [true] + evaluationResults: + description: >- + Per-requirement evaluation results. Never empty; an empty + result set from the processing service is reported as a + `502` instead. + type: array + minItems: 1 + items: + $ref: './components.yaml#/schemas/EvaluationResult' + AttestationVerificationRequest: + description: Requests the verification of an AoV. + type: object + required: [jws] + additionalProperties: false + properties: + jws: + description: The AoV as a compact JWS VC to verify. + type: string + AttestationVerificationResponse: + description: The results of verifying an AoV. + type: object + required: [verified] + properties: + verified: + description: Whether the AoV was deemed valid in all aspects. + type: boolean + examples: [true] + reason: + description: >- + Failure reason when `verified` is false; absent otherwise. + type: string + examples: [invalid signature] + RequestLog: + description: >- + An object representing a logged attestation generation request. + type: object + required: + - id + - type + - exchangeID + - contractID + - vlaID + - data + - evaluationPassing + - evaluationResults + - receivedDate + properties: + id: + description: Server-generated unique identifier for this log entry. + type: string + format: uuid + type: + description: The kind of request that was logged. + $ref: '#/components/schemas/RequestType' + exchangeID: + description: >- + Identifies the data exchange for which this attestation + was attempted. + type: string + format: uuid + contractID: + description: Identifies the contract the exchange was made under. + type: string + format: uuid + vlaID: + description: >- + Identifies the VLA that was used to generate an attestation + against. + type: string + format: uuid + data: + description: >- + The piece of data that was attested to. Any JSON value – + object, array or scalar. + evaluationPassing: + description: Whether all VLA requirements passed. + type: boolean + evaluationResults: + description: >- + Per-requirement evaluation results. Empty if the request + failed before the processing service was reached. + type: array + items: + $ref: './components.yaml#/schemas/EvaluationResult' + receivedDate: + description: ISO-8601 timestamp at which the request was received. + type: string + format: date-time + examples: ['2025-03-16T03:25:00Z'] + vcID: + description: >- + Identifies the issued credential; absent if none was issued. + type: string + format: uuid + error: + description: >- + What went wrong while handling the request; absent if it + succeeded. + $ref: '#/components/schemas/RequestLogError' + VerificationLog: + description: >- + An object representing a logged attestation verification. + + + **Not specified yet**, pending the logging of verification + requests; see `GET /info/verifications`. + type: object + x-implementation-status: planned + RequestType: + description: Describes a type of veracity attestation request. + type: string + enum: [ATTESTATION_REQUEST, PROOF_REQUEST] + RequestLogError: + description: >- + The error that terminated a logged request. + type: object + required: [title] + properties: + title: + description: Short summary of the problem type. + type: string + examples: [Upstream service error] + detail: + description: >- + Human-readable explanation specific to this occurrence. + type: string + Error: + description: >- + RFC 9457-like error object. Note that it is served as + `application/json` rather than `application/problem+json`. + type: object + required: [type, title] + properties: + type: + description: >- + A URI reference identifying the problem type. One of + `/errors/exists`, `/errors/not_found`, `/errors/bad_request`, + `/errors/unsupported_media_type`, `/errors/bad_gateway`, + `/errors/unimplemented` or `/errors/unknown`. + type: string + examples: ['/errors/not_found'] + title: + description: Short summary of the problem type. + type: string + examples: [Upstream service error] + detail: + description: >- + Human-readable explanation specific to this occurrence. + type: string + examples: [vlaId must be a valid UUID] + instance: + description: >- + URI reference identifying the specific occurrence of the + problem. The request path, when known. + type: string + examples: ['/attestation'] diff --git a/docs/spec/dva-processing.yaml b/docs/spec/dva-processing.yaml new file mode 100644 index 00000000..57acd6cf --- /dev/null +++ b/docs/spec/dva-processing.yaml @@ -0,0 +1,374 @@ +--- +openapi: 3.1.0 + + +info: + title: DVA Processing + version: 0.1.0 + description: >- + Stateless veracity-check engine. + Evaluates data-quality requirements — expressed as ODCS `DataQuality` + entries, the same form they take inside a + **Veracity Level Agreement (VLA)** — against supplied data, + and returns one `EvaluationResult` per requirement. + contact: + email: bpeter@edu.bme.hu + + +servers: + - url: http://localhost:5007 + description: Provider + - url: http://localhost:5008 + description: Consumer +tags: + - name: Evaluation + description: Endpoints that evaluate veracity requirements against data. + + +paths: + /evaluate: + post: + tags: [Evaluation] + summary: Evaluate a single requirement against data + description: >- + Runs one requirement's implementation on the supplied data with the + engine it names. Useful for trying a requirement out while building + a VLA. + operationId: evaluate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluationRequest' + examples: + jqNonEmptyActorName: + summary: jq requirement checking that the actor name is non-empty + value: + requirement: + engine: JQ + implementation: >- + { success: (.actor.name | length > 0), + details: "actor name non-empty" } + data: + actor: + name: Jean Dupont + verb: + id: http://adlnet.gov/expapi/verbs/interacted + responses: + '200': + description: The requirement was evaluated. + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluationResult' + '400': + description: >- + The requirement names an engine this service does not implement. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: Malformed request body or missing required fields. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + '500': + description: >- + The engine failed while evaluating. The body is still an + `EvaluationResult`, with `success` false and `error` describing + the failure. + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluationResult' + + + /evaluate-batch: + post: + tags: [Evaluation] + summary: Evaluate every requirement in a VLA against data + description: >- + Walks the VLA's `schema[].quality` arrays and evaluates each + requirement in turn, returning one result per requirement in the + order they were found. Called by the DVA API during the + synchronous attestation flow. + operationId: evaluateBatch + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluateBatchRequest' + examples: + xapiStatementVla: + summary: VLA carrying two jq requirements on one schema object + value: + vla: + apiVersion: v3.0.2 + kind: DataContract + version: 0.1.0 + status: active + id: 570b22e0-2e90-4e02-8c7b-1d6d274629f3 + schema: + - name: xapiStatement + quality: + - engine: JQ + implementation: >- + { success: (.actor.name | length > 0), + details: "actor name non-empty" } + - engine: JQ + implementation: >- + { success: (.verb.id | length > 0), + details: "verb id non-empty" } + data: + actor: + name: Jean Dupont + verb: + id: http://adlnet.gov/expapi/verbs/interacted + responses: + '200': + description: >- + One result per requirement, in the order the requirements appear + in the VLA. A requirement the engine could not run is reported + as a result with `success` false and `error` set, so this stays a + `200`; a VLA declaring no requirements yields an empty array. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/EvaluationResult' + '400': + description: >- + A requirement names an engine this service does not implement. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: >- + Malformed request body, or a `vla` that is not a valid ODCS + document. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + + + /evaluate/from-template: + post: + tags: [Evaluation] + summary: Render a VLA template and evaluate the result against data + description: >- + Fetches the template named by `templateID` from the VLA Manager, + renders its implementation template with `templateModel`, and + evaluates the requirement that comes out against `data`. Useful for + trying a template out while building a VLA. + operationId: evaluateFromTemplate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluationFromTemplateRequest' + examples: + actorNameTemplate: + summary: Render an actor-name check and evaluate conforming data + value: + templateID: a5dee716-2129-4588-a1a2-04a4c2923a79 + templateModel: + field: actor.name + data: + actor: + name: Jean Dupont + verb: + id: http://adlnet.gov/expapi/verbs/interacted + responses: + '200': + description: The rendered requirement was evaluated. + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluationResult' + '400': + description: >- + The template could not be rendered with the given model, or it + names an engine this service does not implement. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: The VLA Manager has no template with the given ID. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: Malformed request body or missing required fields. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + '500': + description: >- + The engine failed while evaluating. The body is still an + `EvaluationResult`, with `success` false and `error` describing + the failure. + content: + application/json: + schema: + $ref: '#/components/schemas/EvaluationResult' + '502': + description: >- + The VLA Manager could not be reached, or answered with something + unusable as a template. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + +components: + schemas: + EvaluationRequest: + type: object + description: Body of `POST /evaluate`. + required: [requirement, data] + additionalProperties: false + properties: + requirement: + $ref: '#/components/schemas/DataQuality' + data: + description: The data document to evaluate the requirement against. + + EvaluateBatchRequest: + type: object + description: Body of `POST /evaluate-batch`. + required: [vla, data] + additionalProperties: false + properties: + vla: + $ref: '#/components/schemas/VLA' + data: + description: The data document to evaluate every requirement against. + + EvaluationFromTemplateRequest: + type: object + description: Body of `POST /evaluate/from-template`. + required: [templateID, templateModel, data] + additionalProperties: false + properties: + templateID: + type: string + format: uuid + description: ID of the VLA template to fetch and render. + examples: [a5dee716-2129-4588-a1a2-04a4c2923a79] + templateModel: + type: object + description: >- + The model to render the template with, per its `variableSchema`: + keys are the template's placeholders, values their + substitutions. + additionalProperties: true + data: + description: >- + The data document to evaluate the rendered requirement against. + + VLA: + type: object + description: >- + A Veracity Level Agreement: an + [ODCS](https://bitol-io.github.io/open-data-contract-standard/) + v3 data contract. Its requirements are the `DataQuality` entries in + the `quality` array of each object in its `schema` array. Validated + as ODCS, so a document carrying fields the standard does not define + is rejected with a `422`. + + DataQuality: + type: object + description: >- + A single veracity requirement, as an + [ODCS](https://bitol-io.github.io/open-data-contract-standard/) + `DataQuality` entry — the same object that appears in a VLA's + `schema[].quality` array. Every ODCS field is accepted; the two + below are the ones this service reads. + properties: + engine: + description: >- + The engine to evaluate `implementation` with. ODCS leaves this + a free-form string, so a value outside `QualityEngine` is only + rejected once evaluation reaches for it — with a `400`. + $ref: '#/components/schemas/QualityEngine' + implementation: + type: string + description: >- + The engine-specific check. For `JQ` this is a jq expression + whose every output must be an object + `{ success: boolean, details: string }`; for `SCHEMA` a JSON + Schema document; for `GREAT_EXPECTATIONS` a YAML expectation + definition. + + QualityEngine: + $ref: './components.yaml#/schemas/QualityEngine' + + EvaluationResult: + $ref: './components.yaml#/schemas/EvaluationResult' + + Error: + type: object + description: >- + RFC 9457-style problem object emitted on every error the service + raises itself. + required: [type, title] + additionalProperties: false + properties: + type: + type: string + description: >- + Machine-readable problem type, normally the HTTP status + constant (e.g. `NOT_FOUND`). + examples: [UNKNOWN_ENGINE] + title: + type: string + description: Short human-readable summary of the problem. + examples: [Unknown quality engine] + detail: + type: string + description: >- + Longer human-readable explanation specific to this occurrence. + + ValidationError: + type: object + description: >- + Request-body validation failures, reported by the framework rather + than by the service, and so shaped differently from `Error`. + required: [detail] + additionalProperties: false + properties: + detail: + type: array + items: + type: object + required: [type, loc, msg] + properties: + type: + type: string + description: Validation failure kind, e.g. `missing`. + examples: [missing] + loc: + type: array + description: Path to the offending field. + items: + oneOf: + - type: string + - type: integer + examples: [[body, requirement]] + msg: + type: string + examples: [Field required] + input: + description: The value that failed validation. diff --git a/docs/spec/dva-vc-manager.yaml b/docs/spec/dva-vc-manager.yaml new file mode 100644 index 00000000..66bf675e --- /dev/null +++ b/docs/spec/dva-vc-manager.yaml @@ -0,0 +1,426 @@ +--- +openapi: 3.1.0 + + +info: + title: DVA VC Manager + version: 0.1.0 + description: >- + Issues and verifies Attestations of Veracity (AoV) + as W3C VC 2.0 JSON Web Signatures (Ed25519). + Also maintains a list of trusted DIDs – AoV verification rejects + untrusted (non-whitelisted) issuers. + contact: + email: bpeter@edu.bme.hu + + +servers: + - url: http://localhost:8001 + description: Provider + - url: http://localhost:8002 + description: Consumer + + +paths: + /aov/issue: + post: + summary: Issue an AoV JWS credential + description: >- + Signs the supplied AoV payload as a W3C VC 2.0 JSON-LD JWS using the + service's persisted Ed25519 private key. The returned `jws` is a + compact `header.payload.signature` string. A UUID for the credential + is generated server-side and embedded in the JWS payload. + operationId: issueAov + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AovIssueRequest' + examples: + NettingSettlement: + summary: Issue an AoV for a netting settlement evaluation + value: + validSince: '2026-07-15T10:00:00Z' + subject: did:web:provider.example.com + issuerId: did:web:intermediary.example.com + recordId: rec-0001 + contractId: contract-0001 + dataExchangeId: xchg-0001 + payload: '{"netAmount":-100,"bankID":"BANK_A"}' + evaluationResults: + - engine: JQ + timestamp: '2026-07-15T10:00:01Z' + success: true + details: zero-sum satisfied + responses: + '200': + description: The signed JWS. + content: + application/json: + schema: + $ref: '#/components/schemas/AovIssueResponse' + '422': + description: Malformed request body or missing required fields. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /aov/verify: + post: + summary: Verify an AoV JWS credential + description: >- + Verifies the Ed25519 signature of a compact AoV JWS. The issuer + DID is extracted from the JWS payload and looked up in the + local whitelist. + operationId: verifyAov + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AovVerifyRequest' + examples: + VerifyJws: + summary: Verify a previously issued AoV JWS + value: + jws: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + responses: + '200': + description: Verification outcome. + content: + application/json: + schema: + $ref: '#/components/schemas/AovVerifyResponse' + examples: + Verified: + summary: Signature valid and issuer whitelisted + value: + verified: true + Rejected: + summary: Issuer not in whitelist + value: + verified: false + reason: issuer not whitelisted + '422': + description: Malformed JWS + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /admin/whitelist: + get: + summary: List all whitelisted DIDs + description: Returns the current DID whitelist as an array. + operationId: listWhitelist + responses: + '200': + description: Array of whitelist entries. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WhitelistEntry' + post: + summary: Add a DID to the whitelist + description: | + Registers a new trusted attester `did:key`. Returns `201` with the + created entry on success. Duplicate `did_key` values return `400`. + operationId: addWhitelistEntry + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WhitelistAddRequest' + examples: + AddIssuer: + summary: Whitelist an issuer did:key + value: + did_key: did:key:z6MktRz8iVwNh1rLKV47C2i2nMe4zwGt7SgLBjS9zw1jNuQY + label: known-provider-1 + responses: + '201': + description: Entry created. + content: + application/json: + schema: + $ref: '#/components/schemas/WhitelistEntry' + '409': + description: Duplicate entry. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: Invalid DID + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /admin/whitelist/{did_key}: + delete: + summary: Remove a DID from the whitelist + description: >- + Deletes the whitelist entry whose `did_key` matches the URL-encoded + path parameter. Returns `204` on success, `404` if not found. + + + The `{did_key}` path parameter is the percent-encoded `did:key:...` + identifier — colon (`:`) must be encoded as `%3A` per RFC 3986. + operationId: removeWhitelistEntry + parameters: + - name: did_key + in: path + required: true + description: >- + URL-encoded did:key identifier to remove + (e.g. `did%3Akey%3Az6Mk...`). + schema: + type: string + example: did%3Akey%3Az6Mku8XYifPt5tfL93VpJhFWuoyQDt6bTRqfWestrpo6YM5d + responses: + '204': + description: Entry removed. + '404': + description: did:key not in whitelist. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /admin/keys: + get: + summary: Get this issuer's own DID + description: >- + Returns the DID derived from the service's persisted Ed25519 + signing key. + operationId: getOwnKey + responses: + '200': + description: Issuer's own DID and persisted key location. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnKey' + /admin/credentials: + get: + summary: List locally issued credentials + description: >- + Returns the append-only PostgreSQL audit records for credentials issued + by this VC manager, newest first. Each entry includes the full compact + JWS and its original issuance request. + operationId: listCredentials + responses: + '200': + description: Issued credential audit records. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CredentialAudit' + /admin/verifications: + get: + summary: List credential verification attempts + description: >- + Returns the append-only PostgreSQL audit records for verification + requests and their returned responses, newest first. + operationId: listVerifications + responses: + '200': + description: Verification audit records. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/VerificationAudit' + + +components: + schemas: + QualityEngine: + $ref: './components.yaml#/schemas/QualityEngine' + # Embedded in `AovIssueRequest.evaluationResults`, exactly as the DVA API + # relays it from the processing service. + EvaluationResult: + $ref: './components.yaml#/schemas/EvaluationResult' + AovIssueRequest: + type: object + description: Body of POST /aov/issue. + additionalProperties: false + required: + - validSince + - subject + - issuerId + - recordId + - contractId + - dataExchangeId + - payload + - evaluationResults + properties: + validSince: + type: string + format: date-time + description: Earliest moment from which the AoV is considered valid. + subject: + type: string + description: The credential subject's identifier (e.g. a did:web). + issuerId: + type: string + description: >- + The logical issuer identifier (separate from the did:key). + recordId: + type: string + description: External record identifier being attested. + contractId: + type: string + description: The data-contract identifier this AoV covers. + dataExchangeId: + type: string + description: The data-exchange identifier this AoV covers. + payload: + type: string + description: >- + The opaque payload (stringified JSON) to sign as the JWS payload. + evaluationResults: + type: array + items: + $ref: '#/components/schemas/EvaluationResult' + description: The VLA evaluation results carried inside the AoV. + AovIssueResponse: + type: object + description: Response of POST /aov/issue. + additionalProperties: false + required: [jws] + properties: + jws: + type: string + description: Compact JWS header.payload.signature (Ed25519). + AovVerifyRequest: + type: object + description: Body of POST /aov/verify. + additionalProperties: false + required: [jws] + properties: + jws: + type: string + description: Compact JWS to verify (`header.payload.signature`). + AovVerifyResponse: + type: object + description: Response of POST /aov/verify. + additionalProperties: false + required: [verified] + properties: + verified: + type: boolean + description: >- + Whether the JWS is well-formed, correctly signed, and the issuer + is whitelisted. + reason: + type: string + nullable: true + description: >- + Present when verified is false. Explains why verification failed. + WhitelistEntry: + type: object + description: A single whitelist entry. + additionalProperties: false + required: [id, did_key] + properties: + id: + type: string + format: uuid + description: Internal UUID assigned to this whitelist entry. + did_key: + type: string + description: >- + The attester's `did:key` identifier (e.g. `did:key:z6Mk...`). + label: + type: string + nullable: true + description: Optional human-readable label for the attester. + WhitelistAddRequest: + type: object + description: Body of `POST /admin/whitelist`. + additionalProperties: false + required: [did_key] + properties: + did_key: + type: string + description: The did:key identifier to whitelist. + label: + type: string + nullable: true + description: Optional human-readable label. + OwnKey: + type: object + description: >- + This issuer's own did:key plus the persisted private-key location. + additionalProperties: false + required: [issuer_did_key, key_path] + properties: + issuer_did_key: + type: string + description: >- + The did:key identifier derived from this service's Ed25519 + public key. + key_path: + type: string + description: >- + Filesystem path where the signing key is persisted. + CredentialAudit: + type: object + description: An issued credential retained in the local PostgreSQL audit log. + required: [id, credential_id, jws, request, created_at] + properties: + id: + type: string + format: uuid + credential_id: + type: string + description: UUID embedded in the credential subject. + jws: + type: string + description: Full compact JWS credential. + request: + type: object + description: Original POST /aov/issue request body. + created_at: + type: string + format: date-time + VerificationAudit: + type: object + description: A verification request and the response returned by this service. + required: [id, request, response, created_at] + properties: + id: + type: string + format: uuid + request: + type: object + description: Original POST /aov/verify request body. + response: + type: object + description: Response body with its HTTP status code. + created_at: + type: string + format: date-time + Error: + type: object + description: RFC 9457-style problem object emitted on all error responses. + additionalProperties: false + required: [type, title] + properties: + type: + type: string + description: A URI reference identifying the problem type. + title: + type: string + description: Short human-readable summary of the problem. + detail: + type: string + nullable: true + description: >- + Longer human-readable explanation specific to this occurrence. diff --git a/docs/spec/openapi.yaml b/docs/spec/openapi.yaml index d66bd4c9..5343a197 100644 --- a/docs/spec/openapi.yaml +++ b/docs/spec/openapi.yaml @@ -486,7 +486,6 @@ paths: - engine: JQ implementation: >- { success: .actor.name | contains('Dupont') } - attesterID: attester-0000 data: actor: name: Jean Dupont @@ -780,11 +779,10 @@ components: type: string enum: [SYNTAX, TIMELINESS, ACCURACY, COMPLETENESS, CONSISTENCY] QualityEngine: - type: string - enum: [SCHEMA, GREAT_EXPECTATIONS, JQ] + $ref: './components.yaml#/schemas/QualityEngine' VeracityRequest: type: object - required: [exchangeID, contract, data] + required: [exchangeID, contract, data, vlaID] properties: exchangeID: type: string @@ -800,6 +798,9 @@ components: $ref: https://raw.githubusercontent.com/bitol-io/open-data-contract-standard/refs/heads/main/schema/odcs-json-schema-latest.json data: $ref: '#/components/schemas/AnyValue' + vlaID: + type: string + example: 14bd2062-0ffc-4b83-830b-aaa9aa1a1ca3 example: exchangeID: xchg-0001 contract: @@ -866,14 +867,9 @@ components: result: success: true timestamp: '2025-03-16T03:25:00Z' - attesterID: attester-0000 AttestationRequest: allOf: - $ref: '#/components/schemas/VeracityRequest' - required: [attesterID] - properties: - attesterID: - type: string ProofRequest: allOf: - $ref: '#/components/schemas/VeracityRequest' @@ -948,26 +944,7 @@ components: action: read object: course_materials/lecturenotes/1 EvaluationResult: - type: object - required: [timestamp, success] - additionalProperties: false - properties: - engine: - type: QualityEngine - timestamp: - type: string - format: datetime - success: - type: bool - details: - type: string - error: - type: string - example: - engine: JQ - timestamp: '2026-01-31T17:48:10.904264Z' - success: true - details: Actor name is correct + $ref: './components.yaml#/schemas/EvaluationResult' Error: type: object required: [message] diff --git a/docs/spec/vla-manager-api.yaml b/docs/spec/vla-manager-api.yaml new file mode 100644 index 00000000..8f86777f --- /dev/null +++ b/docs/spec/vla-manager-api.yaml @@ -0,0 +1,639 @@ +--- +openapi: 3.1.0 + + +info: + title: VLA Manager API + version: 0.1.0 + description: >- + The VLA Manager is responsible for services regarding + **Veracity Level Agreements (VLAs)** and the **templates** used in their + construction. + contact: + email: bpeter@edu.bme.hu + + +servers: + - url: http://localhost:9099 + description: Data Intermediary. +tags: + - name: VLA + description: Endpoints related to Veracity Level Agreements (VLAs). + - name: Templates + description: Endpoints related to the management of VLA templates. + - name: Dev + description: Development-only bulk deletion endpoints. + + +paths: + /template: + get: + tags: [Templates] + summary: List all templates + operationId: listTemplates + responses: + '200': + description: The list of available VLA Templates. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Template' + post: + tags: [Templates] + summary: Create a template + description: >- + Create a new VLA Template. + An `id` is generated automatically. + operationId: createTemplate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TemplateCreate' + examples: + TestTemplate: + summary: Test template + value: + name: TestTemplate + description: VLA Template used for testing + criterionType: VALID_INVALID + targetAspect: SYNTAX + evaluationMethod: + engine: JQ + variableSchema: + properties: + date: + type: string + implementationTemplate: >- + { + success: .date == "{{ date }}", + details: "date matches" + } + responses: + '201': + description: Template created. + content: + application/json: + schema: + $ref: '#/components/schemas/Id' + '422': + description: Invalid request body. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + delete: + tags: [Dev] + summary: Delete all templates + description: Bulk-deletes every VLA Template. + operationId: deleteTemplates + responses: + '204': + description: All templates deleted. + /template/{id}: + parameters: + - $ref: '#/components/parameters/idParam' + get: + tags: [Templates] + summary: Get a template by ID + operationId: getTemplate + responses: + '200': + description: The requested template. + content: + application/json: + schema: + $ref: '#/components/schemas/Template' + '404': + description: No template with the given ID exists. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + patch: + tags: [Templates] + summary: Partially update a template + description: >- + Applies a partial update to the VLA Template identified by the + path `id`. + The request body **must** contain an `id` field that matches the path + parameter, otherwise the service responds with `400 Bad Request`. + operationId: updateTemplate + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TemplatePatch' + responses: + '200': + description: Template updated; returns the updated template. + content: + application/json: + schema: + $ref: '#/components/schemas/Template' + '400': + description: Path `id` does not match body `id`. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: No template with the given ID exists. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: Invalid request body. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + delete: + tags: [Templates] + summary: Delete a template + operationId: deleteTemplate + responses: + '204': + description: Template deleted. + '404': + description: No template with the given ID exists. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /template/{id}/render: + parameters: + - $ref: '#/components/parameters/idParam' + post: + tags: [Templates] + summary: Render a template + description: >- + Renders the template’s `implementationTemplate` (a Handlebars + template, e.g. `{{ date }}`) string with the supplied model. + Returns the engine and the rendered implementation string in a + single JSON object. + operationId: renderTemplate + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: true + description: >- + The template model – any JSON object whose keys match the + variables declared in the template’s `variableSchema`. + examples: + DateModel: + summary: Date model + value: + date: 20250101T000000Z + responses: + '200': + description: Template rendered successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/RenderResult' + example: + engine: JQ + implementation: >- + { + success: .date == "20250101T000000Z", + details: "date matches" + } + '400': + description: Error while rendering template. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: No template with the given ID exists. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: Request body is not a JSON object. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + /vla: + get: + tags: [VLA] + summary: List all VLAs + operationId: listVLAs + responses: + '200': + description: The list of known VLAs. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/VLA' + post: + tags: [VLA] + summary: Create a VLA + description: >- + Create a new Veracity Level Agreement. + An ID is generated automatically and injected into the stored document. + operationId: createVLA + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VLACreate' + examples: + XapiLearningTrace: + summary: E2E VLA – xAPI learning trace + value: + description: E2E VLA – xAPI learning trace. + schema: + name: xapi_statement + logicalType: object + properties: + - name: actor + logicalType: object + required: true + - name: verb + logicalType: object + required: true + quality: + - engine: JQ + implementation: >- + { + success: (.actor.name | length > 0), + details: "actor name non-empty" + } + responses: + '201': + description: VLA successfully created. + content: + application/json: + schema: + $ref: '#/components/schemas/Id' + '422': + description: Invalid request body. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' + delete: + tags: [Dev] + summary: Delete all VLAs + description: >- + Bulk-deletes every VLA. + Intended for development/test environments only. + operationId: deleteVLAs + responses: + '204': + description: All VLAs deleted. + /vla/{id}: + parameters: + - $ref: '#/components/parameters/idParam' + get: + tags: [VLA] + summary: Get a VLA by ID + operationId: getVLA + responses: + '200': + description: The requested VLA. + content: + application/json: + schema: + $ref: '#/components/schemas/VLA' + '404': + description: No VLA with the given ID exists. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /vla/from-templates: + post: + tags: [VLA] + summary: Create a VLA from templates + description: >- + Creates a VLA by rendering a list of VLA templates with supplied + models. + Each entry in `qualityTemplates` is rendered and the resulting quality + requirements are merged into the VLA’s `quality` array before + persistence. + operationId: createVLAFromTemplates + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/VLACreateFromTemplates' + responses: + '201': + description: VLA created. Returns the new VLA id. + content: + application/json: + schema: + $ref: '#/components/schemas/Id' + '400': + description: Failed to render one of the templates. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '404': + description: One of the referenced template IDs was not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '422': + description: Invalid request body. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationError' +components: + parameters: + idParam: + name: id + in: path + required: true + description: UUID identifying the resource. + schema: + type: string + format: uuid + schemas: + Id: + type: object + required: [id] + additionalProperties: false + properties: + id: + type: string + format: uuid + description: UUID of the newly created resource. + VLA: + type: object + additionalProperties: true + required: [id] + description: >- + A Veracity Level Agreement document following the Open Data + Contract Standard (ODCS). + The service generates `id` and injects it into the stored document + on read, alongside the `apiVersion`, `kind`, `version` and `status` + headers it wraps every VLA with. + properties: + id: + type: string + format: uuid + readOnly: true + description: UUID assigned by the service. + apiVersion: + type: string + examples: [v3.0.2] + kind: + type: string + examples: [DataContract] + version: + type: string + status: + type: string + VLACreate: + type: object + additionalProperties: true + description: >- + Payload used to create a new VLA. + All fields are optional; any subset of an ODCS data contract may be + supplied. + An `id` is generated by the service and must not be set by the client. + properties: + description: + type: string + servers: + type: array + schema: + type: object + description: An ODCS schema block (object or list of properties). + quality: + type: array + items: + $ref: '#/components/schemas/DataQuality' + price: + type: object + team: + type: array + roles: + type: array + slaProperties: + type: array + support: + type: array + tags: + type: array + VLACreateFromTemplates: + type: object + description: >- + Payload used to create a new VLA given a list of templates. + Extends `VLACreate` with a `qualityTemplates` array. + Each entry is rendered and the result is merged into the + VLA’s `quality` array. + allOf: + - $ref: '#/components/schemas/VLACreate' + required: [qualityTemplates] + properties: + qualityTemplates: + type: array + items: + type: object + required: [id, model] + properties: + id: + type: string + format: uuid + description: UUID of the VLA template to render. + model: + type: object + description: Key-value pairs to substitute into the template. + additionalProperties: true + example: + id: 3c58c2fd-6d7a-4953-9f76-7c71fc3ac7e2 + model: + value: ok + DataQuality: + type: object + description: A single data quality requirement. + required: [engine, implementation] + additionalProperties: false + properties: + engine: + $ref: '#/components/schemas/QualityEngine' + implementation: + type: string + description: The veracity-check implementation (e.g. a jq expression). + Template: + type: object + required: + - id + - name + - criterionType + - targetAspect + - evaluationMethod + additionalProperties: false + properties: + id: + type: string + format: uuid + readOnly: true + name: + type: string + description: + type: string + criterionType: + $ref: '#/components/schemas/CriterionType' + targetAspect: + $ref: '#/components/schemas/QualityAspect' + evaluationMethod: + $ref: '#/components/schemas/EvaluationMethod' + EvaluationMethod: + type: object + required: [engine, variableSchema, implementationTemplate] + additionalProperties: false + properties: + engine: + $ref: '#/components/schemas/QualityEngine' + variableSchema: + type: object + description: >- + JSON schema describing the variables the template expects + when rendered. + implementationTemplate: + type: string + description: >- + A Handlebars template (e.g. `{{ date }}`) that is rendered + with the model supplied to `POST /template/{id}/render`. + TemplateCreate: + type: object + required: [name, criterionType, targetAspect, evaluationMethod] + additionalProperties: false + description: >- + Payload used to create a new VLA template. + An `id` is generated by the service + and must not be supplied by the client. + properties: + name: + type: string + description: + type: string + criterionType: + $ref: '#/components/schemas/CriterionType' + targetAspect: + $ref: '#/components/schemas/QualityAspect' + evaluationMethod: + $ref: '#/components/schemas/EvaluationMethod' + example: + name: TestTemplate + description: VLA Template used for testing + criterionType: VALID_INVALID + targetAspect: SYNTAX + evaluationMethod: + engine: JQ + variableSchema: + properties: + date: + type: string + implementationTemplate: '{ success: .date == "{{ date }}", details: "date + matches" }' + TemplatePatch: + type: object + required: [id] + additionalProperties: false + description: >- + Payload to partially update an existing VLA template. + The `id` field is **required** and must match the `{id}` path parameter; + otherwise the service responds with `400 Bad Request`. + All other fields are optional – only the supplied fields are updated. + properties: + id: + type: string + format: uuid + name: + type: string + description: + type: string + criterionType: + $ref: '#/components/schemas/CriterionType' + targetAspect: + $ref: '#/components/schemas/QualityAspect' + evaluationMethod: + $ref: '#/components/schemas/EvaluationMethod' + RenderResult: + type: object + required: [engine, implementation] + additionalProperties: false + properties: + engine: + $ref: '#/components/schemas/QualityEngine' + implementation: + type: string + description: The rendered data-quality fragment. + QualityEngine: + $ref: './components.yaml#/schemas/QualityEngine' + CriterionType: + type: string + enum: [VALID_INVALID, IN_RANGE, GREATER_THAN, LESS_THAN] + QualityAspect: + type: string + enum: [SYNTAX, TIMELINESS, ACCURACY, COMPLETENESS, CONSISTENCY] + Error: + type: object + description: >- + Problem object emitted on every error the service raises itself. + required: [type, title] + additionalProperties: false + properties: + type: + type: string + description: >- + Machine-readable problem type, normally the HTTP status + constant (e.g. `NOT_FOUND`). + title: + type: string + description: Short human-readable summary of the problem. + ValidationError: + type: object + description: >- + Request-body validation failures, reported by the framework rather + than by the service, and so shaped differently from `Error`. + required: [detail] + additionalProperties: false + properties: + detail: + type: array + items: + type: object + required: [type, loc, msg] + properties: + type: + type: string + description: Validation failure kind, e.g. `missing`. + examples: [missing] + loc: + type: array + description: Path to the offending field. + items: + oneOf: + - type: string + - type: integer + examples: [[body, criterionType]] + msg: + type: string + examples: [Field required] + input: + description: The value that failed validation. diff --git a/dva-acapy-controller/.dockerignore b/dva-acapy-controller/.dockerignore deleted file mode 100644 index 21d0b898..00000000 --- a/dva-acapy-controller/.dockerignore +++ /dev/null @@ -1 +0,0 @@ -.venv/ diff --git a/dva-acapy-controller/.gitignore b/dva-acapy-controller/.gitignore deleted file mode 100644 index 012eb04b..00000000 --- a/dva-acapy-controller/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -__pycache__/ -log.json diff --git a/dva-acapy-controller/.python-version b/dva-acapy-controller/.python-version deleted file mode 100644 index e4fba218..00000000 --- a/dva-acapy-controller/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/dva-acapy-controller/Dockerfile b/dva-acapy-controller/Dockerfile deleted file mode 100644 index 18eaad78..00000000 --- a/dva-acapy-controller/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM python:3.12-slim AS build - -# Install uv -COPY --from=ghcr.io/astral-sh/uv:0.5 /uv /uvx /bin/ -# Install netcat for healthcheck -RUN apt-get update && apt-get install -y netcat-openbsd - -# Set working directory -WORKDIR /app/ - -# Install dependencies -RUN \ - --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=./dva-acapy-controller/uv.lock,target=uv.lock \ - --mount=type=bind,source=./dva-acapy-controller/pyproject.toml,target=pyproject.toml \ - uv sync \ - --frozen \ - --no-install-project \ - --compile-bytecode \ - --no-editable - -# Copy app files -COPY ./dva-acapy-controller/ /app/ - -# Sync project -RUN \ - --mount=type=cache,target=/root/.cache/uv \ - uv sync \ - --frozen \ - --compile-bytecode \ - --no-editable - -# Run app -CMD ["uv", "run", "uvicorn", "dva_acapy_controller.controller:app", "--host", "0.0.0.0", "--port", "8050", "--log-level", "debug"] diff --git a/dva-acapy-controller/LICENSE b/dva-acapy-controller/LICENSE deleted file mode 100644 index d6456956..00000000 --- a/dva-acapy-controller/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/dva-acapy-controller/README.md b/dva-acapy-controller/README.md deleted file mode 100644 index 2aeda95e..00000000 --- a/dva-acapy-controller/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# ACA-Py Controller for DVA - -Implements SSI/VC-related business logic as an [ACA-Py controller](https://aca-py.org/latest/gettingStarted/ACA-PyAgentArchitecture/). diff --git a/dva-acapy-controller/pyproject.toml b/dva-acapy-controller/pyproject.toml deleted file mode 100644 index 643cb8ee..00000000 --- a/dva-acapy-controller/pyproject.toml +++ /dev/null @@ -1,32 +0,0 @@ -[project] -name = "dva-acapy-controller" -version = "0.1.0" -description = "Data Veracity Assurance ACA-Py Integration" -readme = "README.md" -requires-python = ">=3.10" -authors = [ - { name = "FTSRG", email = "bpeter@edu.bme.hu" }, - { name = "Rajmund Szilveszter Hubai", email = "hubai.rajmund@edu.bme.hu" }, -] -license = "Apache-2.0" -dependencies = [ - "fastapi~=0.136.0", - "httpx~=0.28.1", - "psycopg-pool>=3.2.8", - "psycopg[binary]>=3.2.13", - "requests~=2.33.0", - "uvicorn~=0.34.3", -] - -[project.scripts] -dva-acapy-controller = "dva_acapy_controller.main:main" - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/dva_acapy_controller"] - -[dependency-groups] -dev = ["pytest>=8.4.1"] diff --git a/dva-acapy-controller/src/dva_acapy_controller/config.py b/dva-acapy-controller/src/dva_acapy_controller/config.py deleted file mode 100644 index 9f73c4d7..00000000 --- a/dva-acapy-controller/src/dva_acapy_controller/config.py +++ /dev/null @@ -1,17 +0,0 @@ -from os import environ as env - - -ADMIN_URL = env.get("ADMIN_URL") -ADMIN_LABEL = env.get("ADMIN_LABEL") - -PEER_AGENT_URL = env.get("PEER_AGENT_URL") -PEER_CONTROLLER_URL = env.get("PEER_CONTROLLER_URL") -PEER_CONTROLLER_PORT_IN = env.get("PEER_CONTROLLER_PORT_IN") -PEER_CONTROLLER_PORT_OUT = env.get("PEER_CONTROLLER_PORT_OUT") -PEER_LABEL = env.get("PEER_LABEL") - -POSTGRES_URL = env.get("DVA_POSTGRES_URL", default="postgresql://localhost:5432/dva") -POSTGRES_USER = env.get("DVA_POSTGRES_USER", default="postgres") -POSTGRES_PASSWORD = env.get("DVA_POSTGRES_PASSWORD", default="postgres") - -LOG_FILE = "log.json" diff --git a/dva-acapy-controller/src/dva_acapy_controller/controller.py b/dva-acapy-controller/src/dva_acapy_controller/controller.py deleted file mode 100644 index fc0da8bd..00000000 --- a/dva-acapy-controller/src/dva_acapy_controller/controller.py +++ /dev/null @@ -1,465 +0,0 @@ -import requests -import json - -from psycopg_pool import AsyncConnectionPool -from asyncio import Queue, CancelledError -from contextlib import asynccontextmanager -from datetime import datetime -from fastapi import FastAPI, Request, HTTPException -from fastapi.responses import StreamingResponse, HTMLResponse -from fastapi.staticfiles import StaticFiles -from pathlib import Path -from pydantic import BaseModel -from time import sleep -from typing import Dict, Any -from uuid import uuid4 - -from .config import ( - ADMIN_LABEL, - ADMIN_URL, - LOG_FILE, - PEER_LABEL, - POSTGRES_URL, - POSTGRES_USER, - POSTGRES_PASSWORD, -) - - -class AOV(BaseModel): - vc_id: str - valid_since: datetime - subject: str - issuer_id: str - record_id: str - contract_id: str - data_exchange_id: str - payload: str - - -class AOVRequest(BaseModel): - request_id: str - exchange_id: str - contract_id: str - subject: str - issuer_id: str - payload: Dict[str, Any] - target: str = "self" - - -class AoVPresentationRequest(BaseModel): - dataExchangeId: str - attesterLabel: str - attesterAgentURL: str - - -@asynccontextmanager -async def lifespan(app): - print(f"{ADMIN_LABEL} Controller is starting...") - await presentation_queue.put({"message": "Startup test data!"}) - print("Starting self initialization...") - await init_all_self() - print("Self init done!") - global pool - pool = AsyncConnectionPool( - conninfo=POSTGRES_URL, - kwargs=dict(user=POSTGRES_USER, password=POSTGRES_PASSWORD), - min_size=2, - max_size=10, - ) - pool.open() - - yield - - print(f"{ADMIN_LABEL} Controller is shutting down...") - pool.close() - if webhook_logs: - with open("log_human_readable.json", "w") as f: - json.dump(webhook_logs, f, indent=2) - - -pool: AsyncConnectionPool - -app = FastAPI(lifespan=lifespan) -app.mount("/static", StaticFiles(directory="static", html=True), name="static") - -webhook_logs = [] -presentation_queue = Queue() - -SELF_CONNECTION_ID = None -SELF_SCHEMA_ID = None -SELF_CRED_DEF_ID = None - - -@app.get("/", response_class=HTMLResponse) -async def root(): - html = Path("static/index.html").read_text(encoding="utf-8") - return HTMLResponse(content=html) - - -# Helper function: wait until connection is active -def wait_until_connection_active(admin_url, conn_id, timeout=10): - for _ in range(timeout): - conns = requests.get(f"{admin_url}/connections").json()["results"] - conn = next((c for c in conns if c["connection_id"] == conn_id), None) - if conn and conn["state"] == "active": - return True - sleep(1) - return False - - -@app.post("/webhooks/topic/{topic}/") -async def webhook_listener(topic: str, request: Request): - data = await request.json() - data["source"] = ADMIN_LABEL - webhook_logs.append({"topic": topic, "data": data}) - with open(LOG_FILE, "w") as f: - json.dump(webhook_logs, f, indent=2) - await presentation_queue.put(data) - print(f"Webhook received on topic: {topic}") - return {"status": "received"} - - -@app.get("/connections/self") -async def get_self_connection(): - global SELF_CONNECTION_ID - if not SELF_CONNECTION_ID: - return {"error": "Self-connection not initialized."} - return {"self_connection_id": SELF_CONNECTION_ID} - - -@app.post("/init_all_self") -async def init_all_self(): - global SELF_CONNECTION_ID, SELF_SCHEMA_ID, SELF_CRED_DEF_ID - - # Step 1: Create Invitation for self-connection - invitation_resp = requests.post( - f"{ADMIN_URL}/out-of-band/create-invitation", - json={ - "handshake_protocols": ["https://didcomm.org/didexchange/1.0"], - "use_public_did": False, - }, - ) - if invitation_resp.status_code != 200: - return { - "error": "Failed to create self-invitation", - "details": invitation_resp.text, - } - - invitation = invitation_resp.json()["invitation"] - - # Step 2: Receive own invitation - receive_resp = requests.post( - f"{ADMIN_URL}/out-of-band/receive-invitation", - json=invitation, - ) - if receive_resp.status_code != 200: - return { - "error": "Failed to accept own invitation", - "details": receive_resp.text, - } - - conn_id = receive_resp.json()["connection_id"] - - # Step 3: Wait until connection is active - if not wait_until_connection_active(f"{ADMIN_URL}/", conn_id): - return {"error": "Self connection did not become active"} - - SELF_CONNECTION_ID = conn_id - - # Step 4: Create Self-Identity Schema - schema_resp = requests.post( - f"{ADMIN_URL}/schemas", - json={ - "schema_name": f"Self-Identity-{uuid4().hex[:6]}", - "schema_version": "1.0", - "attributes": [ - "vc_id", - "valid_since", - "subject", - "issuer_id", - "record_id", - "contract_id", - "data_exchange_id", - "payload", - ], - }, - ) - if schema_resp.status_code != 200: - return { - "error": "Failed to create Self-Identity schema", - "details": schema_resp.text, - } - - SELF_SCHEMA_ID = schema_resp.json().get("schema_id") - - # Step 5: Create Credential Definition - cred_def_resp = requests.post( - f"{ADMIN_URL}/credential-definitions", - json={ - "schema_id": SELF_SCHEMA_ID, - "support_revocation": False, - "tag": "self-identity", - }, - ) - if cred_def_resp.status_code != 200: - return { - "error": "Failed to create Self-Identity credential definition", - "details": cred_def_resp.text, - } - - SELF_CRED_DEF_ID = cred_def_resp.json().get("credential_definition_id") - - with open("cred_def.json", "w") as f: - json.dump({"cred_def_id": SELF_CRED_DEF_ID}, f) - - # Step 6: Self-Issue Credential to Own Wallet - cred_attrs = { - "vc_id": str(uuid4()), - "valid_since": datetime.utcnow().isoformat(), - "subject": "Provider Subject", - "issuer_id": "Provider", - "record_id": str(uuid4()), - "contract_id": "contract123", - "data_exchange_id": "xchg123", - "payload": "[]", - } - - issue_payload = { - "connection_id": SELF_CONNECTION_ID, - "credential_preview": { - "@type": "issue-credential/2.0/credential-preview", - "attributes": [{"name": k, "value": v} for k, v in cred_attrs.items()], - }, - "filter": {"indy": {"cred_def_id": SELF_CRED_DEF_ID}}, - } - - issue_resp = requests.post( - f"{ADMIN_URL}/issue-credential-2.0/send-offer", json=issue_payload - ) - if issue_resp.status_code != 200: - return { - "error": "Failed to self-issue credential", - "details": issue_resp.text, - } - - return { - "message": "Self-connection, schema, credential definition, and self-credential issued successfully.", - "self_connection_id": SELF_CONNECTION_ID, - "self_schema_id": SELF_SCHEMA_ID, - "self_cred_def_id": SELF_CRED_DEF_ID, - } - - -@app.post("/generate_aov") -async def generate_aov(payload: AOVRequest): - global SELF_CONNECTION_ID, SELF_CRED_DEF_ID - - record = AOV( - vc_id=str(uuid4()), - valid_since=datetime.utcnow(), - subject=payload.subject, - issuer_id=payload.issuer_id, - record_id=str(uuid4()), - contract_id=payload.contract_id, - data_exchange_id=payload.exchange_id, - payload=json.dumps(payload.payload), - ) - - target = payload.target - if target not in ["self", PEER_LABEL]: - raise HTTPException(status_code=400, detail="Invalid target") - - if target == "self": - if not SELF_CONNECTION_ID or not SELF_CRED_DEF_ID: - raise HTTPException( - status_code=400, - detail="Self connection or credential definition not initialized", - ) - connection_id = SELF_CONNECTION_ID - cred_def_id = SELF_CRED_DEF_ID - else: - conns = requests.get(f"{ADMIN_URL}/connections").json()["results"] - peer_conn = next( - (c for c in conns if c["connection_id"] != SELF_CONNECTION_ID), - None, - ) - if not peer_conn: - raise HTTPException( - status_code=400, detail=f"No {PEER_LABEL} connection found" - ) - connection_id = peer_conn["connection_id"] - - try: - with open("cred_def.txt", "r") as f: - cred_def_data = json.load(f) - cred_def_id = cred_def_data["cred_def_id"] - except Exception: - raise HTTPException( - status_code=500, detail="Missing or invalid credential definition file" - ) - - cred_attrs = { - "vc_id": record.vc_id, - "valid_since": record.valid_since.isoformat(), - "subject": record.subject, - "issuer_id": record.issuer_id, - "record_id": record.record_id, - "contract_id": record.contract_id, - "data_exchange_id": record.data_exchange_id, - "payload": record.payload, - } - - issue_payload = { - "connection_id": connection_id, - "credential_preview": { - "@type": "issue-credential/2.0/credential-preview", - "attributes": [{"name": k, "value": v} for k, v in cred_attrs.items()], - }, - "filter": {"indy": {"cred_def_id": cred_def_id}}, - } - - issue_resp = requests.post( - f"{ADMIN_URL}/issue-credential-2.0/send", json=issue_payload - ) - print(f"Issue response: {issue_resp}") - - if issue_resp.status_code != 200: - return {"error": "Credential issue failed", "details": issue_resp.text} - - async with pool.connection() as conn: - cur = await conn.execute( - """ - UPDATE request_logs - SET vc_issued_date = %s, vc_id = %s - WHERE request_id = %s - """, - (datetime.utcnow(), record.vc_id, payload.request_id), - ) - match cur.rowcount: - case 0: - print("Did not update any PostgreSQL table rows; this is likely a bug") - case 1: - print( - f"Successfully updated PostgreSQL table row for {payload.request_id}" - ) - case _: - print( - f"Updated more than one PostgreSQL table rows for {payload.request_id}; this is likely a bug" - ) - - return {"message": f"AOV issued to {target}", "credential_data": cred_attrs} - - -@app.post("/request_presentation_from_peer") -async def request_presentation_from_peer(payload: AoVPresentationRequest): - create_inv_resp = requests.post( - f"{ADMIN_URL}/out-of-band/create-invitation", - json={ - "handshake_protocols": ["https://didcomm.org/didexchange/1.0"], - "use_public_did": False, - }, - ) - if create_inv_resp.status_code != 200: - raise HTTPException( - status_code=500, - detail="Failed to create OOB invitation for attester", - ) - - invitation = create_inv_resp.json().get("invitation") - if not invitation: - raise HTTPException( - status_code=500, - detail="Failed to create OOB invitation for attester", - ) - - receive_inv_resp = requests.post( - f"{payload.attesterAgentURL}/out-of-band/receive-invitation", - json=invitation, - ) - if receive_inv_resp.status_code != 200: - raise HTTPException( - status_code=500, - detail="Failed to send invitation OOB invitation to attester", - ) - - is_active = False - for _ in range(15): - conns = requests.get(f"{ADMIN_URL}/connections").json().get("results", []) - attester_conn = next( - ( - c - for c in conns - if c["state"] == "active" - and c.get("their_label", "").lower() == payload.attesterLabel - ), - None, - ) - if attester_conn: - is_active = True - break - sleep(1) - - if not is_active: - raise HTTPException( - status_code=500, - detail="Connection to attester timed out", - ) - - pres_request = { - "connection_id": attester_conn.get("connection_id"), - "presentation_request": { - "indy": { - "name": "AoVPresentationRequest", - "version": "1.0", - "requested_attributes": { - "attr_subject": {"name": "subject", "restrictions": [{}]}, - "attr_issuer_id": {"name": "issuer_id", "restrictions": [{}]}, - "attr_vc_id": {"name": "vc_id", "restrictions": [{}]}, - "attr_valid_since": {"name": "valid_since", "restrictions": [{}]}, - "attr_record_id": {"name": "record_id", "restrictions": [{}]}, - "attr_contract_id": {"name": "contract_id", "restrictions": [{}]}, - "attr_data_exchange_id": { - "name": "data_exchange_id", - "restrictions": [ - {"attr::data_exchange_id::value": payload.dataExchangeId}, - ], - }, - "attr_payload": {"name": "payload", "restrictions": [{}]}, - }, - "requested_predicates": {}, - }, - }, - } - - resp = requests.post( - f"{ADMIN_URL}/present-proof-2.0/send-request", - json=pres_request, - ) - if resp.status_code != 200: - raise HTTPException( - status_code=500, - detail="Failed to send AoV presentation request to attester", - ) - resp.raise_for_status() - data = resp.json() - - return { - "message": "Presentation request sent to attester", - "aov": data, - } - - -@app.get("/events/") -async def events(): - async def event_generator(): - while True: - try: - event = await presentation_queue.get() - event_source = event.get("source") - if event_source is None: - event_source = ADMIN_LABEL - yield f"data: {json.dumps(event)}\n\n" - except CancelledError: - break - - return StreamingResponse(event_generator(), media_type="text/event-stream") diff --git a/dva-acapy-controller/static/index.html b/dva-acapy-controller/static/index.html deleted file mode 100644 index 03897652..00000000 --- a/dva-acapy-controller/static/index.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - -DVA ACA-Py Controller - - - -

DVA ACA-Py Controller – Real-Time Event Log

- -
- Listening for events… - - -
- - - - diff --git a/dva-acapy-controller/tests/test_controller.py b/dva-acapy-controller/tests/test_controller.py deleted file mode 100644 index a209c3d7..00000000 --- a/dva-acapy-controller/tests/test_controller.py +++ /dev/null @@ -1,42 +0,0 @@ -from pathlib import Path - -from fastapi.testclient import TestClient - -from dva_acapy_controller.config import ADMIN_LABEL -from dva_acapy_controller.controller import app -from dva_acapy_controller.controller import presentation_queue -from dva_acapy_controller.controller import webhook_logs - - -client = TestClient(app) - - -def test_get_root(): - resp = client.get("/") - - assert resp.status_code == 200 - assert resp.headers["Content-Type"].startswith("text/html") - assert resp.content == Path("static/index.html").read_bytes() - - -def test_post_webhook(): - topic = "testtopic" - data = { - "foo": "bar", - "baz": [1, 2, 3], - } - data_with_source = {**data, "source": ADMIN_LABEL} - - resp = client.post(f"/webhooks/topic/{topic}", json=data) - - assert resp.status_code == 200 - assert resp.headers["Content-Type"] == "application/json" - assert resp.json() == {"status": "received"} - assert webhook_logs[-1] == { - "topic": topic, - "data": data_with_source, - } - assert presentation_queue.get_nowait() == data_with_source - - -# Further tests would need refactoring and the ACA-Py agent diff --git a/dva-api/Dockerfile b/dva-api/Dockerfile index eb87ab0d..6459f9e2 100755 --- a/dva-api/Dockerfile +++ b/dva-api/Dockerfile @@ -20,7 +20,7 @@ EXPOSE 9090 RUN mkdir /app/ COPY --from=build /home/gradle/src/*/build/libs/*.jar /app/ COPY --chmod=755 ./dva-api/docker/docker-entrypoint /app/ -COPY ./docs/spec/openapi.yaml /app/ -ENV DVA_OPENAPI_FILE=/app/openapi.yaml +COPY ./docs/spec/dva-api.yaml ./docs/spec/components.yaml /app/spec/ +ENV DVA_OPENAPI_FILE=/app/spec/dva-api.yaml ENTRYPOINT ["/app/docker-entrypoint"] HEALTHCHECK --interval=5s --timeout=5s CMD nc -z localhost 9090 diff --git a/dva-api/README.md b/dva-api/README.md index 6dcdf1c3..d0fdaf49 100644 --- a/dva-api/README.md +++ b/dva-api/README.md @@ -18,7 +18,8 @@ docker buildx build -t dva-api:latest -f dva-api/Dockerfile ./ > Run it from the parent directory (the repository root). > ```console > docker run --rm -it \ -> -v ./docs/spec/openapi.yaml:/home/gradle/docs/spec/openapi.yaml:ro \ +> -v ./docs/spec/dva-api.yaml:/home/gradle/docs/spec/dva-api.yaml:ro \ +> -v ./docs/spec/components.yaml:/home/gradle/docs/spec/components.yaml:ro \ > -v /run/docker.sock:/run/docker.sock \ > $(docker buildx build -q --no-cache --target build -f dva-api/Dockerfile ./) \ > gradle test diff --git a/dva-api/api/build.gradle.kts b/dva-api/api/build.gradle.kts index 627cea29..0c432775 100644 --- a/dva-api/api/build.gradle.kts +++ b/dva-api/api/build.gradle.kts @@ -5,31 +5,27 @@ plugins { } dependencies { - implementation(libs.slf4j.api) implementation(libs.bundles.logging) + implementation(libs.bundles.postgres) implementation(libs.bundles.ktor.client) implementation(libs.bundles.ktor.server) implementation(libs.ktor.server.html.builder) - implementation(libs.bundles.postgres) - implementation(libs.rabbitmq.amqp.client) - implementation(libs.rabbitmq.kotlin) - - implementation(libs.kotlinx.datetime) - implementation(project.dependencies.platform(libs.koin.bom)) implementation(libs.bundles.ktor.koin) implementation(libs.handlebars.java) + implementation(libs.kotlinx.datetime) implementation(project(":model")) runtimeOnly(libs.logevents) - testImplementation(libs.bundles.testcontainers.rabbitmq) testImplementation(libs.ktor.client.content.negotiation) + testImplementation(libs.ktor.client.mock) testImplementation(libs.ktor.server.test.host) + testImplementation(libs.mockk) } application { diff --git a/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/Application.kt b/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/Application.kt index 0426d212..e71c9912 100644 --- a/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/Application.kt +++ b/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/Application.kt @@ -1,15 +1,15 @@ package hu.bme.mit.ftsrg.dva.api -import com.rabbitmq.client.Connection -import com.rabbitmq.client.ConnectionFactory -import hu.bme.mit.ftsrg.dva.api.db.* +import hu.bme.mit.ftsrg.dva.api.db.PgRequestLogRepo +import hu.bme.mit.ftsrg.dva.api.db.configureDatabases import hu.bme.mit.ftsrg.dva.api.err.addHandlers -import hu.bme.mit.ftsrg.dva.api.rabbit.connectWithRetry -import hu.bme.mit.ftsrg.dva.api.route.* -import hu.bme.mit.ftsrg.dva.log.ReqestLogRepo -import hu.bme.mit.ftsrg.dva.log.VerifRequestLogRepo -import hu.bme.mit.ftsrg.dva.vla.TemplateRepo -import hu.bme.mit.ftsrg.dva.vla.VLARepo +import hu.bme.mit.ftsrg.dva.api.route.aovRoutes +import hu.bme.mit.ftsrg.dva.api.route.docRoutes +import hu.bme.mit.ftsrg.dva.api.route.infoRoutes +import hu.bme.mit.ftsrg.dva.api.upstream.Upstream +import hu.bme.mit.ftsrg.dva.api.upstream.UpstreamClient +import hu.bme.mit.ftsrg.dva.api.upstream.configureForUpstreams +import hu.bme.mit.ftsrg.dva.log.RequestLogRepo import io.ktor.client.* import io.ktor.client.engine.cio.CIO import io.ktor.http.* @@ -24,7 +24,8 @@ import kotlinx.serialization.json.Json import org.koin.dsl.module import org.koin.ktor.plugin.Koin import org.slf4j.event.Level -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation +import kotlin.time.Clock +import kotlin.time.ExperimentalTime import io.ktor.server.application.install as serverInstall import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation @@ -60,30 +61,15 @@ fun Application.installPlugins() { serverInstall(Resources) } +@OptIn(ExperimentalTime::class) fun Application.configureKoin() { - val rabbitHost = environment.config.property("rabbitmq.host").getString() - + val upstreamURLs: Map = + Upstream.entries.associateWith { environment.config.property(it.configKey).getString() } val appModule = module { - single { - ConnectionFactory().run { - host = rabbitHost - connectWithRetry(logger = log) - } - } - single { - HttpClient(CIO) { - install(ClientContentNegotiation) { - json(Json { - explicitNulls = true - ignoreUnknownKeys = true - }) - } - } - } - single { PgTemplateRepo() } - single { PgRequestLogRepo() } - single { PgVerifRequestLogRepo() } - single { PgVLARepo() } + single { HttpClient(CIO) { configureForUpstreams() } } + single { PgRequestLogRepo() } + single { Clock.System } + single { UpstreamClient(http = get(), baseURLs = upstreamURLs) } } serverInstall(Koin) { modules(appModule) } @@ -91,9 +77,6 @@ fun Application.configureKoin() { fun Application.addRoutes() { docRoutes(openapiPath = environment.config.property("swagger.openapiFile").getString()) - templateRoutes() aovRoutes() - vlaRoutes() - evaluationRoutes() infoRoutes() } diff --git a/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/db/PgRequestLogRepo.kt b/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/db/PgRequestLogRepo.kt index 9d2e928c..bf517cbe 100644 --- a/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/db/PgRequestLogRepo.kt +++ b/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/db/PgRequestLogRepo.kt @@ -1,8 +1,7 @@ package hu.bme.mit.ftsrg.dva.api.db -import hu.bme.mit.ftsrg.dva.log.ReqestLogRepo import hu.bme.mit.ftsrg.dva.log.RequestLog -import hu.bme.mit.ftsrg.dva.log.RequestLogNew +import hu.bme.mit.ftsrg.dva.log.RequestLogRepo import kotlinx.datetime.TimeZone.Companion.UTC import kotlinx.datetime.toLocalDateTime import kotlinx.serialization.json.Json @@ -12,7 +11,7 @@ import kotlin.uuid.Uuid import kotlin.uuid.toJavaUuid @OptIn(ExperimentalUuidApi::class, ExperimentalTime::class) -class PgRequestLogRepo : ReqestLogRepo { +class PgRequestLogRepo : RequestLogRepo { override suspend fun all(): List = suspendTransaction { RequestLogEntity.all().map { it.toModel() } } @@ -21,21 +20,18 @@ class PgRequestLogRepo : ReqestLogRepo { RequestLogEntity.findById(id.toJavaUuid())?.toModel() } - override suspend fun add(request: RequestLogNew): RequestLog? = suspendTransaction { + override suspend fun add(request: RequestLog): RequestLog? = suspendTransaction { RequestLogEntity.new { type = request.type.name - requestID = request.requestID.toString() - exchangeID = request.exchangeID - contractID = request.contractID + exchangeID = request.exchangeID.toString() + contractID = request.contractID.toString() vlaID = request.vlaID.toString() data = Json.encodeToString(request.data) - attesterID = request.attesterID - evaluationPassing = request.evaluationPassing ?: false + evaluationPassing = request.evaluationPassing evaluationResults = Json.encodeToString(request.evaluationResults) receivedDate = request.receivedDate.toLocalDateTime(UTC) - evaluationDate = request.evaluationDate?.toLocalDateTime(UTC) - vcIssuedDate = request.vcIssuedDate?.toLocalDateTime(UTC) - vcID = request.vcID + vcID = request.vcID.toString() + error = request.error?.let { Json.encodeToString(it) } }.toModel() } } \ No newline at end of file diff --git a/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/db/PgTemplateRepo.kt b/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/db/PgTemplateRepo.kt deleted file mode 100644 index 298c9d74..00000000 --- a/dva-api/api/src/main/kotlin/hu/bme/mit/ftsrg/dva/api/db/PgTemplateRepo.kt +++ /dev/null @@ -1,61 +0,0 @@ -package hu.bme.mit.ftsrg.dva.api.db - -import hu.bme.mit.ftsrg.dva.vla.Template -import hu.bme.mit.ftsrg.dva.vla.TemplateNew -import hu.bme.mit.ftsrg.dva.vla.TemplatePatch -import hu.bme.mit.ftsrg.dva.vla.TemplateRepo -import kotlinx.serialization.json.Json -import org.jetbrains.exposed.v1.core.SqlExpressionBuilder.eq -import org.jetbrains.exposed.v1.jdbc.deleteAll -import org.jetbrains.exposed.v1.jdbc.deleteWhere -import kotlin.uuid.ExperimentalUuidApi -import kotlin.uuid.Uuid -import kotlin.uuid.toJavaUuid - -@OptIn(ExperimentalUuidApi::class) -class PgTemplateRepo : TemplateRepo { - override suspend fun all(): List