diff --git a/.github/workflows/blackformat.yml b/.github/workflows/blackformat.yml index d4f97101aa..93d54834ae 100644 --- a/.github/workflows/blackformat.yml +++ b/.github/workflows/blackformat.yml @@ -17,4 +17,4 @@ jobs: - name: Black Code Formatter Check # The version of black should be adjusted at the same time dev # dependencies are updated. - uses: psf/black@24.4.0 + uses: psf/black@24.4.2 diff --git a/.github/workflows/publish-indy.yml b/.github/workflows/publish-indy.yml deleted file mode 100644 index 3d95cc6d84..0000000000 --- a/.github/workflows/publish-indy.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Publish ACA-Py Image (Indy) -run-name: Publish ACA-Py ${{ inputs.tag || github.event.release.tag_name }} Image (Indy ${{ inputs.indy_version || '1.16.0' }}) -on: - release: - types: [published] - - workflow_dispatch: - inputs: - indy_version: - description: 'Indy SDK Version' - required: true - default: 1.16.0 - type: string - tag: - description: 'Image tag' - required: true - type: string - platforms: - description: 'Platforms - Comma separated list of the platforms to support.' - required: true - default: linux/amd64 - type: string - ref: - description: 'Optional - The branch, tag or SHA to checkout.' - required: false - type: string - -# Note: -# - ACA-Py with Indy SDK image builds do not include support for the linux/arm64 platform. -# - See notes below for details. - -env: - INDY_VERSION: ${{ inputs.indy_version || '1.16.0' }} - - # Images do not include support for the linux/arm64 platform due to a known issue compiling the postgres plugin - # - https://github.com/hyperledger/indy-sdk/issues/2445 - # There is a pending PR to fix this issue here; https://github.com/hyperledger/indy-sdk/pull/2453 - # - # linux/386 platform support has been disabled pending a permanent fix for https://github.com/hyperledger/aries-cloudagent-python/issues/2124 - # PLATFORMS: ${{ inputs.platforms || 'linux/amd64,linux/386' }} - PLATFORMS: ${{ inputs.platforms || 'linux/amd64' }} - -jobs: - publish-image: - strategy: - fail-fast: false - matrix: - python-version: ['3.9'] - - name: Publish ACA-Py Image (Indy) - runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v4 - with: - ref: ${{ inputs.ref || '' }} - - - name: Gather image info - id: info - run: | - echo "repo-owner=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_OUTPUT - - - name: Cache Docker layers - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Image Metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ghcr.io/${{ steps.info.outputs.repo-owner }}/aries-cloudagent-python - tags: | - type=raw,value=py${{ matrix.python-version }}-indy-${{ env.INDY_VERSION }}-${{ inputs.tag || github.event.release.tag_name }} - - - name: Build and Push Image to ghcr.io - uses: docker/build-push-action@v5 - with: - push: true - context: . - file: docker/Dockerfile.indy - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - target: main - build-args: | - python_version=${{ matrix.python-version }} - indy_version=${{ env.INDY_VERSION }} - acapy_version=${{ inputs.tag || github.event.release.tag_name }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - platforms: ${{ env.PLATFORMS }} - - # Temp fix - # https://github.com/docker/build-push-action/issues/252 - # https://github.com/moby/buildkit/issues/1896 - - name: Move cache - run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000000..37aa855c3c --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,76 @@ +# SPDX-License-Identifier: Apache-2.0 + +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + workflow_dispatch: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '17 21 * * 4' + push: + branches: [ "main" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@v4 # was v4.1.1 - b4ffde65f46336ab88eb53be808477a3936bae11 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@v2.3.3 # was v2.3.1 - 0864cf19026789058feabb7e87baa5f140aac736 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@v4 # was v3.pre.node20 97a0fba1372883ab732affbe8f94b823f91727db + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v3 # was v3.24.9 - 1b1aada464948af03b950897e5eb522f92603cc2 + with: + sarif_file: results.sarif \ No newline at end of file diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 30d997d594..57e3b01580 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -1,10 +1,11 @@ name: Snyk Container on: - pull_request: - branches: [main] + push: + branches: + - main paths: - - aries_cloudagent - - docker + - aries_cloudagent/** + - docker/** jobs: snyk: @@ -12,13 +13,15 @@ jobs: if: ${{ github.repository_owner == 'hyperledger' }} steps: - uses: actions/checkout@v4 + - name: Build a Docker image run: docker build -t aries-cloudagent -f docker/Dockerfile . + - name: Run Snyk to check Docker image for vulnerabilities # Snyk can be used to break the build when it detects vulnerabilities. # In this case we want to upload the issues to GitHub Code Scanning continue-on-error: true - uses: snyk/actions/docker@master + uses: snyk/actions/docker@0.4.0 env: # In order to use the Snyk Action you will need to have a Snyk API token. # More details in https://github.com/snyk/actions#getting-your-snyk-token @@ -27,6 +30,14 @@ jobs: with: image: aries-cloudagent args: --file=docker/Dockerfile + + # Replace any "null" security severity values with 0. The null value is used in the case + # of license-related findings, which do not do not indicate a security vulnerability. + # See https://github.com/github/codeql-action/issues/2187 for more context. + - name: Post process snyk sarif file + run: | + sed -i 's/"security-severity": "null"/"security-severity": "0"/g' snyk.sarif + - name: Upload result to GitHub Code Scanning uses: github/codeql-action/upload-sarif@v3 with: diff --git a/.github/workflows/tests-indy.yml b/.github/workflows/tests-indy.yml deleted file mode 100644 index 8b7651a39f..0000000000 --- a/.github/workflows/tests-indy.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Tests (Indy) - -on: - workflow_call: - inputs: - python-version: - required: true - type: string - indy-version: - required: true - type: string - os: - required: true - type: string - -jobs: - tests: - name: Test Python ${{ inputs.python-version }} on Indy ${{ inputs.indy-version }} - runs-on: ${{ inputs.os }} - steps: - - uses: actions/checkout@v4 - - - name: Cache image layers - uses: actions/cache@v4 - with: - path: /tmp/.buildx-cache-test - key: ${{ runner.os }}-buildx-test-${{ github.sha }} - restore-keys: | - ${{ runner.os }}-buildx-test- - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build test image - uses: docker/build-push-action@v5 - with: - load: true - context: . - file: docker/Dockerfile.indy - target: acapy-test - tags: acapy-test:latest - build-args: | - python_version=${{ inputs.python-version }} - indy_version=${{ inputs.indy-version }} - cache-from: type=local,src=/tmp/.buildx-cache-test - cache-to: type=local,dest=/tmp/.buildx-cache-test-new,mode=max - - # Temp fix - # https://github.com/docker/build-push-action/issues/252 - # https://github.com/moby/buildkit/issues/1896 - - name: Move cache - run: | - rm -rf /tmp/.buildx-cache-test - mv /tmp/.buildx-cache-test-new /tmp/.buildx-cache-test - - - name: Run pytest - run: | - docker run --rm acapy-test:latest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 038ab3fb1f..0901419505 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook - rev: v9.5.0 + rev: v9.16.0 hooks: - id: commitlint stages: [commit-msg] @@ -8,13 +8,13 @@ repos: additional_dependencies: ['@commitlint/config-conventional'] - repo: https://github.com/psf/black # Ensure this is synced with pyproject.toml - rev: 24.1.1 + rev: 24.4.2 hooks: - id: black stages: [commit] - repo: https://github.com/astral-sh/ruff-pre-commit # Ensure this is synced with pyproject.toml - rev: v0.1.2 + rev: v0.4.4 hooks: - id: ruff stages: [commit] diff --git a/CHANGELOG.md b/CHANGELOG.md index c99c3fe240..bdee88dd48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,16 @@ # Aries Cloud Agent Python Changelog -## 0.12.1rc1 +## 0.12.1 ### April 26, 2024 -Release 0.12.1rc1 is a small patch to cleanup some edge case issues in the handling of Out of Band invitations, revocation notification webhooks, and connection querying uncovered after the 0.12.0 release. Fixes and improvements were also made to the generation of ACA-Py's OpenAPI specifications. +Release 0.12.1 is a small patch to cleanup some edge case issues in the handling of Out of Band invitations, revocation notification webhooks, and connection querying uncovered after the 0.12.0 release. Fixes and improvements were also made to the generation of ACA-Py's OpenAPI specifications. -### 0.12.1rc1 Breaking Changes +### 0.12.1 Breaking Changes There are no breaking changes in this release. -#### 0.12.1rc1 Categorized List of Pull Requests +#### 0.12.1 Categorized List of Pull Requests - Out of Band Invitations and Connection Establishment updates/fixes: - 🐛 Fix ServiceDecorator parsing in oob record handling [\#2910](https://github.com/hyperledger/aries-cloudagent-python/pull/2910) [ff137](https://github.com/ff137) @@ -40,6 +40,7 @@ There are no breaking changes in this release. - Update GHA so that broken image links work on docs site - without breaking them on GitHub [\#2852](https://github.com/hyperledger/aries-cloudagent-python/pull/2852) [swcurran](https://github.com/swcurran) - Dependencies and Internal Updates: + - chore(deps): Bump psf/black from 24.4.0 to 24.4.2 in the all-actions group [\#2924](https://github.com/hyperledger/aries-cloudagent-python/pull/2924) [dependabot bot](https://github.com/dependabot bot) - fix: fixes a regression that requires a log file in multi-tenant mode [\#2918](https://github.com/hyperledger/aries-cloudagent-python/pull/2918) [amanji](https://github.com/amanji) - Update AnonCreds to 0.2.2 [\#2917](https://github.com/hyperledger/aries-cloudagent-python/pull/2917) [swcurran](https://github.com/swcurran) - chore(deps): Bump aiohttp from 3.9.3 to 3.9.4 dependencies python [\#2902](https://github.com/hyperledger/aries-cloudagent-python/pull/2902) [dependabot bot](https://github.com/dependabot bot) @@ -49,6 +50,7 @@ There are no breaking changes in this release. - refactor: logging configs setup [\#2870](https://github.com/hyperledger/aries-cloudagent-python/pull/2870) [amanji](https://github.com/amanji) - Release management pull requests: + - 0.12.1 [\#2926](https://github.com/hyperledger/aries-cloudagent-python/pull/2926) [swcurran](https://github.com/swcurran) - 0.12.1rc1 [\#2921](https://github.com/hyperledger/aries-cloudagent-python/pull/2921) [swcurran](https://github.com/swcurran) - 0.12.1rc0 [\#2912](https://github.com/hyperledger/aries-cloudagent-python/pull/2912) [swcurran](https://github.com/swcurran) diff --git a/README.md b/README.md index ee231d25f9..4077f04249 100644 --- a/README.md +++ b/README.md @@ -14,27 +14,27 @@ Check it out! It's much easier to navigate than this GitHub repo for reading the Hyperledger Aries Cloud Agent Python (ACA-Py) is a foundation for building Verifiable Credential (VC) ecosystems. It operates in the second and third layers of the [Trust Over IP framework (PDF)](https://trustoverip.org/wp-content/uploads/2020/05/toip_050520_primer.pdf) using [DIDComm messaging](https://github.com/hyperledger/aries-rfcs/tree/main/concepts/0005-didcomm) and [Hyperledger Aries](https://www.hyperledger.org/use/aries) protocols. The "cloud" in the name means that ACA-Py runs on servers (cloud, enterprise, IoT devices, and so forth), and is not designed to run on mobile devices. -ACA-Py is built on the Aries concepts and features that make up [Aries Interop Profile (AIP) 2.0](https://github.com/hyperledger/aries-rfcs/tree/main/concepts/0302-aries-interop-profile#aries-interop-profile-version-20). [ACA-Py’s supported Aries protocols](docs/features/SupportedRFCs.md) include, most importantly, protocols for issuing, verifying, and holding verifiable credentials using both [Hyperledger AnonCreds] verifiable credential format, and the [W3C Standard Verifiable Credential Data Model] format using JSON-LD with LD-Signatures and BBS+ Signatures. Coming soon -- issuing and presenting [Hyperledger AnonCreds] verifiable credentials using the [W3C Standard Verifiable Credential Data Model] format. +ACA-Py is built on the Aries concepts and features that make up [Aries Interop Profile (AIP) 2.0](https://github.com/hyperledger/aries-rfcs/tree/main/concepts/0302-aries-interop-profile#aries-interop-profile-version-20). [ACA-Py’s supported Aries protocols](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/features/SupportedRFCs.md) include, most importantly, protocols for issuing, verifying, and holding verifiable credentials using both [Hyperledger AnonCreds] verifiable credential format, and the [W3C Standard Verifiable Credential Data Model] format using JSON-LD with LD-Signatures and BBS+ Signatures. Coming soon -- issuing and presenting [Hyperledger AnonCreds] verifiable credentials using the [W3C Standard Verifiable Credential Data Model] format. [Hyperledger AnonCreds]: https://www.hyperledger.org/use/anoncreds [W3C Standard Verifiable Credential Data Model]: https://www.w3.org/TR/vc-data-model/ To use ACA-Py you create a business logic controller that "talks to" an ACA-Py instance (sending HTTP requests and receiving webhook notifications), and ACA-Py handles the Aries and DIDComm protocols and related functionality. Your controller can be built in any language that supports making and receiving HTTP requests; knowledge of Python is not needed. Together, this means you can focus on building VC solutions using familiar web development technologies, instead of having to learn the nuts and bolts of low-level cryptography and Trust over IP-type Aries protocols. -This [checklist-style overview document](docs/features/SupportedRFCs.md) provides a full list of the features in ACA-Py. +This [checklist-style overview document](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/features/SupportedRFCs.md) provides a full list of the features in ACA-Py. The following is a list of some of the core features needed for a production deployment, with a link to detailed information about the capability. ### Multi-Tenant -ACA-Py supports "multi-tenant" scenarios. In these scenarios, one (scalable) instance of ACA-Py uses one database instance, and are together capable of managing separate secure storage (for private keys, DIDs, credentials, etc.) for many different actors. This enables (for example) an "issuer-as-a-service", where an enterprise may have many VC issuers, each with different identifiers, using the same instance of ACA-Py to interact with VC holders as required. Likewise, an ACA-Py instance could be a "cloud wallet" for many holders (e.g. people or organizations) that, for whatever reason, cannot use a mobile device for a wallet. Learn more about multi-tenant deployments [here](docs/features/Multitenancy.md). +ACA-Py supports "multi-tenant" scenarios. In these scenarios, one (scalable) instance of ACA-Py uses one database instance, and are together capable of managing separate secure storage (for private keys, DIDs, credentials, etc.) for many different actors. This enables (for example) an "issuer-as-a-service", where an enterprise may have many VC issuers, each with different identifiers, using the same instance of ACA-Py to interact with VC holders as required. Likewise, an ACA-Py instance could be a "cloud wallet" for many holders (e.g. people or organizations) that, for whatever reason, cannot use a mobile device for a wallet. Learn more about multi-tenant deployments [here](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/features/Multitenancy.md). ### Mediator Service -Startup options allow the use of an ACA-Py as an Aries [mediator](https://github.com/hyperledger/aries-rfcs/tree/main/concepts/0046-mediators-and-relays#summary) using core Aries protocols to coordinate its mediation role. Such an ACA-Py instance receives, stores and forwards messages to Aries agents that (for example) lack an addressable endpoint on the Internet such as a mobile wallet. A live instance of a public mediator based on ACA-Py is available [here](https://indicio-tech.github.io/mediator/) from Indicio Technologies. Learn more about deploying a mediator [here](docs/features/Mediation.md). See the [Aries Mediator Service](https://github.com/hyperledger/aries-mediator-service) for a "best practices" configuration of an Aries mediator. +Startup options allow the use of an ACA-Py as an Aries [mediator](https://github.com/hyperledger/aries-rfcs/tree/main/concepts/0046-mediators-and-relays#summary) using core Aries protocols to coordinate its mediation role. Such an ACA-Py instance receives, stores and forwards messages to Aries agents that (for example) lack an addressable endpoint on the Internet such as a mobile wallet. A live instance of a public mediator based on ACA-Py is available [here](https://indicio-tech.github.io/mediator/) from Indicio Technologies. Learn more about deploying a mediator [here](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/features/Mediation.md). See the [Aries Mediator Service](https://github.com/hyperledger/aries-mediator-service) for a "best practices" configuration of an Aries mediator. ### Indy Transaction Endorsing -ACA-Py supports a Transaction Endorsement protocol, for agents that don't have write access to an Indy ledger. Endorser support is documented [here](docs/features/Endorser.md). +ACA-Py supports a Transaction Endorsement protocol, for agents that don't have write access to an Indy ledger. Endorser support is documented [here](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/features/Endorser.md). ### Scaled Deployments @@ -42,7 +42,7 @@ ACA-Py supports deployments in scaled environments such as in Kubernetes environ ### VC-API Endpoints -A set of endpoints conforming to the vc-api specification are included to manage w3c credentials and presentations. They are documented [here](docs/features/JsonLdCredentials.md#vc-api) and a postman demo is available [here](docs/features/JsonLdCredentials.md#vc-api). +A set of endpoints conforming to the vc-api specification are included to manage w3c credentials and presentations. They are documented [here](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/features/JsonLdCredentials.md#vc-api) and a postman demo is available [here](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/features/JsonLdCredentials.md#vc-api). ## Example Uses @@ -65,7 +65,7 @@ For those new to SSI, Aries and ACA-Py, there are a couple of Linux Foundation e The latter is the most useful for developers wanting to get a solid basis in using ACA-Py and other Aries Frameworks. -Also included here is a much more concise (but less maintained) [Getting Started Guide](docs/gettingStarted/README.md) that will take you from knowing next to nothing about decentralized identity to developing Aries-based business apps and services. You’ll run an Indy ledger (with no ramp-up time), ACA-Py apps and developer-oriented demos. The guide has a table of contents so you can skip the parts you already know. +Also included here is a much more concise (but less maintained) [Getting Started Guide](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/gettingStarted/README.md) that will take you from knowing next to nothing about decentralized identity to developing Aries-based business apps and services. You’ll run an Indy ledger (with no ramp-up time), ACA-Py apps and developer-oriented demos. The guide has a table of contents so you can skip the parts you already know. ### Understanding the Architecture @@ -73,22 +73,22 @@ There is an [architectural deep dive webinar](https://www.youtube.com/watch?v=FX ![drawing](./aca-py_architecture.png) -You can extend ACA-Py using plug-ins, which can be loaded at runtime. Plug-ins are mentioned in the [webinar](https://docs.google.com/presentation/d/1K7qiQkVi4n-lpJ3nUZY27OniUEM0c8HAIk4imCWCx5Q/edit#slide=id.g5d43fe05cc_0_145) and are [described in more detail here](docs/features/PlugIns.md). An ever-expanding set of ACA-Py plugins can be found +You can extend ACA-Py using plug-ins, which can be loaded at runtime. Plug-ins are mentioned in the [webinar](https://docs.google.com/presentation/d/1K7qiQkVi4n-lpJ3nUZY27OniUEM0c8HAIk4imCWCx5Q/edit#slide=id.g5d43fe05cc_0_145) and are [described in more detail here](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/features/PlugIns.md). An ever-expanding set of ACA-Py plugins can be found in the [Aries ACA-Py Plugins repository]. Check them out -- it might have the very plugin you need! [Aries ACA-Py Plugins repository]: https://github.com/hyperledger/aries-acapy-plugins ### Installation and Usage -Use the ["install and go" page for developers](docs/features/DevReadMe.md) if you are comfortable with Trust over IP and Aries concepts. ACA-Py can be run with Docker without installation (highly recommended), or can be installed [from PyPi](https://pypi.org/project/aries-cloudagent/). In the repository `/demo` folder there is a full set of demos for developers to use in getting up to speed quickly. Start with the [Traction Workshop] to go through a complete ACA-Py-based Issuer-Holder-Verifier flow in about 20 minutes. Next, the [Alice-Faber Demo](docs/demo/README.md) is a great way for developers try a zero-install example of how to use the ACA-Py API to operate a couple of Aries Agents. The [Read the Docs](https://aries-cloud-agent-python.readthedocs.io/en/latest/) overview is also a way to understand the internal modules and APIs that make up an ACA-Py instance. +Use the ["install and go" page for developers](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/features/DevReadMe.md) if you are comfortable with Trust over IP and Aries concepts. ACA-Py can be run with Docker without installation (highly recommended), or can be installed [from PyPi](https://pypi.org/project/aries-cloudagent/). In the repository `/demo` folder there is a full set of demos for developers to use in getting up to speed quickly. Start with the [Traction Workshop] to go through a complete ACA-Py-based Issuer-Holder-Verifier flow in about 20 minutes. Next, the [Alice-Faber Demo](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/demo/README.md) is a great way for developers try a zero-install example of how to use the ACA-Py API to operate a couple of Aries Agents. The [Read the Docs](https://aries-cloud-agent-python.readthedocs.io/en/latest/) overview is also a way to understand the internal modules and APIs that make up an ACA-Py instance. -If you would like to develop on ACA-Py locally note that we use Poetry for dependency management and packaging, if you are unfamiliar with poetry please see our [cheat sheet](docs/deploying/Poetry.md) +If you would like to develop on ACA-Py locally note that we use Poetry for dependency management and packaging, if you are unfamiliar with poetry please see our [cheat sheet](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/deploying/Poetry.md) -[Traction Workshop]: docs/demo/Aries-Workshop.md +[Traction Workshop]: https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/demo/Aries-Workshop.md ## About the ACA-Py Admin API -The [overview of ACA-Py’s API](docs/features/AdminAPI.md) is a great starting place for learning about the ACA-Py API when you are starting to build your own controller. +The [overview of ACA-Py’s API](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/features/AdminAPI.md) is a great starting place for learning about the ACA-Py API when you are starting to build your own controller. An ACA-Py instance puts together an OpenAPI-documented REST interface based on the protocols that are loaded. This is used by a controller application (written in any language) to manage the behavior of the agent. The controller can initiate actions (e.g. issuing a credential) and can respond to agent events (e.g. sending a presentation request after a connection is accepted). Agent events are delivered to the controller as webhooks to a configured URL. @@ -98,7 +98,7 @@ Technical note: the administrative API exposed by the agent for the controller t There are a number of resources for getting help with ACA-Py and troubleshooting any problems you might run into. The -[Troubleshooting](docs/testing/Troubleshooting.md) document contains some +[Troubleshooting](https://github.com/hyperledger/aries-cloudagent-python/blob/main/docs/testing/Troubleshooting.md) document contains some guidance about issues that have been experienced in the past. Feel free to submit PRs to supplement the troubleshooting document! Searching the [ACA-Py GitHub issues](https://github.com/hyperledger/aries-cloudagent-python/issues) @@ -113,15 +113,14 @@ The initial implementation of ACA-Py was developed by the Government of British [BC Digital Trust]: https://digital.gov.bc.ca/digital-trust/ -See the [MAINTAINERS.md](./MAINTAINERS.md) file for a list of the current ACA-Py +See the [MAINTAINERS.md](https://github.com/hyperledger/aries-cloudagent-python/blob/main/MAINTAINERS.md) file for a list of the current ACA-Py maintainers, and the guidelines for becoming a Maintainer. We'd love to have you -join the team if you are willing and able to carry out the [duties of a -Maintainer](./MAINTAINERS.md#the-duties-of-a-maintainer). +join the team if you are willing and able to carry out the [duties of a Maintainer](https://github.com/hyperledger/aries-cloudagent-python/blob/main/MAINTAINERS.md#the-duties-of-a-maintainer). ## Contributing -Pull requests are welcome! Please read our [contributions guide](./CONTRIBUTING.md) and submit your PRs. We enforce [developer certificate of origin](https://developercertificate.org/) (DCO) commit signing — [guidance](https://github.com/apps/dco) on this is available. We also welcome issues submitted about problems you encounter in using ACA-Py. +Pull requests are welcome! Please read our [contributions guide](https://github.com/hyperledger/aries-cloudagent-python/blob/main/CONTRIBUTING.md) and submit your PRs. We enforce [developer certificate of origin](https://developercertificate.org/) (DCO) commit signing — [guidance](https://github.com/apps/dco) on this is available. We also welcome issues submitted about problems you encounter in using ACA-Py. ## License -[Apache License Version 2.0](LICENSE) +[Apache License Version 2.0](https://github.com/hyperledger/aries-cloudagent-python/blob/main/LICENSE) diff --git a/aries_cloudagent/admin/decorators/auth.py b/aries_cloudagent/admin/decorators/auth.py new file mode 100644 index 0000000000..818f297d40 --- /dev/null +++ b/aries_cloudagent/admin/decorators/auth.py @@ -0,0 +1,80 @@ +"""Authentication decorators for the admin API.""" + +import functools + +from aiohttp import web + +from ...utils import general as general_utils +from ..request_context import AdminRequestContext + + +def admin_authentication(handler): + """Decorator to require authentication via admin API key. + + The decorator will check for a valid x-api-key header and + reject the request if it is missing or invalid. + If the agent is running in insecure mode, the request will be allowed without a key. + """ + + @functools.wraps(handler) + async def admin_auth(request): + context: AdminRequestContext = request["context"] + profile = context.profile + header_admin_api_key = request.headers.get("x-api-key") + valid_key = general_utils.const_compare( + profile.settings.get("admin.admin_api_key"), header_admin_api_key + ) + insecure_mode = bool(profile.settings.get("admin.admin_insecure_mode")) + + # We have to allow OPTIONS method access to paths without a key since + # browsers performing CORS requests will never include the original + # x-api-key header from the method that triggered the preflight + # OPTIONS check. + if insecure_mode or valid_key or (request.method == "OPTIONS"): + return await handler(request) + else: + raise web.HTTPUnauthorized( + reason="API Key invalid or missing", + text="API Key invalid or missing", + ) + + return admin_auth + + +def tenant_authentication(handler): + """Decorator to enable non-admin authentication. + + The decorator will: + - check for a valid bearer token in the Autorization header if running + in multi-tenant mode + - check for a valid x-api-key header if running in single-tenant mode + """ + + @functools.wraps(handler) + async def tenant_auth(request): + context: AdminRequestContext = request["context"] + profile = context.profile + authorization_header = request.headers.get("Authorization") + header_admin_api_key = request.headers.get("x-api-key") + valid_key = general_utils.const_compare( + profile.settings.get("admin.admin_api_key"), header_admin_api_key + ) + insecure_mode = bool(profile.settings.get("admin.admin_insecure_mode")) + multitenant_enabled = profile.settings.get("multitenant.enabled") + + # CORS fix: allow OPTIONS method access to paths without a token + if ( + (multitenant_enabled and authorization_header) + or (not multitenant_enabled and valid_key) + or insecure_mode + or request.method == "OPTIONS" + ): + return await handler(request) + else: + auth_mode = "Authorization token" if multitenant_enabled else "API key" + raise web.HTTPUnauthorized( + reason=f"{auth_mode} missing or invalid", + text=f"{auth_mode} missing or invalid", + ) + + return tenant_auth diff --git a/aries_cloudagent/admin/routes.py b/aries_cloudagent/admin/routes.py new file mode 100644 index 0000000000..62abab842a --- /dev/null +++ b/aries_cloudagent/admin/routes.py @@ -0,0 +1,232 @@ +"""Admin server routes.""" + +import asyncio +import re + +from aiohttp import web +from aiohttp_apispec import ( + docs, + response_schema, +) +from marshmallow import fields + +from ..core.plugin_registry import PluginRegistry +from ..messaging.models.openapi import OpenAPISchema +from ..utils.stats import Collector +from ..version import __version__ +from .decorators.auth import admin_authentication + + +class AdminModulesSchema(OpenAPISchema): + """Schema for the modules endpoint.""" + + result = fields.List( + fields.Str(metadata={"description": "admin module"}), + metadata={"description": "List of admin modules"}, + ) + + +class AdminConfigSchema(OpenAPISchema): + """Schema for the config endpoint.""" + + config = fields.Dict( + required=True, metadata={"description": "Configuration settings"} + ) + + +class AdminStatusSchema(OpenAPISchema): + """Schema for the status endpoint.""" + + version = fields.Str(metadata={"description": "Version code"}) + label = fields.Str(allow_none=True, metadata={"description": "Default label"}) + timing = fields.Dict(required=False, metadata={"description": "Timing results"}) + conductor = fields.Dict( + required=False, metadata={"description": "Conductor statistics"} + ) + + +class AdminResetSchema(OpenAPISchema): + """Schema for the reset endpoint.""" + + +class AdminStatusLivelinessSchema(OpenAPISchema): + """Schema for the liveliness endpoint.""" + + alive = fields.Boolean( + metadata={"description": "Liveliness status", "example": True} + ) + + +class AdminStatusReadinessSchema(OpenAPISchema): + """Schema for the readiness endpoint.""" + + ready = fields.Boolean( + metadata={"description": "Readiness status", "example": True} + ) + + +class AdminShutdownSchema(OpenAPISchema): + """Response schema for admin Module.""" + + +@docs(tags=["server"], summary="Fetch the list of loaded plugins") +@response_schema(AdminModulesSchema(), 200, description="") +@admin_authentication +async def plugins_handler(request: web.BaseRequest): + """Request handler for the loaded plugins list. + + Args: + request: aiohttp request object + + Returns: + The module list response + + """ + registry = request.app["context"].inject_or(PluginRegistry) + plugins = registry and sorted(registry.plugin_names) or [] + return web.json_response({"result": plugins}) + + +@docs(tags=["server"], summary="Fetch the server configuration") +@response_schema(AdminConfigSchema(), 200, description="") +@admin_authentication +async def config_handler(request: web.BaseRequest): + """Request handler for the server configuration. + + Args: + request: aiohttp request object + + Returns: + The web response + + """ + config = { + k: ( + request.app["context"].settings[k] + if (isinstance(request.app["context"].settings[k], (str, int))) + else request.app["context"].settings[k].copy() + ) + for k in request.app["context"].settings + if k + not in [ + "admin.admin_api_key", + "multitenant.jwt_secret", + "wallet.key", + "wallet.rekey", + "wallet.seed", + "wallet.storage_creds", + ] + } + for index in range(len(config.get("admin.webhook_urls", []))): + config["admin.webhook_urls"][index] = re.sub( + r"#.*", + "", + config["admin.webhook_urls"][index], + ) + + return web.json_response({"config": config}) + + +@docs(tags=["server"], summary="Fetch the server status") +@response_schema(AdminStatusSchema(), 200, description="") +@admin_authentication +async def status_handler(request: web.BaseRequest): + """Request handler for the server status information. + + Args: + request: aiohttp request object + + Returns: + The web response + + """ + status = {"version": __version__} + status["label"] = request.app["context"].settings.get("default_label") + collector = request.app["context"].inject_or(Collector) + if collector: + status["timing"] = collector.results + if request.app["conductor_stats"]: + status["conductor"] = await request.app["conductor_stats"]() + return web.json_response(status) + + +@docs(tags=["server"], summary="Reset statistics") +@response_schema(AdminResetSchema(), 200, description="") +@admin_authentication +async def status_reset_handler(request: web.BaseRequest): + """Request handler for resetting the timing statistics. + + Args: + request: aiohttp request object + + Returns: + The web response + + """ + collector = request.app["context"].inject_or(Collector) + if collector: + collector.reset() + return web.json_response({}) + + +async def redirect_handler(request: web.BaseRequest): + """Perform redirect to documentation.""" + raise web.HTTPFound("/api/doc") + + +@docs(tags=["server"], summary="Liveliness check") +@response_schema(AdminStatusLivelinessSchema(), 200, description="") +async def liveliness_handler(request: web.BaseRequest): + """Request handler for liveliness check. + + Args: + request: aiohttp request object + + Returns: + The web response, always indicating True + + """ + app_live = request.app._state["alive"] + if app_live: + return web.json_response({"alive": app_live}) + else: + raise web.HTTPServiceUnavailable(reason="Service not available") + + +@docs(tags=["server"], summary="Readiness check") +@response_schema(AdminStatusReadinessSchema(), 200, description="") +async def readiness_handler(request: web.BaseRequest): + """Request handler for liveliness check. + + Args: + request: aiohttp request object + + Returns: + The web response, indicating readiness for further calls + + """ + app_ready = request.app._state["ready"] and request.app._state["alive"] + if app_ready: + return web.json_response({"ready": app_ready}) + else: + raise web.HTTPServiceUnavailable(reason="Service not ready") + + +@docs(tags=["server"], summary="Shut down server") +@response_schema(AdminShutdownSchema(), description="") +@admin_authentication +async def shutdown_handler(request: web.BaseRequest): + """Request handler for server shutdown. + + Args: + request: aiohttp request object + + Returns: + The web response (empty production) + + """ + request.app._state["ready"] = False + loop = asyncio.get_event_loop() + asyncio.ensure_future(request.app["conductor_stop"](), loop=loop) + + return web.json_response({}) diff --git a/aries_cloudagent/admin/server.py b/aries_cloudagent/admin/server.py index c5fb4dc516..d9d58fa92b 100644 --- a/aries_cloudagent/admin/server.py +++ b/aries_cloudagent/admin/server.py @@ -6,20 +6,17 @@ import uuid import warnings import weakref -from hmac import compare_digest from typing import Callable, Coroutine, Optional, Pattern, Sequence, cast import aiohttp_cors import jwt from aiohttp import web from aiohttp_apispec import ( - docs, - response_schema, setup_aiohttp_apispec, validation_middleware, ) -from marshmallow import fields +from aries_cloudagent.wallet import singletons from ..config.injection_context import InjectionContext from ..config.logging import context_wallet_id @@ -27,20 +24,32 @@ from ..core.plugin_registry import PluginRegistry from ..core.profile import Profile from ..ledger.error import LedgerConfigError, LedgerTransactionError -from ..messaging.models.openapi import OpenAPISchema from ..messaging.responder import BaseResponder -from ..messaging.valid import UUIDFour from ..multitenant.base import BaseMultitenantManager, MultitenantManagerError +from ..storage.base import BaseStorage from ..storage.error import StorageNotFoundError +from ..storage.type import RECORD_TYPE_ACAPY_UPGRADING from ..transport.outbound.message import OutboundMessage from ..transport.outbound.status import OutboundSendStatus from ..transport.queue.basic import BasicMessageQueue +from ..utils import general as general_utils from ..utils.stats import Collector from ..utils.task_queue import TaskQueue from ..version import __version__ +from ..wallet.anoncreds_upgrade import check_upgrade_completion_loop from .base_server import BaseAdminServer from .error import AdminSetupError from .request_context import AdminRequestContext +from .routes import ( + config_handler, + liveliness_handler, + plugins_handler, + readiness_handler, + redirect_handler, + shutdown_handler, + status_handler, + status_reset_handler, +) LOGGER = logging.getLogger(__name__) @@ -58,57 +67,8 @@ "acapy::keylist::updated": "keylist", } - -class AdminModulesSchema(OpenAPISchema): - """Schema for the modules endpoint.""" - - result = fields.List( - fields.Str(metadata={"description": "admin module"}), - metadata={"description": "List of admin modules"}, - ) - - -class AdminConfigSchema(OpenAPISchema): - """Schema for the config endpoint.""" - - config = fields.Dict( - required=True, metadata={"description": "Configuration settings"} - ) - - -class AdminStatusSchema(OpenAPISchema): - """Schema for the status endpoint.""" - - version = fields.Str(metadata={"description": "Version code"}) - label = fields.Str(allow_none=True, metadata={"description": "Default label"}) - timing = fields.Dict(required=False, metadata={"description": "Timing results"}) - conductor = fields.Dict( - required=False, metadata={"description": "Conductor statistics"} - ) - - -class AdminResetSchema(OpenAPISchema): - """Schema for the reset endpoint.""" - - -class AdminStatusLivelinessSchema(OpenAPISchema): - """Schema for the liveliness endpoint.""" - - alive = fields.Boolean( - metadata={"description": "Liveliness status", "example": True} - ) - - -class AdminStatusReadinessSchema(OpenAPISchema): - """Schema for the readiness endpoint.""" - - ready = fields.Boolean( - metadata={"description": "Readiness status", "example": True} - ) - - -class AdminShutdownSchema(OpenAPISchema): - """Response schema for admin Module.""" +anoncreds_wallets = singletons.IsAnoncredsSingleton().wallets +in_progress_upgrades = singletons.UpgradeInProgressSingleton() class AdminResponder(BaseResponder): @@ -205,6 +165,40 @@ async def ready_middleware(request: web.BaseRequest, handler: Coroutine): raise web.HTTPServiceUnavailable(reason="Shutdown in progress") +@web.middleware +async def upgrade_middleware(request: web.BaseRequest, handler: Coroutine): + """Blocking middleware for upgrades.""" + context: AdminRequestContext = request["context"] + + # Already upgraded + if context.profile.name in anoncreds_wallets: + return await handler(request) + + # Upgrade in progress + if context.profile.name in in_progress_upgrades.wallets: + raise web.HTTPServiceUnavailable(reason="Upgrade in progress") + + # Avoid try/except in middleware with find_all_records + upgrade_initiated = [] + async with context.profile.session() as session: + storage = session.inject(BaseStorage) + upgrade_initiated = await storage.find_all_records(RECORD_TYPE_ACAPY_UPGRADING) + if upgrade_initiated: + # If we get here, than another instance started an upgrade + # We need to check for completion (or fail) in another process + in_progress_upgrades.set_wallet(context.profile.name) + is_subwallet = context.metadata and "wallet_id" in context.metadata + asyncio.create_task( + check_upgrade_completion_loop( + context.profile, + is_subwallet, + ) + ) + raise web.HTTPServiceUnavailable(reason="Upgrade in progress") + + return await handler(request) + + @web.middleware async def debug_middleware(request: web.BaseRequest, handler: Coroutine): """Show request detail in debug log.""" @@ -218,13 +212,6 @@ async def debug_middleware(request: web.BaseRequest, handler: Coroutine): return await handler(request) -def const_compare(string1, string2): - """Compare two strings in constant time.""" - if string1 is None or string2 is None: - return False - return compare_digest(string1.encode(), string2.encode()) - - class AdminServer(BaseAdminServer): """Admin HTTP server class.""" @@ -272,8 +259,6 @@ def __init__( self.multitenant_manager = context.inject_or(BaseMultitenantManager) self._additional_route_pattern: Optional[Pattern] = None - self.server_paths = [] - @property def additional_routes_pattern(self) -> Optional[Pattern]: """Pattern for configured additional routes to permit base wallet to access.""" @@ -306,87 +291,8 @@ async def make_application(self) -> web.Application: # we check here. assert self.admin_insecure_mode ^ bool(self.admin_api_key) - def is_unprotected_path(path: str): - return path in [ - "/api/doc", - "/api/docs/swagger.json", - "/favicon.ico", - "/ws", # ws handler checks authentication - "/status/live", - "/status/ready", - ] or path.startswith("/static/swagger/") - - # If admin_api_key is None, then admin_insecure_mode must be set so - # we can safely enable the admin server with no security - if self.admin_api_key: - - @web.middleware - async def check_token(request: web.Request, handler): - header_admin_api_key = request.headers.get("x-api-key") - valid_key = const_compare(self.admin_api_key, header_admin_api_key) - - # We have to allow OPTIONS method access to paths without a key since - # browsers performing CORS requests will never include the original - # x-api-key header from the method that triggered the preflight - # OPTIONS check. - if ( - valid_key - or is_unprotected_path(request.path) - or (request.method == "OPTIONS") - ): - return await handler(request) - else: - raise web.HTTPUnauthorized() - - middlewares.append(check_token) - collector = self.context.inject_or(Collector) - if self.multitenant_manager: - - @web.middleware - async def check_multitenant_authorization(request: web.Request, handler): - authorization_header = request.headers.get("Authorization") - path = request.path - - is_multitenancy_path = path.startswith("/multitenancy") - is_server_path = path in self.server_paths or path == "/features" - - # subwallets are not allowed to access multitenancy routes - if authorization_header and is_multitenancy_path: - raise web.HTTPUnauthorized() - - base_limited_access_path = ( - re.match( - f"^/connections/(?:receive-invitation|{UUIDFour.PATTERN})", path - ) - or path.startswith("/out-of-band/receive-invitation") - or path.startswith("/mediation/requests/") - or re.match( - f"/mediation/(?:request/{UUIDFour.PATTERN}|" - f"{UUIDFour.PATTERN}/default-mediator)", - path, - ) - or path.startswith("/mediation/default-mediator") - or self._matches_additional_routes(path) - ) - - # base wallet is not allowed to perform ssi related actions. - # Only multitenancy and general server actions - if ( - not authorization_header - and not is_multitenancy_path - and not is_server_path - and not is_unprotected_path(path) - and not base_limited_access_path - and not (request.method == "OPTIONS") # CORS fix - ): - raise web.HTTPUnauthorized() - - return await handler(request) - - middlewares.append(check_multitenant_authorization) - @web.middleware async def setup_context(request: web.Request, handler): authorization_header = request.headers.get("Authorization") @@ -453,6 +359,9 @@ async def setup_context(request: web.Request, handler): middlewares.append(setup_context) + # Upgrade middleware needs the context setup + middlewares.append(upgrade_middleware) + # Register validation_middleware last avoiding unauthorized validations middlewares.append(validation_middleware) @@ -466,19 +375,16 @@ async def setup_context(request: web.Request, handler): ) server_routes = [ - web.get("/", self.redirect_handler, allow_head=True), - web.get("/plugins", self.plugins_handler, allow_head=False), - web.get("/status", self.status_handler, allow_head=False), - web.get("/status/config", self.config_handler, allow_head=False), - web.post("/status/reset", self.status_reset_handler), - web.get("/status/live", self.liveliness_handler, allow_head=False), - web.get("/status/ready", self.readiness_handler, allow_head=False), - web.get("/shutdown", self.shutdown_handler, allow_head=False), + web.get("/", redirect_handler, allow_head=True), + web.get("/plugins", plugins_handler, allow_head=False), + web.get("/status", status_handler, allow_head=False), + web.get("/status/config", config_handler, allow_head=False), + web.post("/status/reset", status_reset_handler), + web.get("/status/live", liveliness_handler, allow_head=False), + web.get("/status/ready", readiness_handler, allow_head=False), + web.get("/shutdown", shutdown_handler, allow_head=False), web.get("/ws", self.websocket_handler, allow_head=False), ] - - # Store server_paths for multitenant authorization handling - self.server_paths = [route.path for route in server_routes] app.add_routes(server_routes) plugin_registry = self.context.inject_or(PluginRegistry) @@ -511,6 +417,11 @@ async def setup_context(request: web.Request, handler): app._state["ready"] = False app._state["alive"] = False + # set global-like variables + app["context"] = self.context + app["conductor_stats"] = self.conductor_stats + app["conductor_stop"] = self.conductor_stop + return app async def start(self) -> None: @@ -583,6 +494,10 @@ def sort_dict(raw: dict) -> dict: async def stop(self) -> None: """Stop the webserver.""" + # Stopped before admin server is created + if not self.app: + return + self.app._state["ready"] = False # in case call does not come through OpenAPI for queue in self.websocket_queues.values(): queue.stop() @@ -622,156 +537,6 @@ async def on_startup(self, app: web.Application): swagger["securityDefinitions"] = security_definitions swagger["security"] = security - @docs(tags=["server"], summary="Fetch the list of loaded plugins") - @response_schema(AdminModulesSchema(), 200, description="") - async def plugins_handler(self, request: web.BaseRequest): - """Request handler for the loaded plugins list. - - Args: - request: aiohttp request object - - Returns: - The module list response - - """ - registry = self.context.inject_or(PluginRegistry) - plugins = registry and sorted(registry.plugin_names) or [] - return web.json_response({"result": plugins}) - - @docs(tags=["server"], summary="Fetch the server configuration") - @response_schema(AdminConfigSchema(), 200, description="") - async def config_handler(self, request: web.BaseRequest): - """Request handler for the server configuration. - - Args: - request: aiohttp request object - - Returns: - The web response - - """ - config = { - k: ( - self.context.settings[k] - if (isinstance(self.context.settings[k], (str, int))) - else self.context.settings[k].copy() - ) - for k in self.context.settings - if k - not in [ - "admin.admin_api_key", - "multitenant.jwt_secret", - "wallet.key", - "wallet.rekey", - "wallet.seed", - "wallet.storage_creds", - ] - } - for index in range(len(config.get("admin.webhook_urls", []))): - config["admin.webhook_urls"][index] = re.sub( - r"#.*", - "", - config["admin.webhook_urls"][index], - ) - - return web.json_response({"config": config}) - - @docs(tags=["server"], summary="Fetch the server status") - @response_schema(AdminStatusSchema(), 200, description="") - async def status_handler(self, request: web.BaseRequest): - """Request handler for the server status information. - - Args: - request: aiohttp request object - - Returns: - The web response - - """ - status = {"version": __version__} - status["label"] = self.context.settings.get("default_label") - collector = self.context.inject_or(Collector) - if collector: - status["timing"] = collector.results - if self.conductor_stats: - status["conductor"] = await self.conductor_stats() - return web.json_response(status) - - @docs(tags=["server"], summary="Reset statistics") - @response_schema(AdminResetSchema(), 200, description="") - async def status_reset_handler(self, request: web.BaseRequest): - """Request handler for resetting the timing statistics. - - Args: - request: aiohttp request object - - Returns: - The web response - - """ - collector = self.context.inject_or(Collector) - if collector: - collector.reset() - return web.json_response({}) - - async def redirect_handler(self, request: web.BaseRequest): - """Perform redirect to documentation.""" - raise web.HTTPFound("/api/doc") - - @docs(tags=["server"], summary="Liveliness check") - @response_schema(AdminStatusLivelinessSchema(), 200, description="") - async def liveliness_handler(self, request: web.BaseRequest): - """Request handler for liveliness check. - - Args: - request: aiohttp request object - - Returns: - The web response, always indicating True - - """ - app_live = self.app._state["alive"] - if app_live: - return web.json_response({"alive": app_live}) - else: - raise web.HTTPServiceUnavailable(reason="Service not available") - - @docs(tags=["server"], summary="Readiness check") - @response_schema(AdminStatusReadinessSchema(), 200, description="") - async def readiness_handler(self, request: web.BaseRequest): - """Request handler for liveliness check. - - Args: - request: aiohttp request object - - Returns: - The web response, indicating readiness for further calls - - """ - app_ready = self.app._state["ready"] and self.app._state["alive"] - if app_ready: - return web.json_response({"ready": app_ready}) - else: - raise web.HTTPServiceUnavailable(reason="Service not ready") - - @docs(tags=["server"], summary="Shut down server") - @response_schema(AdminShutdownSchema(), description="") - async def shutdown_handler(self, request: web.BaseRequest): - """Request handler for server shutdown. - - Args: - request: aiohttp request object - - Returns: - The web response (empty production) - - """ - self.app._state["ready"] = False - loop = asyncio.get_event_loop() - asyncio.ensure_future(self.conductor_stop(), loop=loop) - - return web.json_response({}) - def notify_fatal_error(self): """Set our readiness flags to force a restart (openshift).""" LOGGER.error("Received shutdown request notify_fatal_error()") @@ -793,7 +558,7 @@ async def websocket_handler(self, request): else: header_admin_api_key = request.headers.get("x-api-key") # authenticated via http header? - queue.authenticated = const_compare( + queue.authenticated = general_utils.const_compare( header_admin_api_key, self.admin_api_key ) @@ -838,7 +603,7 @@ async def websocket_handler(self, request): LOGGER.exception( "Exception in websocket receiving task:" ) - if self.admin_api_key and const_compare( + if self.admin_api_key and general_utils.const_compare( self.admin_api_key, msg_api_key ): # authenticated via websocket message diff --git a/aries_cloudagent/admin/tests/test_admin_server.py b/aries_cloudagent/admin/tests/test_admin_server.py index 300e82f758..24c8ebfe6c 100644 --- a/aries_cloudagent/admin/tests/test_admin_server.py +++ b/aries_cloudagent/admin/tests/test_admin_server.py @@ -1,22 +1,28 @@ import gc import json +from unittest import IsolatedAsyncioTestCase import pytest -from aries_cloudagent.tests import mock -from unittest import IsolatedAsyncioTestCase from aiohttp import ClientSession, DummyCookieJar, TCPConnector, web from aiohttp.test_utils import unused_port +from aries_cloudagent.tests import mock +from aries_cloudagent.wallet import singletons + from ...config.default_context import DefaultContextBuilder from ...config.injection_context import InjectionContext from ...core.event_bus import Event +from ...core.goal_code_registry import GoalCodeRegistry from ...core.in_memory import InMemoryProfile from ...core.protocol_registry import ProtocolRegistry -from ...core.goal_code_registry import GoalCodeRegistry +from ...storage.base import BaseStorage +from ...storage.record import StorageRecord +from ...storage.type import RECORD_TYPE_ACAPY_UPGRADING from ...utils.stats import Collector from ...utils.task_queue import TaskQueue - +from ...wallet.anoncreds_upgrade import UPGRADING_RECORD_IN_PROGRESS from .. import server as test_module +from ..request_context import AdminRequestContext from ..server import AdminServer, AdminSetupError @@ -119,7 +125,7 @@ def get_admin_server( collector = Collector() context.injector.bind_instance(test_module.Collector, collector) - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile(settings=settings) self.port = unused_port() return AdminServer( @@ -190,105 +196,6 @@ async def test_import_routes(self): server = self.get_admin_server({"admin.admin_insecure_mode": True}, context) app = await server.make_application() - async def test_import_routes_multitenant_middleware(self): - # imports all default admin routes - context = InjectionContext( - settings={"multitenant.base_wallet_routes": ["/test"]} - ) - context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) - context.injector.bind_instance(GoalCodeRegistry, GoalCodeRegistry()) - context.injector.bind_instance( - test_module.BaseMultitenantManager, - mock.MagicMock(spec=test_module.BaseMultitenantManager), - ) - await DefaultContextBuilder().load_plugins(context) - server = self.get_admin_server( - { - "admin.admin_insecure_mode": False, - "admin.admin_api_key": "test-api-key", - }, - context, - ) - - # cover multitenancy start code - app = await server.make_application() - app["swagger_dict"] = {} - await server.on_startup(app) - - # multitenant authz - [mt_authz_middle] = [ - m for m in app.middlewares if ".check_multitenant_authorization" in str(m) - ] - - mock_request = mock.MagicMock( - method="GET", - headers={"Authorization": "Bearer ..."}, - path="/multitenancy/etc", - text=mock.CoroutineMock(return_value="abc123"), - ) - with self.assertRaises(test_module.web.HTTPUnauthorized): - await mt_authz_middle(mock_request, None) - - mock_request = mock.MagicMock( - method="GET", - headers={}, - path="/protected/non-multitenancy/non-server", - text=mock.CoroutineMock(return_value="abc123"), - ) - with self.assertRaises(test_module.web.HTTPUnauthorized): - await mt_authz_middle(mock_request, None) - - mock_request = mock.MagicMock( - method="GET", - headers={"Authorization": "Bearer ..."}, - path="/protected/non-multitenancy/non-server", - text=mock.CoroutineMock(return_value="abc123"), - ) - mock_handler = mock.CoroutineMock() - await mt_authz_middle(mock_request, mock_handler) - mock_handler.assert_called_once_with(mock_request) - - mock_request = mock.MagicMock( - method="GET", - headers={"Authorization": "Non-bearer ..."}, - path="/test", - text=mock.CoroutineMock(return_value="abc123"), - ) - mock_handler = mock.CoroutineMock() - await mt_authz_middle(mock_request, mock_handler) - mock_handler.assert_called_once_with(mock_request) - - # multitenant setup context exception paths - [setup_ctx_middle] = [m for m in app.middlewares if ".setup_context" in str(m)] - - mock_request = mock.MagicMock( - method="GET", - headers={"Authorization": "Non-bearer ..."}, - path="/protected/non-multitenancy/non-server", - text=mock.CoroutineMock(return_value="abc123"), - ) - with self.assertRaises(test_module.web.HTTPUnauthorized): - await setup_ctx_middle(mock_request, None) - - mock_request = mock.MagicMock( - method="GET", - headers={"Authorization": "Bearer ..."}, - path="/protected/non-multitenancy/non-server", - text=mock.CoroutineMock(return_value="abc123"), - ) - with mock.patch.object( - server.multitenant_manager, - "get_profile_for_token", - mock.CoroutineMock(), - ) as mock_get_profile: - mock_get_profile.side_effect = [ - test_module.MultitenantManagerError("corrupt token"), - test_module.StorageNotFoundError("out of memory"), - ] - for i in range(2): - with self.assertRaises(test_module.web.HTTPUnauthorized): - await setup_ctx_middle(mock_request, None) - async def test_register_external_plugin_x(self): context = InjectionContext() context.injector.bind_instance(ProtocolRegistry, ProtocolRegistry()) @@ -477,6 +384,47 @@ async def test_server_health_state(self): assert response.status == 503 await server.stop() + async def test_upgrade_middleware(self): + profile = InMemoryProfile.test_profile() + self.context = AdminRequestContext.test_context({}, profile) + self.request_dict = { + "context": self.context, + } + request = mock.MagicMock( + method="GET", + path_qs="/schemas/created", + match_info={}, + __getitem__=lambda _, k: self.request_dict[k], + ) + handler = mock.CoroutineMock() + + await test_module.upgrade_middleware(request, handler) + + async with profile.session() as session: + storage = session.inject(BaseStorage) + upgrading_record = StorageRecord( + RECORD_TYPE_ACAPY_UPGRADING, + UPGRADING_RECORD_IN_PROGRESS, + ) + # No upgrade in progress + await storage.add_record(upgrading_record) + + # Upgrade in progress without cache + with self.assertRaises(test_module.web.HTTPServiceUnavailable): + await test_module.upgrade_middleware(request, handler) + + # Upgrade in progress with cache + singletons.UpgradeInProgressSingleton().set_wallet("test-profile") + with self.assertRaises(test_module.web.HTTPServiceUnavailable): + await test_module.upgrade_middleware(request, handler) + + singletons.UpgradeInProgressSingleton().remove_wallet("test-profile") + await storage.delete_record(upgrading_record) + + # Upgrade in progress with cache + singletons.IsAnoncredsSingleton().set_wallet("test-profile") + await test_module.upgrade_middleware(request, handler) + @pytest.fixture async def server(): diff --git a/aries_cloudagent/admin/tests/test_auth.py b/aries_cloudagent/admin/tests/test_auth.py new file mode 100644 index 0000000000..2d6700a147 --- /dev/null +++ b/aries_cloudagent/admin/tests/test_auth.py @@ -0,0 +1,138 @@ +from unittest import IsolatedAsyncioTestCase + +from aiohttp import web + +from aries_cloudagent.tests import mock + +from ...core.in_memory.profile import InMemoryProfile +from ..decorators.auth import admin_authentication, tenant_authentication +from ..request_context import AdminRequestContext + + +class TestAdminAuthentication(IsolatedAsyncioTestCase): + def setUp(self) -> None: + + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "admin_api_key", + "admin.admin_insecure_mode": False, + } + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], headers={}, method="POST" + ) + self.decorated_handler = mock.CoroutineMock() + + async def test_options_request(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], headers={}, method="OPTIONS" + ) + decor_func = admin_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + async def test_insecure_mode(self): + self.profile.settings["admin.admin_insecure_mode"] = True + decor_func = admin_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + async def test_invalid_api_key(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "wrong-key"}, + method="POST", + ) + decor_func = admin_authentication(self.decorated_handler) + with self.assertRaises(web.HTTPUnauthorized): + await decor_func(self.request) + + async def test_valid_api_key(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "admin_api_key"}, + method="POST", + ) + decor_func = admin_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + +class TestTenantAuthentication(IsolatedAsyncioTestCase): + def setUp(self) -> None: + + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "admin_api_key", + "admin.admin_insecure_mode": False, + "multitenant.enabled": True, + } + ) + self.context = AdminRequestContext.test_context({}, self.profile) + self.request_dict = { + "context": self.context, + } + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], headers={}, method="POST" + ) + self.decorated_handler = mock.CoroutineMock() + + async def test_options_request(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], headers={}, method="OPTIONS" + ) + decor_func = tenant_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + async def test_insecure_mode(self): + self.profile.settings["admin.admin_insecure_mode"] = True + decor_func = tenant_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + async def test_single_tenant_invalid_api_key(self): + self.profile.settings["multitenant.enabled"] = False + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "wrong-key"}, + method="POST", + ) + decor_func = tenant_authentication(self.decorated_handler) + with self.assertRaises(web.HTTPUnauthorized): + await decor_func(self.request) + + async def test_single_tenant_valid_api_key(self): + self.profile.settings["multitenant.enabled"] = False + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "admin_api_key"}, + method="POST", + ) + decor_func = tenant_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) + + async def test_multi_tenant_missing_auth_header(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "wrong-key"}, + method="POST", + ) + decor_func = tenant_authentication(self.decorated_handler) + with self.assertRaises(web.HTTPUnauthorized): + await decor_func(self.request) + + async def test_multi_tenant_valid_auth_header(self): + self.request = mock.MagicMock( + __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "admin_api_key", "Authorization": "Bearer my-jwt"}, + method="POST", + ) + decor_func = tenant_authentication(self.decorated_handler) + await decor_func(self.request) + self.decorated_handler.assert_called_once_with(self.request) diff --git a/aries_cloudagent/anoncreds/default/legacy_indy/registry.py b/aries_cloudagent/anoncreds/default/legacy_indy/registry.py index 5bed179f96..a5a89a35e2 100644 --- a/aries_cloudagent/anoncreds/default/legacy_indy/registry.py +++ b/aries_cloudagent/anoncreds/default/legacy_indy/registry.py @@ -692,7 +692,7 @@ async def _get_or_fetch_rev_reg_def_max_cred_num( def _indexes_to_bit_array(self, indexes: List[int], size: int) -> List[int]: """Turn a sequence of indexes into a full state bit array.""" - return [1 if index in indexes else 0 for index in range(1, size + 1)] + return [1 if index in indexes else 0 for index in range(0, size + 1)] async def _get_ledger(self, profile: Profile, rev_reg_def_id: str): async with profile.session() as session: @@ -1123,7 +1123,7 @@ async def fix_ledger_entry( async def txn_submit( self, - profile: Profile, + ledger: BaseLedger, ledger_transaction: str, sign: bool = None, taa_accept: bool = None, @@ -1131,10 +1131,6 @@ async def txn_submit( write_ledger: bool = True, ) -> str: """Submit a transaction to the ledger.""" - ledger = profile.inject(BaseLedger) - - if not ledger: - raise LedgerError("No ledger available") try: async with ledger: diff --git a/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py b/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py index 58631cfadb..830a0bb722 100644 --- a/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py +++ b/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py @@ -9,7 +9,6 @@ from base58 import alphabet from .....anoncreds.base import ( - AnonCredsRegistrationError, AnonCredsSchemaAlreadyExists, ) from .....anoncreds.models.anoncreds_schema import ( @@ -21,7 +20,7 @@ from .....connections.models.conn_record import ConnRecord from .....core.in_memory.profile import InMemoryProfile from .....ledger.base import BaseLedger -from .....ledger.error import LedgerError, LedgerObjectAlreadyExistsError +from .....ledger.error import LedgerObjectAlreadyExistsError from .....messaging.responder import BaseResponder from .....protocols.endorse_transaction.v1_0.manager import ( TransactionManager, @@ -728,27 +727,16 @@ async def test_register_revocation_registry_definition_with_create_transaction_a assert mock_create_record.called async def test_txn_submit(self): - self.profile.inject = mock.MagicMock( - side_effect=[ - None, - mock.CoroutineMock( - txn_submit=mock.CoroutineMock(side_effect=LedgerError("test error")) - ), - mock.CoroutineMock( - txn_submit=mock.CoroutineMock(return_value="transaction response") - ), - ] + self.profile.context.injector.bind_instance( + BaseLedger, + mock.MagicMock( + txn_submit=mock.CoroutineMock(return_value="transaction_id") + ), ) - - # No ledger - with self.assertRaises(LedgerError): - await self.registry.txn_submit(self.profile, "test_txn") - # Write error - with self.assertRaises(AnonCredsRegistrationError): - await self.registry.txn_submit(self.profile, "test_txn") - - result = await self.registry.txn_submit(self.profile, "test_txn") - assert result == "transaction response" + async with self.profile.session() as session: + ledger = session.inject(BaseLedger) + result = await self.registry.txn_submit(ledger, "test_txn") + assert result == "transaction_id" async def test_register_revocation_list_no_endorsement(self): self.profile.context.injector.bind_instance( diff --git a/aries_cloudagent/anoncreds/holder.py b/aries_cloudagent/anoncreds/holder.py index 32ee6e86b1..957a8eb745 100644 --- a/aries_cloudagent/anoncreds/holder.py +++ b/aries_cloudagent/anoncreds/holder.py @@ -15,6 +15,7 @@ Presentation, PresentCredentials, create_link_secret, + W3cCredential, ) from aries_askar import AskarError, AskarErrorCode @@ -205,6 +206,25 @@ async def store_credential( except AnoncredsError as err: raise AnonCredsHolderError("Error processing received credential") from err + return await self._finish_store_credential( + credential_definition, + cred_recvd, + credential_request_metadata, + credential_attr_mime_types, + credential_id, + rev_reg_def, + ) + + async def _finish_store_credential( + self, + credential_definition: dict, + cred_recvd: Credential, + credential_request_metadata: dict, + credential_attr_mime_types: dict = None, + credential_id: str = None, + rev_reg_def: dict = None, + ) -> str: + credential_data = cred_recvd.to_dict() schema_id = cred_recvd.schema_id schema_id_parts = re.match(r"^(\w+):2:([^:]+):([^:]+)$", schema_id) if not schema_id_parts: @@ -259,6 +279,63 @@ async def store_credential( return credential_id + async def store_credential_w3c( + self, + credential_definition: dict, + credential_data: dict, + credential_request_metadata: dict, + credential_attr_mime_types: dict = None, + credential_id: str = None, + rev_reg_def: dict = None, + ) -> str: + """Store a credential in the wallet. + + Args: + credential_definition: Credential definition for this credential + credential_data: Credential data generated by the issuer + credential_request_metadata: credential request metadata generated + by the issuer + credential_attr_mime_types: dict mapping attribute names to (optional) + MIME types to store as non-secret record, if specified + credential_id: optionally override the stored credential id + rev_reg_def: revocation registry definition in json + + Returns: + the ID of the stored credential + + """ + try: + secret = await self.get_master_secret() + cred_w3c = W3cCredential.load(credential_data) + await asyncio.get_event_loop().run_in_executor( + None, + cred_w3c.process, + credential_request_metadata, + secret, + credential_definition, + rev_reg_def, + ) + cred_legacy = Credential.from_w3c(cred_w3c) + cred_recvd = await asyncio.get_event_loop().run_in_executor( + None, + cred_legacy.process, + credential_request_metadata, + secret, + credential_definition, + rev_reg_def, + ) + except AnoncredsError as err: + raise AnonCredsHolderError("Error processing received credential") from err + + return await self._finish_store_credential( + credential_definition, + cred_recvd, + credential_request_metadata, + credential_attr_mime_types, + credential_id, + rev_reg_def, + ) + async def get_credentials(self, start: int, count: int, wql: dict): """Get credentials stored in the wallet. diff --git a/aries_cloudagent/anoncreds/issuer.py b/aries_cloudagent/anoncreds/issuer.py index 1840d0de34..45c018de2b 100644 --- a/aries_cloudagent/anoncreds/issuer.py +++ b/aries_cloudagent/anoncreds/issuer.py @@ -13,6 +13,7 @@ CredentialOffer, KeyCorrectnessProof, Schema, + W3cCredential, ) from aries_askar import AskarError @@ -642,3 +643,62 @@ async def create_credential( raise AnonCredsIssuerError("Error creating credential") from err return credential.to_json() + + async def create_credential_w3c( + self, + credential_offer: dict, + credential_request: dict, + credential_values: dict, + ) -> str: + """Create Credential.""" + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + schema_id = credential_offer["schema_id"] + schema_result = await anoncreds_registry.get_schema(self.profile, schema_id) + cred_def_id = credential_offer["cred_def_id"] + schema_attributes = schema_result.schema_value.attr_names + + try: + async with self.profile.session() as session: + cred_def = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id) + cred_def_private = await session.handle.fetch( + CATEGORY_CRED_DEF_PRIVATE, cred_def_id + ) + except AskarError as err: + raise AnonCredsIssuerError( + "Error retrieving credential definition" + ) from err + + if not cred_def or not cred_def_private: + raise AnonCredsIssuerError( + "Credential definition not found for credential issuance" + ) + + raw_values = {} + for attribute in schema_attributes: + # Ensure every attribute present in schema to be set. + # Extraneous attribute names are ignored. + try: + credential_value = credential_values[attribute] + except KeyError: + raise AnonCredsIssuerError( + "Provided credential values are missing a value " + f"for the schema attribute '{attribute}'" + ) + + raw_values[attribute] = str(credential_value) + + try: + credential = await asyncio.get_event_loop().run_in_executor( + None, + lambda: W3cCredential.create( + cred_def.raw_value, + cred_def_private.raw_value, + credential_offer, + credential_request, + raw_values, + ), + ) + except AnoncredsError as err: + raise AnonCredsIssuerError("Error creating credential") from err + + return credential.to_json() diff --git a/aries_cloudagent/anoncreds/routes.py b/aries_cloudagent/anoncreds/routes.py index 316cc208ba..2eec01d12e 100644 --- a/aries_cloudagent/anoncreds/routes.py +++ b/aries_cloudagent/anoncreds/routes.py @@ -13,6 +13,7 @@ ) from marshmallow import fields +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..core.event_bus import EventBus from ..ledger.error import LedgerError @@ -145,6 +146,7 @@ class SchemaPostRequestSchema(OpenAPISchema): @docs(tags=["anoncreds - schemas"], summary="Create a schema on the connected ledger") @request_schema(SchemaPostRequestSchema()) @response_schema(SchemaResultSchema(), 200, description="") +@tenant_authentication async def schemas_post(request: web.BaseRequest): """Request handler for creating a schema. @@ -216,6 +218,7 @@ async def schemas_post(request: web.BaseRequest): @docs(tags=["anoncreds - schemas"], summary="Retrieve an individual schemas details") @match_info_schema(SchemaIdMatchInfo()) @response_schema(GetSchemaResultSchema(), 200, description="") +@tenant_authentication async def schema_get(request: web.BaseRequest): """Request handler for getting a schema. @@ -245,6 +248,7 @@ async def schema_get(request: web.BaseRequest): @docs(tags=["anoncreds - schemas"], summary="Retrieve all schema ids") @querystring_schema(SchemasQueryStringSchema()) @response_schema(GetSchemasResponseSchema(), 200, description="") +@tenant_authentication async def schemas_get(request: web.BaseRequest): """Request handler for getting all schemas. @@ -388,6 +392,7 @@ class CredDefsQueryStringSchema(OpenAPISchema): ) @request_schema(CredDefPostRequestSchema()) @response_schema(CredDefResultSchema(), 200, description="") +@tenant_authentication async def cred_def_post(request: web.BaseRequest): """Request handler for creating . @@ -439,6 +444,7 @@ async def cred_def_post(request: web.BaseRequest): ) @match_info_schema(CredIdMatchInfo()) @response_schema(GetCredDefResultSchema(), 200, description="") +@tenant_authentication async def cred_def_get(request: web.BaseRequest): """Request handler for getting credential definition. @@ -486,6 +492,7 @@ class GetCredDefsResponseSchema(OpenAPISchema): ) @querystring_schema(CredDefsQueryStringSchema()) @response_schema(GetCredDefsResponseSchema(), 200, description="") +@tenant_authentication async def cred_defs_get(request: web.BaseRequest): """Request handler for getting all credential definitions. @@ -576,6 +583,7 @@ class RevRegCreateRequestSchemaAnoncreds(OpenAPISchema): ) @request_schema(RevRegCreateRequestSchemaAnoncreds()) @response_schema(RevRegDefResultSchema(), 200, description="") +@tenant_authentication async def rev_reg_def_post(request: web.BaseRequest): """Request handler for creating revocation registry definition.""" context: AdminRequestContext = request["context"] @@ -659,6 +667,7 @@ class RevListCreateRequestSchema(OpenAPISchema): ) @request_schema(RevListCreateRequestSchema()) @response_schema(RevListResultSchema(), 200, description="") +@tenant_authentication async def rev_list_post(request: web.BaseRequest): """Request handler for creating registering a revocation list.""" context: AdminRequestContext = request["context"] @@ -694,6 +703,7 @@ async def rev_list_post(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema(), description="") +@tenant_authentication async def upload_tails_file(request: web.BaseRequest): """Request handler to upload local tails file for revocation registry. @@ -729,6 +739,7 @@ async def upload_tails_file(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema(), description="") +@tenant_authentication async def set_active_registry(request: web.BaseRequest): """Request handler to set the active registry. diff --git a/aries_cloudagent/anoncreds/tests/test_holder.py b/aries_cloudagent/anoncreds/tests/test_holder.py index 6a286b050a..549a5777d8 100644 --- a/aries_cloudagent/anoncreds/tests/test_holder.py +++ b/aries_cloudagent/anoncreds/tests/test_holder.py @@ -18,19 +18,19 @@ ) from aries_askar import AskarError, AskarErrorCode -from aries_cloudagent.anoncreds.holder import AnonCredsHolder, AnonCredsHolderError +from ..holder import AnonCredsHolder, AnonCredsHolderError from aries_cloudagent.anoncreds.tests.mock_objects import ( MOCK_CRED, MOCK_CRED_DEF, MOCK_PRES, MOCK_PRES_REQ, ) +from aries_cloudagent.askar.profile import AskarProfile from aries_cloudagent.askar.profile_anon import AskarAnoncredsProfile from aries_cloudagent.core.in_memory.profile import ( InMemoryProfile, InMemoryProfileSession, ) -from aries_cloudagent.indy.sdk.profile import IndySdkProfile from aries_cloudagent.tests import mock from aries_cloudagent.wallet.error import WalletNotFoundError @@ -57,6 +57,9 @@ def __init__(self, bad_schema=False, bad_cred_def=False): def to_json_buffer(self): return b"credential" + def to_dict(self): + return MOCK_CRED + class MockCredential: def __init__(self, bad_schema=False, bad_cred_def=False): @@ -199,7 +202,7 @@ async def test_create_credential_request( async def test_create_credential_request_with_non_anoncreds_profile_throws_x(self): self.profile = InMemoryProfile.test_profile( settings={"wallet-type": "askar"}, - profile_class=IndySdkProfile, + profile_class=AskarProfile, ) self.holder = test_module.AnonCredsHolder(self.profile) with self.assertRaises(ValueError): diff --git a/aries_cloudagent/anoncreds/tests/test_issuer.py b/aries_cloudagent/anoncreds/tests/test_issuer.py index 49e00cd4e6..b1ba1517a3 100644 --- a/aries_cloudagent/anoncreds/tests/test_issuer.py +++ b/aries_cloudagent/anoncreds/tests/test_issuer.py @@ -3,11 +3,7 @@ from unittest import IsolatedAsyncioTestCase import pytest -from anoncreds import ( - Credential, - CredentialDefinition, - CredentialOffer, -) +from anoncreds import Credential, CredentialDefinition, CredentialOffer, W3cCredential from aries_askar import AskarError, AskarErrorCode from aries_cloudagent.anoncreds.base import ( @@ -29,6 +25,9 @@ SchemaResult, SchemaState, ) +from aries_cloudagent.askar.profile import ( + AskarProfile, +) from aries_cloudagent.askar.profile_anon import ( AskarAnoncredsProfile, ) @@ -37,7 +36,6 @@ InMemoryProfile, InMemoryProfileSession, ) -from aries_cloudagent.indy.sdk.profile import IndySdkProfile from aries_cloudagent.tests import mock from .. import issuer as test_module @@ -139,9 +137,7 @@ async def test_init(self): assert isinstance(self.issuer.profile, AskarAnoncredsProfile) async def test_init_wrong_profile_type(self): - self.issuer._profile = InMemoryProfile.test_profile( - profile_class=IndySdkProfile - ) + self.issuer._profile = InMemoryProfile.test_profile(profile_class=AskarProfile) with self.assertRaises(ValueError): self.issuer.profile @@ -744,6 +740,21 @@ async def test_create_credential_offer_create( assert mock_create.called assert result is not None + @mock.patch.object(InMemoryProfileSession, "handle") + @mock.patch.object(CredentialDefinition, "load", return_value=MockCredDefEntry()) + @mock.patch.object(CredentialOffer, "create", return_value=MockCredOffer()) + async def test_create_credential_offer_create_vcdi( + self, mock_create, mock_load, mock_session_handle + ): + mock_session_handle.fetch = mock.CoroutineMock( + side_effect=[MockCredDefEntry(), MockKeyProof()] + ) + result = await self.issuer.create_credential_offer("cred-def-id") + assert mock_session_handle.fetch.called + assert mock_load.called + assert mock_create.called + assert result is not None + @mock.patch.object(InMemoryProfileSession, "handle") @mock.patch.object(Credential, "create", return_value=MockCredential()) async def test_create_credential(self, mock_create, mock_session_handle): @@ -762,3 +773,23 @@ async def test_create_credential(self, mock_create, mock_session_handle): assert result is not None assert mock_session_handle.fetch.called assert mock_create.called + + @mock.patch.object(InMemoryProfileSession, "handle") + @mock.patch.object(W3cCredential, "create", return_value=MockCredential()) + async def test_create_credential_vcdi(self, mock_create, mock_session_handle): + self.profile.inject = mock.Mock( + return_value=mock.MagicMock( + get_schema=mock.CoroutineMock(return_value=MockSchemaResult()), + ) + ) + + mock_session_handle.fetch = mock.CoroutineMock(return_value=MockCredDefEntry()) + result = await self.issuer.create_credential_w3c( + {"schema_id": "schema-id", "cred_def_id": "cred-def-id"}, + {}, + {"attr1": "value1", "attr2": "value2"}, + ) + + assert result is not None + assert mock_session_handle.fetch.called + assert mock_create.called diff --git a/aries_cloudagent/anoncreds/tests/test_revocation_setup.py b/aries_cloudagent/anoncreds/tests/test_revocation_setup.py index e21c09f305..24a6f471a6 100644 --- a/aries_cloudagent/anoncreds/tests/test_revocation_setup.py +++ b/aries_cloudagent/anoncreds/tests/test_revocation_setup.py @@ -37,7 +37,6 @@ async def asyncSetUp(self) -> None: async def test_on_cred_def_support_revocation_registers_revocation_def( self, mock_register_revocation_registry_definition ): - event = CredDefFinishedEvent( CredDefFinishedPayload( schema_id="schema_id", @@ -60,7 +59,6 @@ async def test_on_cred_def_support_revocation_registers_revocation_def( async def test_on_cred_def_author_with_auto_create_rev_reg_config_registers_reg_def( self, mock_register_revocation_registry_definition ): - self.profile.settings["endorser.author"] = True self.profile.settings["endorser.auto_create_rev_reg"] = True event = CredDefFinishedEvent( @@ -85,7 +83,6 @@ async def test_on_cred_def_author_with_auto_create_rev_reg_config_registers_reg_ async def test_on_cred_def_author_with_auto_create_rev_reg_config_and_support_revoc_option_registers_reg_def( self, mock_register_revocation_registry_definition ): - self.profile.settings["endorser.author"] = True self.profile.settings["endorser.auto_create_rev_reg"] = True event = CredDefFinishedEvent( @@ -110,7 +107,6 @@ async def test_on_cred_def_author_with_auto_create_rev_reg_config_and_support_re async def test_on_cred_def_not_author_or_support_rev_option( self, mock_register_revocation_registry_definition ): - event = CredDefFinishedEvent( CredDefFinishedPayload( schema_id="schema_id", diff --git a/aries_cloudagent/anoncreds/tests/test_routes.py b/aries_cloudagent/anoncreds/tests/test_routes.py index 5e90463b54..288f7f7eba 100644 --- a/aries_cloudagent/anoncreds/tests/test_routes.py +++ b/aries_cloudagent/anoncreds/tests/test_routes.py @@ -54,7 +54,10 @@ class TestAnoncredsRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self) -> None: self.session_inject = {} self.profile = InMemoryProfile.test_profile( - settings={"wallet.type": "askar-anoncreds"}, + settings={ + "wallet.type": "askar-anoncreds", + "admin.admin_api_key": "secret-key", + }, profile_class=AskarAnoncredsProfile, ) self.context = AdminRequestContext.test_context( @@ -69,6 +72,7 @@ async def asyncSetUp(self) -> None: query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) @mock.patch.object( @@ -342,7 +346,7 @@ async def test_set_active_registry(self, mock_set): async def test_schema_endpoints_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -355,6 +359,7 @@ async def test_schema_endpoints_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) # POST schema @@ -382,7 +387,7 @@ async def test_schema_endpoints_wrong_profile_403(self): async def test_cred_def_endpoints_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -395,6 +400,7 @@ async def test_cred_def_endpoints_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) # POST cred def @@ -425,7 +431,7 @@ async def test_cred_def_endpoints_wrong_profile_403(self): async def test_rev_reg_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -438,6 +444,7 @@ async def test_rev_reg_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( @@ -458,7 +465,7 @@ async def test_rev_reg_wrong_profile_403(self): async def test_rev_list_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -471,6 +478,7 @@ async def test_rev_list_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( @@ -481,7 +489,7 @@ async def test_rev_list_wrong_profile_403(self): async def test_uploads_tails_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -494,6 +502,7 @@ async def test_uploads_tails_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.match_info = {"rev_reg_id": "rev_reg_id"} @@ -502,7 +511,7 @@ async def test_uploads_tails_wrong_profile_403(self): async def test_active_registry_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -515,6 +524,7 @@ async def test_active_registry_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.match_info = {"rev_reg_id": "rev_reg_id"} diff --git a/aries_cloudagent/commands/tests/test_provision.py b/aries_cloudagent/commands/tests/test_provision.py index 15b7438791..9afd7cde98 100644 --- a/aries_cloudagent/commands/tests/test_provision.py +++ b/aries_cloudagent/commands/tests/test_provision.py @@ -1,5 +1,3 @@ -import pytest - from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase @@ -19,26 +17,6 @@ def test_bad_calls(self): with self.assertRaises(SystemExit): test_module.execute(["bad"]) - @pytest.mark.indy - def test_provision_wallet(self): - test_seed = "testseed000000000000000000000001" - test_module.execute( - [ - "--wallet-type", - "indy", - "--wallet-name", - "test_wallet", - "--wallet-key", - "key", - "--seed", - test_seed, - "--no-ledger", - "--endpoint", - "test_endpoint", - "--recreate-wallet", - ] - ) - async def test_provision_ledger_configured(self): profile = mock.MagicMock(close=mock.CoroutineMock()) with mock.patch.object( diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py index ed8b1fe948..96e5961f1c 100644 --- a/aries_cloudagent/config/argparse.py +++ b/aries_cloudagent/config/argparse.py @@ -585,6 +585,7 @@ def add_arguments(self, parser: ArgumentParser): metavar="", env_var="ACAPY_STORAGE_TYPE", help=( + "DEPRECATED: This option is ignored. " "Specifies the type of storage provider to use for the internal " "storage engine. This storage interface is used to store internal " "state. Supported internal storage types are 'basic' (memory) " @@ -640,14 +641,16 @@ def add_arguments(self, parser: ArgumentParser): "resolver instance." ), ) - parser.add_argument( - "--universal-resolver-bearer-token", - type=str, - nargs="?", - metavar="", - env_var="ACAPY_UNIVERSAL_RESOLVER_BEARER_TOKEN", - help="Bearer token if universal resolver instance requires authentication.", - ), + ( + parser.add_argument( + "--universal-resolver-bearer-token", + type=str, + nargs="?", + metavar="", + env_var="ACAPY_UNIVERSAL_RESOLVER_BEARER_TOKEN", + help="Bearer token if universal resolver instance requires authentication.", # noqa: E501 + ), + ) def get_settings(self, args: Namespace) -> dict: """Extract general settings.""" @@ -1573,10 +1576,10 @@ def add_arguments(self, parser: ArgumentParser): default="basic", env_var="ACAPY_WALLET_TYPE", help=( - "Specifies the type of Indy wallet provider to use. " + "Specifies the type of wallet provider to use. " "Supported internal storage types are 'basic' (memory), 'askar' " "and 'askar-anoncreds'." - "The default (if not specified) is 'basic'. 'indy' is deprecated." + "The default (if not specified) is 'basic'." ), ) parser.add_argument( @@ -1600,10 +1603,7 @@ def add_arguments(self, parser: ArgumentParser): help=( "Specifies the storage configuration to use for the wallet. " "This is required if you are for using 'postgres_storage' wallet " - 'storage type. For example, \'{"url":"localhost:5432", ' - '"wallet_scheme":"MultiWalletSingleTable"}\'. This ' - "configuration maps to the indy sdk postgres plugin " - "(PostgresConfig)." + 'storage type. For example, \'{"url":"localhost:5432"}\'.' ), ) parser.add_argument( @@ -1627,9 +1627,8 @@ def add_arguments(self, parser: ArgumentParser): "This is required if you are for using 'postgres_storage' wallet " 'For example, \'{"account":"postgres","password": ' '"mysecretpassword","admin_account":"postgres", ' - '"admin_password":"mysecretpassword"}\'. This configuration maps ' - "to the indy sdk postgres plugin (PostgresCredentials). NOTE: " - "admin_user must have the CREATEDB role or else initialization " + '"admin_password":"mysecretpassword"}\'.' + "NOTE: admin_user must have the CREATEDB role or else initialization " "will fail." ), ) @@ -1683,7 +1682,7 @@ def get_settings(self, args: Namespace) -> dict: if args.recreate_wallet: settings["wallet.recreate"] = True # check required settings for persistent wallets - if settings["wallet.type"] in ["indy", "askar", "askar-anoncreds"]: + if settings["wallet.type"] in ["askar", "askar-anoncreds"]: # requires name, key if not args.wallet_name or not args.wallet_key: raise ArgsParseError( @@ -1698,7 +1697,7 @@ def get_settings(self, args: Namespace) -> dict: if not args.wallet_storage_config or not args.wallet_storage_creds: raise ArgsParseError( "Parameters --wallet-storage-config and --wallet-storage-creds " - "must be provided for indy postgres wallets" + "must be provided for postgres wallets" ) return settings diff --git a/aries_cloudagent/config/default_context.py b/aries_cloudagent/config/default_context.py index a14de29720..3bd323a171 100644 --- a/aries_cloudagent/config/default_context.py +++ b/aries_cloudagent/config/default_context.py @@ -15,7 +15,6 @@ from ..resolver.did_resolver import DIDResolver from ..tails.base import BaseTailsServer from ..transport.wire_format import BaseWireFormat -from ..utils.dependencies import is_indy_sdk_module_installed from ..utils.stats import Collector from ..wallet.default_verification_key_strategy import ( BaseVerificationKeyStrategy, @@ -70,18 +69,6 @@ async def build_context(self) -> InjectionContext: async def bind_providers(self, context: InjectionContext): """Bind various class providers.""" - # Bind global indy pool provider to be able to share pools between wallets - # It is important the ledger pool provider is available in the base context - # so it can be shared by all wallet instances. If we set it in the indy sdk - # profile provider it could mean other wallets won't have access to the provider - if is_indy_sdk_module_installed(): - from ..ledger.indy import IndySdkLedgerPool, IndySdkLedgerPoolProvider - - context.injector.bind_provider( - IndySdkLedgerPool, - CachedProvider(IndySdkLedgerPoolProvider(), ("ledger.pool_name",)), - ) - context.injector.bind_provider(ProfileManager, ProfileManagerProvider()) wallet_type = self.settings.get("wallet.type") diff --git a/aries_cloudagent/core/conductor.py b/aries_cloudagent/core/conductor.py index d1f44e68ab..9c60a32606 100644 --- a/aries_cloudagent/core/conductor.py +++ b/aries_cloudagent/core/conductor.py @@ -7,6 +7,7 @@ """ +import asyncio import hashlib import json import logging @@ -17,12 +18,8 @@ from ..admin.base_server import BaseAdminServer from ..admin.server import AdminResponder, AdminServer -from ..commands.upgrade import ( - add_version_record, - get_upgrade_version_list, - upgrade, -) -from ..config.default_context import ContextBuilder +from ..commands.upgrade import add_version_record, get_upgrade_version_list, upgrade +from ..config.default_context import ContextBuilder, DefaultContextBuilder from ..config.injection_context import InjectionContext from ..config.ledger import ( get_genesis_transactions, @@ -63,7 +60,11 @@ from ..storage.base import BaseStorage from ..storage.error import StorageNotFoundError from ..storage.record import StorageRecord -from ..storage.type import RECORD_TYPE_ACAPY_STORAGE_TYPE +from ..storage.type import ( + RECORD_TYPE_ACAPY_STORAGE_TYPE, + STORAGE_TYPE_VALUE_ANONCREDS, + STORAGE_TYPE_VALUE_ASKAR, +) from ..transport.inbound.manager import InboundTransportManager from ..transport.inbound.message import InboundMessage from ..transport.outbound.base import OutboundDeliveryError @@ -71,10 +72,12 @@ from ..transport.outbound.message import OutboundMessage from ..transport.outbound.status import OutboundSendStatus from ..transport.wire_format import BaseWireFormat +from ..utils.profiles import get_subwallet_profiles_from_storage from ..utils.stats import Collector from ..utils.task_queue import CompletedTask, TaskQueue from ..vc.ld_proofs.document_loader import DocumentLoader from ..version import RECORD_TYPE_ACAPY_VERSION, __version__ +from ..wallet.anoncreds_upgrade import upgrade_wallet_to_anoncreds_if_requested from ..wallet.did_info import DIDInfo from .dispatcher import Dispatcher from .error import StartupError @@ -111,6 +114,8 @@ def __init__(self, context_builder: ContextBuilder) -> None: self.root_profile: Profile = None self.setup_public_did: DIDInfo = None + force_agent_anoncreds = False + @property def context(self) -> InjectionContext: """Accessor for the injection context.""" @@ -121,6 +126,9 @@ async def setup(self): context = await self.context_builder.build_context() + if self.force_agent_anoncreds: + context.settings.set_value("wallet.type", "askar-anoncreds") + # Fetch genesis transactions if necessary if context.settings.get("ledger.ledger_config_list"): await load_multiple_genesis_transactions_from_config(context.settings) @@ -168,17 +176,6 @@ async def setup(self): self.root_profile, ), ) - elif ( - self.root_profile.BACKEND_NAME == "indy" - and ledger.BACKEND_NAME == "indy" - ): - context.injector.bind_provider( - IndyVerifier, - ClassProvider( - "aries_cloudagent.indy.sdk.verifier.IndySdkVerifier", - self.root_profile, - ), - ) else: raise MultipleLedgerManagerError( "Multiledger is supported only for Indy SDK or Askar " @@ -522,7 +519,9 @@ async def start(self) -> None: except Exception: LOGGER.exception("Error accepting mediation invitation") - # notify protocols of startup status + await self.check_for_wallet_upgrades_in_progress() + + # notify protcols of startup status await self.root_profile.notify(STARTUP_EVENT_TOPIC, {}) async def stop(self, timeout=1.0): @@ -796,8 +795,9 @@ async def check_for_valid_wallet_type(self, profile): ) except StorageNotFoundError: acapy_version = None + # Any existing agent will have acapy_version record if acapy_version: - storage_type_from_storage = "askar" + storage_type_from_storage = STORAGE_TYPE_VALUE_ASKAR LOGGER.info( f"Existing agent found. Setting wallet type to {storage_type_from_storage}." # noqa: E501 ) @@ -820,6 +820,38 @@ async def check_for_valid_wallet_type(self, profile): ) if storage_type_from_storage != storage_type_from_config: - raise StartupError( - f"Wallet type config [{storage_type_from_config}] doesn't match with the wallet type in storage [{storage_type_record.value}]" # noqa: E501 - ) + if ( + storage_type_from_config == STORAGE_TYPE_VALUE_ASKAR + and storage_type_from_storage == STORAGE_TYPE_VALUE_ANONCREDS + ): + LOGGER.warning( + "The agent has been upgrade to use anoncreds wallet. Please update the wallet.type in the config file to 'askar-anoncreds'" # noqa: E501 + ) + # Allow agent to create anoncreds profile with askar + # wallet type config by stopping conductor and reloading context + await self.stop() + self.force_agent_anoncreds = True + self.context.settings.set_value("wallet.type", "askar-anoncreds") + self.context_builder = DefaultContextBuilder(self.context.settings) + await self.setup() + else: + raise StartupError( + f"Wallet type config [{storage_type_from_config}] doesn't match with the wallet type in storage [{storage_type_record.value}]" # noqa: E501 + ) + + async def check_for_wallet_upgrades_in_progress(self): + """Check for upgrade and upgrade if needed.""" + multitenant_mgr = self.context.inject_or(BaseMultitenantManager) + if multitenant_mgr: + subwallet_profiles = await get_subwallet_profiles_from_storage( + self.root_profile + ) + await asyncio.gather( + *[ + upgrade_wallet_to_anoncreds_if_requested(profile, is_subwallet=True) + for profile in subwallet_profiles + ] + ) + + else: + await upgrade_wallet_to_anoncreds_if_requested(self.root_profile) diff --git a/aries_cloudagent/core/in_memory/profile.py b/aries_cloudagent/core/in_memory/profile.py index 28e5712bdd..7473f57e3d 100644 --- a/aries_cloudagent/core/in_memory/profile.py +++ b/aries_cloudagent/core/in_memory/profile.py @@ -97,10 +97,17 @@ def test_profile( @classmethod def test_session( - cls, settings: Mapping[str, Any] = None, bind: Mapping[Type, Any] = None + cls, + settings: Mapping[str, Any] = None, + bind: Mapping[Type, Any] = None, + profile_class: Any = None, ) -> "InMemoryProfileSession": """Used in tests to quickly create InMemoryProfileSession.""" - session = InMemoryProfileSession(cls.test_profile(), settings=settings) + if profile_class is not None: + test_profile = cls.test_profile(profile_class=profile_class) + else: + test_profile = cls.test_profile() + session = InMemoryProfileSession(test_profile, settings=settings) session._active = True session._init_context() if bind: diff --git a/aries_cloudagent/core/profile.py b/aries_cloudagent/core/profile.py index 7b2b2f50da..d3bdfe2aeb 100644 --- a/aries_cloudagent/core/profile.py +++ b/aries_cloudagent/core/profile.py @@ -331,7 +331,6 @@ class ProfileManagerProvider(BaseProvider): "askar": "aries_cloudagent.askar.profile.AskarProfileManager", "askar-anoncreds": "aries_cloudagent.askar.profile_anon.AskarAnonProfileManager", "in_memory": "aries_cloudagent.core.in_memory.InMemoryProfileManager", - "indy": "aries_cloudagent.indy.sdk.profile.IndySdkProfileManager", } def __init__(self): diff --git a/aries_cloudagent/core/tests/test_conductor.py b/aries_cloudagent/core/tests/test_conductor.py index 55a8ab0c4b..685e420005 100644 --- a/aries_cloudagent/core/tests/test_conductor.py +++ b/aries_cloudagent/core/tests/test_conductor.py @@ -117,6 +117,8 @@ async def test_startup_version_record_exists(self): ) as mock_outbound_mgr, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -166,6 +168,7 @@ async def test_startup_version_record_exists(self): mock_inbound_mgr.return_value.stop.assert_awaited_once_with() mock_outbound_mgr.return_value.stop.assert_awaited_once_with() + assert mock_upgrade.called async def test_startup_version_no_upgrade_add_record(self): builder: ContextBuilder = StubContextBuilder(self.test_settings) @@ -176,6 +179,8 @@ async def test_startup_version_no_upgrade_add_record(self): ) as mock_inbound_mgr, mock.patch.object( test_module, "OutboundTransportManager", autospec=True ) as mock_outbound_mgr, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -213,6 +218,8 @@ async def test_startup_version_no_upgrade_add_record(self): ) as mock_inbound_mgr, mock.patch.object( test_module, "OutboundTransportManager", autospec=True ) as mock_outbound_mgr, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -257,6 +264,8 @@ async def test_startup_version_force_upgrade(self): ) as mock_outbound_mgr, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -296,6 +305,8 @@ async def test_startup_version_force_upgrade(self): ) as mock_outbound_mgr, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -335,6 +346,8 @@ async def test_startup_version_force_upgrade(self): ) as mock_outbound_mgr, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -373,6 +386,8 @@ async def test_startup_version_record_not_exists(self): ) as mock_outbound_mgr, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -449,6 +464,8 @@ async def test_startup_no_public_did(self): ) as mock_outbound_mgr, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -492,6 +509,8 @@ async def test_stats(self): ) as mock_inbound_mgr, mock.patch.object( test_module, "OutboundTransportManager", autospec=True ) as mock_outbound_mgr, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger: mock_inbound_mgr.return_value.sessions = ["dummy"] @@ -884,6 +903,8 @@ async def test_admin(self): ) as admin_start, mock.patch.object( admin, "stop", autospec=True ) as admin_stop, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -936,6 +957,8 @@ async def test_admin_startx(self): ) as oob_mgr, mock.patch.object( test_module, "ConnectionManager" ) as conn_mgr, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -992,7 +1015,9 @@ async def test_start_static(self): ), ), mock.patch.object( test_module, "OutboundTransportManager", autospec=True - ) as mock_outbound_mgr: + ) as mock_outbound_mgr, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade: mock_outbound_mgr.return_value.registered_transports = { "test": mock.MagicMock(schemes=["http"]) } @@ -1166,7 +1191,9 @@ async def test_print_invite_connection(self): ), ), mock.patch.object( test_module, "OutboundTransportManager", autospec=True - ) as mock_outbound_mgr: + ) as mock_outbound_mgr, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade: mock_outbound_mgr.return_value.registered_transports = { "test": mock.MagicMock(schemes=["http"]) } @@ -1203,6 +1230,8 @@ async def test_clear_default_mediator(self): "MediationManager", return_value=mock.MagicMock(clear_default_mediator=mock.CoroutineMock()), ) as mock_mgr, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -1254,7 +1283,9 @@ async def test_set_default_mediator(self): mock.MagicMock(value=f"v{__version__}"), ] ), - ): + ), mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade: await conductor.start() await conductor.stop() mock_mgr.return_value.set_default_mediator_by_id.assert_called_once() @@ -1277,6 +1308,8 @@ async def test_set_default_mediator_x(self): "retrieve_by_id", mock.CoroutineMock(side_effect=Exception()), ), mock.patch.object(test_module, "LOGGER") as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -1425,6 +1458,8 @@ async def test_mediator_invitation_0160(self, mock_from_url, _): ) as mock_mgr, mock.patch.object( mock_conn_record, "metadata_set", mock.CoroutineMock() ), mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -1484,6 +1519,8 @@ async def test_mediator_invitation_0434(self, mock_from_url, _): ) ), ) as mock_mgr, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -1542,6 +1579,8 @@ async def test_mediation_invitation_should_use_stored_invitation( ), mock.patch.object( test_module, "MediationManager", return_value=mock_mediation_manager ), mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -1596,7 +1635,9 @@ async def test_mediation_invitation_should_not_create_connection_for_old_invitat mock.MagicMock(value=f"v{__version__}"), ] ), - ): + ), mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade: # when await conductor.start() await conductor.stop() @@ -1631,6 +1672,8 @@ async def test_mediator_invitation_x(self, _): ) as mock_from_url, mock.patch.object( test_module, "LOGGER" ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -1694,6 +1737,8 @@ async def test_startup_x_no_storage_version(self): ) as mock_outbound_mgr, mock.patch.object( test_module, "LOGGER" ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -1735,6 +1780,8 @@ async def test_startup_storage_type_exists_and_matches(self): ) as mock_outbound_mgr, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -1774,7 +1821,7 @@ async def test_startup_storage_type_exists_and_matches(self): await conductor.stop() - async def test_startup_storage_type_exists_and_does_not_match(self): + async def test_startup_storage_type_anoncreds_and_config_askar_re_calls_setup(self): builder: ContextBuilder = StubContextBuilder(self.test_settings) conductor = test_module.Conductor(builder) @@ -1785,6 +1832,8 @@ async def test_startup_storage_type_exists_and_does_not_match(self): ) as mock_outbound_mgr, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -1819,9 +1868,9 @@ async def test_startup_storage_type_exists_and_does_not_match(self): mock_inbound_mgr.return_value.registered_transports = {} mock_outbound_mgr.return_value.registered_transports = {} - - with self.assertRaises(test_module.StartupError): + with mock.patch.object(test_module.Conductor, "setup") as mock_setup: await conductor.start() + assert mock_setup.called await conductor.stop() @@ -1838,6 +1887,8 @@ async def test_startup_storage_type_does_not_exist_and_existing_agent_then_set_t ) as mock_outbound_mgr, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( @@ -1902,6 +1953,8 @@ async def test_startup_storage_type_does_not_exist_and_new_anoncreds_agent( ) as mock_outbound_mgr, mock.patch.object( test_module, "LoggingConfigurator", autospec=True ) as mock_logger, mock.patch.object( + test_module, "upgrade_wallet_to_anoncreds_if_requested", return_value=False + ) as mock_upgrade, mock.patch.object( BaseStorage, "find_record", mock.CoroutineMock( diff --git a/aries_cloudagent/holder/routes.py b/aries_cloudagent/holder/routes.py index 7b7ac9a198..97d354497d 100644 --- a/aries_cloudagent/holder/routes.py +++ b/aries_cloudagent/holder/routes.py @@ -10,9 +10,9 @@ request_schema, response_schema, ) - from marshmallow import fields +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..indy.holder import IndyHolder, IndyHolderError from ..indy.models.cred_precis import IndyCredInfoSchema @@ -193,6 +193,7 @@ class CredRevokedResultSchema(OpenAPISchema): @docs(tags=["credentials"], summary="Fetch credential from wallet by id") @match_info_schema(HolderCredIdMatchInfoSchema()) @response_schema(IndyCredInfoSchema(), 200, description="") +@tenant_authentication async def credentials_get(request: web.BaseRequest): """Request handler for retrieving credential. @@ -220,6 +221,7 @@ async def credentials_get(request: web.BaseRequest): @match_info_schema(HolderCredIdMatchInfoSchema()) @querystring_schema(CredRevokedQueryStringSchema()) @response_schema(CredRevokedResultSchema(), 200, description="") +@tenant_authentication async def credentials_revoked(request: web.BaseRequest): """Request handler for querying revocation status of credential. @@ -263,6 +265,7 @@ async def credentials_revoked(request: web.BaseRequest): @docs(tags=["credentials"], summary="Get attribute MIME types from wallet") @match_info_schema(HolderCredIdMatchInfoSchema()) @response_schema(AttributeMimeTypesResultSchema(), 200, description="") +@tenant_authentication async def credentials_attr_mime_types_get(request: web.BaseRequest): """Request handler for getting credential attribute MIME types. @@ -285,6 +288,7 @@ async def credentials_attr_mime_types_get(request: web.BaseRequest): @docs(tags=["credentials"], summary="Remove credential from wallet by id") @match_info_schema(HolderCredIdMatchInfoSchema()) @response_schema(HolderModuleResponseSchema(), description="") +@tenant_authentication async def credentials_remove(request: web.BaseRequest): """Request handler for searching connection records. @@ -316,6 +320,7 @@ async def credentials_remove(request: web.BaseRequest): ) @querystring_schema(CredentialsListQueryStringSchema()) @response_schema(CredInfoListSchema(), 200, description="") +@tenant_authentication async def credentials_list(request: web.BaseRequest): """Request handler for searching credential records. @@ -354,6 +359,7 @@ async def credentials_list(request: web.BaseRequest): ) @match_info_schema(HolderCredIdMatchInfoSchema()) @response_schema(VCRecordSchema(), 200, description="") +@tenant_authentication async def w3c_cred_get(request: web.BaseRequest): """Request handler for retrieving W3C credential. @@ -385,6 +391,7 @@ async def w3c_cred_get(request: web.BaseRequest): ) @match_info_schema(HolderCredIdMatchInfoSchema()) @response_schema(HolderModuleResponseSchema(), 200, description="") +@tenant_authentication async def w3c_cred_remove(request: web.BaseRequest): """Request handler for deleting W3C credential. @@ -422,6 +429,7 @@ async def w3c_cred_remove(request: web.BaseRequest): @request_schema(W3CCredentialsListRequestSchema()) @querystring_schema(CredentialsListQueryStringSchema()) @response_schema(VCRecordListSchema(), 200, description="") +@tenant_authentication async def w3c_creds_list(request: web.BaseRequest): """Request handler for searching W3C credential records. diff --git a/aries_cloudagent/holder/tests/test_routes.py b/aries_cloudagent/holder/tests/test_routes.py index 88323ce8e0..be0f941d07 100644 --- a/aries_cloudagent/holder/tests/test_routes.py +++ b/aries_cloudagent/holder/tests/test_routes.py @@ -1,15 +1,13 @@ import json +from unittest import IsolatedAsyncioTestCase from aries_cloudagent.tests import mock -from unittest import IsolatedAsyncioTestCase from ...core.in_memory import InMemoryProfile -from ...ledger.base import BaseLedger - from ...indy.holder import IndyHolder +from ...ledger.base import BaseLedger from ...storage.vc_holder.base import VCHolder from ...storage.vc_holder.vc_record import VCRecord - from .. import routes as test_module VC_RECORD = VCRecord( @@ -33,7 +31,11 @@ class TestHolderRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.context = self.profile.context setattr(self.context, "profile", self.profile) @@ -43,6 +45,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_credentials_get(self): diff --git a/aries_cloudagent/indy/models/tests/test_pres_preview.py b/aries_cloudagent/indy/models/tests/test_pres_preview.py index 8f1d94614d..fc03114f5a 100644 --- a/aries_cloudagent/indy/models/tests/test_pres_preview.py +++ b/aries_cloudagent/indy/models/tests/test_pres_preview.py @@ -5,7 +5,6 @@ from time import time from unittest import TestCase -from unittest import IsolatedAsyncioTestCase from aries_cloudagent.tests import mock from ....core.in_memory import InMemoryProfile @@ -350,8 +349,7 @@ def test_eq(self): assert pred_spec_a != pred_spec_b -@pytest.mark.indy -class TestIndyPresPreviewAsync(IsolatedAsyncioTestCase): +class TestIndyPresPreviewAsync: """Presentation preview tests""" @pytest.mark.asyncio @@ -503,7 +501,6 @@ async def test_satisfaction(self): assert not attr_spec.satisfies(pred_spec) -@pytest.mark.indy class TestIndyPresPreview(TestCase): """Presentation preview tests""" diff --git a/aries_cloudagent/indy/sdk/error.py b/aries_cloudagent/indy/sdk/error.py deleted file mode 100644 index a79e0a6194..0000000000 --- a/aries_cloudagent/indy/sdk/error.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Indy error handling.""" - -from typing import Type - -from indy.error import IndyError - -from ...core.error import BaseError - - -class IndyErrorHandler: - """Trap IndyError and raise an appropriate LedgerError instead.""" - - def __init__(self, message: str = None, error_cls: Type[BaseError] = BaseError): - """Init the context manager.""" - self.error_cls = error_cls - self.message = message - - def __enter__(self): - """Enter the context manager.""" - return self - - def __exit__(self, err_type, err_value, err_traceback): - """Exit the context manager.""" - if isinstance(err_value, IndyError): - raise IndyErrorHandler.wrap_error( - err_value, self.message, self.error_cls - ) from err_value - - @classmethod - def wrap_error( - cls, - err_value: IndyError, - message: str = None, - error_cls: Type[BaseError] = BaseError, - ) -> BaseError: - """Create an instance of BaseError from an IndyError.""" - err_msg = message or "Exception while performing indy operation" - indy_message = hasattr(err_value, "message") and err_value.message - if indy_message: - err_msg += f": {indy_message}" - err = error_cls(err_msg) - err.__traceback__ = err_value.__traceback__ - return err diff --git a/aries_cloudagent/indy/sdk/holder.py b/aries_cloudagent/indy/sdk/holder.py deleted file mode 100644 index 77e8f54256..0000000000 --- a/aries_cloudagent/indy/sdk/holder.py +++ /dev/null @@ -1,468 +0,0 @@ -"""Indy SDK holder implementation.""" - -import json -import logging -import re -from collections import OrderedDict -from typing import Optional, Sequence, Tuple, Union - -import indy.anoncreds -from indy.error import ErrorCode, IndyError - -from ...indy.sdk.wallet_setup import IndyOpenWallet -from ...ledger.base import BaseLedger -from ...storage.error import StorageError, StorageNotFoundError -from ...storage.indy import IndySdkStorage -from ...storage.record import StorageRecord -from ...wallet.error import WalletNotFoundError -from ..holder import IndyHolder, IndyHolderError -from .error import IndyErrorHandler -from .util import create_tails_reader - -LOGGER = logging.getLogger(__name__) - - -class IndySdkHolder(IndyHolder): - """Indy-SDK holder implementation.""" - - def __init__(self, wallet: IndyOpenWallet): - """Initialize an IndyHolder instance. - - Args: - wallet: IndyOpenWallet instance - - """ - self.wallet = wallet - - async def create_credential_request( - self, credential_offer: dict, credential_definition: dict, holder_did: str - ) -> Tuple[str, str]: - """Create a credential request for the given credential offer. - - Args: - credential_offer: The credential offer to create request for - credential_definition: The credential definition to create an offer for - holder_did: the DID of the agent making the request - - Returns: - A tuple of the credential request and credential request metadata - - """ - - with IndyErrorHandler( - "Error when creating credential request", IndyHolderError - ): - ( - credential_request_json, - credential_request_metadata_json, - ) = await indy.anoncreds.prover_create_credential_req( - self.wallet.handle, - holder_did, - json.dumps(credential_offer), - json.dumps(credential_definition), - self.wallet.master_secret_id, - ) - - LOGGER.debug( - "Created credential request. " - "credential_request_json=%s credential_request_metadata_json=%s", - credential_request_json, - credential_request_metadata_json, - ) - - return credential_request_json, credential_request_metadata_json - - async def store_credential( - self, - credential_definition: dict, - credential_data: dict, - credential_request_metadata: dict, - credential_attr_mime_types=None, - credential_id: str = None, - rev_reg_def: dict = None, - ) -> str: - """Store a credential in the wallet. - - Args: - credential_definition: Credential definition for this credential - credential_data: Credential data generated by the issuer - credential_request_metadata: credential request metadata generated - by the issuer - credential_attr_mime_types: dict mapping attribute names to (optional) - MIME types to store as non-secret record, if specified - credential_id: optionally override the stored credential id - rev_reg_def: revocation registry definition in json - - Returns: - the ID of the stored credential - - """ - with IndyErrorHandler( - "Error when storing credential in wallet", IndyHolderError - ): - credential_id = await indy.anoncreds.prover_store_credential( - wallet_handle=self.wallet.handle, - cred_id=credential_id, - cred_req_metadata_json=json.dumps(credential_request_metadata), - cred_json=json.dumps(credential_data), - cred_def_json=json.dumps(credential_definition), - rev_reg_def_json=json.dumps(rev_reg_def) if rev_reg_def else None, - ) - - if credential_attr_mime_types: - mime_types = { - attr: credential_attr_mime_types.get(attr) - for attr in credential_data["values"] - if attr in credential_attr_mime_types - } - if mime_types: - record = StorageRecord( - type=IndyHolder.RECORD_TYPE_MIME_TYPES, - value=credential_id, - tags=mime_types, - id=f"{IndyHolder.RECORD_TYPE_MIME_TYPES}::{credential_id}", - ) - indy_stor = IndySdkStorage(self.wallet) - await indy_stor.add_record(record) - - return credential_id - - async def get_credentials(self, start: int, count: int, wql: dict): - """Get credentials stored in the wallet. - - Args: - start: Starting index - count: Number of records to return - wql: wql query dict - - """ - - async def fetch(limit): - """Fetch up to limit (default smaller of all remaining or 256) creds.""" - creds = [] - CHUNK = min(record_count, limit or record_count, IndyHolder.CHUNK) - cardinality = min(limit or record_count, record_count) - - with IndyErrorHandler( - "Error fetching credentials from wallet", IndyHolderError - ): - while len(creds) < cardinality: - batch = json.loads( - await indy.anoncreds.prover_fetch_credentials( - search_handle, CHUNK - ) - ) - creds.extend(batch) - if len(batch) < CHUNK: - break - return creds - - with IndyErrorHandler( - "Error when constructing wallet credential query", IndyHolderError - ): - ( - search_handle, - record_count, - ) = await indy.anoncreds.prover_search_credentials( - self.wallet.handle, json.dumps(wql) - ) - - if start > 0: - # must move database cursor manually - await fetch(start) - credentials = await fetch(count) - - await indy.anoncreds.prover_close_credentials_search(search_handle) - - return credentials - - async def get_credentials_for_presentation_request_by_referent( - self, - presentation_request: dict, - referents: Sequence[str], - start: int, - count: int, - extra_query: Optional[dict] = None, - ): - """Get credentials stored in the wallet. - - Args: - presentation_request: Valid presentation request from issuer - referents: Presentation request referents to use to search for creds - start: Starting index - count: Maximum number of records to return - extra_query: wql query dict - - """ - - async def fetch(reft, limit): - """Fetch up to limit (default smaller of all remaining or 256) creds.""" - creds = [] - CHUNK = min(IndyHolder.CHUNK, limit or IndyHolder.CHUNK) - - with IndyErrorHandler( - "Error fetching credentials from wallet for presentation request", - IndyHolderError, - ): - while not limit or len(creds) < limit: - batch = json.loads( - await indy.anoncreds.prover_fetch_credentials_for_proof_req( - search_handle, reft, CHUNK - ) - ) - creds.extend(batch) - if len(batch) < CHUNK: - break - return creds - - with IndyErrorHandler( - "Error when constructing wallet credential query", IndyHolderError - ): - search_handle = ( - await ( - indy.anoncreds.prover_search_credentials_for_proof_req( - self.wallet.handle, - json.dumps(presentation_request), - json.dumps(extra_query), - ) - ) - ) - - if not referents: - referents = ( - *presentation_request["requested_attributes"], - *presentation_request["requested_predicates"], - ) - creds_dict = OrderedDict() - - try: - for reft in referents: - # must move database cursor manually - if start > 0: - await fetch(reft, start) - credentials = await fetch(reft, count) - - for cred in credentials: - cred_id = cred["cred_info"]["referent"] - if cred_id not in creds_dict: - cred["presentation_referents"] = {reft} - creds_dict[cred_id] = cred - else: - creds_dict[cred_id]["presentation_referents"].add(reft) - finally: - # Always close - await indy.anoncreds.prover_close_credentials_search_for_proof_req( - search_handle - ) - - for cred in creds_dict.values(): - cred["presentation_referents"] = list(cred["presentation_referents"]) - - creds_ordered = tuple( - sorted( - creds_dict.values(), - key=lambda c: ( - c["cred_info"]["rev_reg_id"] or "", # irrevocable 1st - c["cred_info"][ - "referent" - ], # should be descending by timestamp if we had it - ), - ) - )[:count] - return creds_ordered - - async def get_credential(self, credential_id: str) -> str: - """Get a credential stored in the wallet. - - Args: - credential_id: Credential id to retrieve - - """ - try: - credential_json = await indy.anoncreds.prover_get_credential( - self.wallet.handle, credential_id - ) - except IndyError as err: - if err.error_code == ErrorCode.WalletItemNotFound: - raise WalletNotFoundError( - "Credential {} not found in wallet {}".format( - credential_id, self.wallet.name - ) - ) - else: - raise IndyErrorHandler.wrap_error( - err, - f"Error when fetching credential {credential_id}", - IndyHolderError, - ) from err - - return credential_json - - async def credential_revoked( - self, ledger: BaseLedger, credential_id: str, fro: int = None, to: int = None - ) -> bool: - """Check ledger for revocation status of credential by cred id. - - Args: - credential_id: Credential id to check - - """ - cred = json.loads(await self.get_credential(credential_id)) - rev_reg_id = cred["rev_reg_id"] - - if rev_reg_id: - cred_rev_id = int(cred["cred_rev_id"]) - (rev_reg_delta, _) = await ledger.get_revoc_reg_delta( - rev_reg_id, - fro, - to, - ) - - return cred_rev_id in rev_reg_delta["value"].get("revoked", []) - else: - return False - - async def delete_credential(self, credential_id: str): - """Remove a credential stored in the wallet. - - Args: - credential_id: Credential id to remove - - """ - try: - indy_stor = IndySdkStorage(self.wallet) - mime_types_record = await indy_stor.get_record( - IndyHolder.RECORD_TYPE_MIME_TYPES, - f"{IndyHolder.RECORD_TYPE_MIME_TYPES}::{credential_id}", - ) - await indy_stor.delete_record(mime_types_record) - except StorageNotFoundError: - pass # MIME types record not present: carry on - - try: - await indy.anoncreds.prover_delete_credential( - self.wallet.handle, credential_id - ) - except IndyError as err: - if err.error_code == ErrorCode.WalletItemNotFound: - raise WalletNotFoundError( - "Credential {} not found in wallet {}".format( - credential_id, self.wallet.name - ) - ) - else: - raise IndyErrorHandler.wrap_error( - err, "Error when deleting credential", IndyHolderError - ) from err - - async def get_mime_type( - self, credential_id: str, attr: str = None - ) -> Union[dict, str]: - """Get MIME type per attribute (or for all attributes). - - Args: - credential_id: credential id - attr: attribute of interest or omit for all - - Returns: Attribute MIME type or dict mapping attribute names to MIME types - attr_meta_json = all_meta.tags.get(attr) - - """ - try: - mime_types_record = await IndySdkStorage(self.wallet).get_record( - IndyHolder.RECORD_TYPE_MIME_TYPES, - f"{IndyHolder.RECORD_TYPE_MIME_TYPES}::{credential_id}", - ) - except StorageError: - return None # no MIME types: not an error - - return mime_types_record.tags.get(attr) if attr else mime_types_record.tags - - async def create_presentation( - self, - presentation_request: dict, - requested_credentials: dict, - schemas: dict, - credential_definitions: dict, - rev_states: dict = None, - ) -> str: - """Get credentials stored in the wallet. - - Args: - presentation_request: Valid indy format presentation request - requested_credentials: Indy format requested credentials - schemas: Indy formatted schemas JSON - credential_definitions: Indy formatted credential definitions JSON - rev_states: Indy format revocation states JSON - - """ - - for reft, spec in presentation_request.get("requested_attributes", {}).items(): - for r in spec.get("restrictions", []): - for k in r: - m = re.match("^attr::(.*)::value$", k) - if not m: - continue - - named_attrs = ( - [spec["name"]] if "name" in spec else spec.get("names", []) - ) - restricted_attr = m.group(1) - if m and restricted_attr not in named_attrs: # wrong attr: hopeless - LOGGER.error( - f"Presentation request {presentation_request['nonce']} " - f"requested attribute {reft} names {named_attrs} " - f"but restricts {restricted_attr} value" - ) - raise IndyHolderError( - f"Requested attribute {reft} names {named_attrs} " - f"but restricts {restricted_attr} value" - ) - - with IndyErrorHandler("Error when constructing proof", IndyHolderError): - presentation_json = await indy.anoncreds.prover_create_proof( - self.wallet.handle, - json.dumps(presentation_request), - json.dumps(requested_credentials), - self.wallet.master_secret_id, - json.dumps(schemas), - json.dumps(credential_definitions), - json.dumps(rev_states) if rev_states else "{}", - ) - - return presentation_json - - async def create_revocation_state( - self, - cred_rev_id: str, - rev_reg_def: dict, - rev_reg_delta: dict, - timestamp: int, - tails_file_path: str, - ) -> str: - """Create current revocation state for a received credential. - - Args: - cred_rev_id: credential revocation id in revocation registry - rev_reg_def: revocation registry definition - rev_reg_delta: revocation delta - timestamp: delta timestamp - - Returns: - the revocation state - - """ - - with IndyErrorHandler( - "Error when constructing revocation state", IndyHolderError - ): - tails_file_reader = await create_tails_reader(tails_file_path) - rev_state_json = await indy.anoncreds.create_revocation_state( - tails_file_reader, - rev_reg_def_json=json.dumps(rev_reg_def), - cred_rev_id=cred_rev_id, - rev_reg_delta_json=json.dumps(rev_reg_delta), - timestamp=timestamp, - ) - - return rev_state_json diff --git a/aries_cloudagent/indy/sdk/issuer.py b/aries_cloudagent/indy/sdk/issuer.py deleted file mode 100644 index 8ac62b8637..0000000000 --- a/aries_cloudagent/indy/sdk/issuer.py +++ /dev/null @@ -1,378 +0,0 @@ -"""Indy SDK issuer implementation.""" - -import json -import logging -from typing import Sequence, Tuple - -import indy.anoncreds -import indy.blob_storage -from indy.error import AnoncredsRevocationRegistryFullError, IndyError, ErrorCode - -from ...indy.sdk.profile import IndySdkProfile -from ...messaging.util import encode -from ...storage.error import StorageError - -from ..issuer import ( - IndyIssuer, - IndyIssuerError, - IndyIssuerRevocationRegistryFullError, - DEFAULT_CRED_DEF_TAG, - DEFAULT_SIGNATURE_TYPE, -) - -from .error import IndyErrorHandler -from .util import create_tails_reader, create_tails_writer - -LOGGER = logging.getLogger(__name__) - - -class IndySdkIssuer(IndyIssuer): - """Indy-SDK issuer implementation.""" - - def __init__(self, profile: IndySdkProfile): - """Initialize an IndyIssuer instance. - - Args: - profile: IndySdkProfile instance - - """ - self.profile = profile - - async def create_schema( - self, - origin_did: str, - schema_name: str, - schema_version: str, - attribute_names: Sequence[str], - ) -> Tuple[str, str]: - """Create a new credential schema. - - Args: - origin_did: the DID issuing the credential definition - schema_name: the schema name - schema_version: the schema version - attribute_names: a sequence of schema attribute names - - Returns: - A tuple of the schema ID and JSON - - """ - - with IndyErrorHandler("Error when creating schema", IndyIssuerError): - schema_id, schema_json = await indy.anoncreds.issuer_create_schema( - origin_did, - schema_name, - schema_version, - json.dumps(attribute_names), - ) - return (schema_id, schema_json) - - async def credential_definition_in_wallet( - self, credential_definition_id: str - ) -> bool: - """Check whether a given credential definition ID is present in the wallet. - - Args: - credential_definition_id: The credential definition ID to check - """ - try: - await indy.anoncreds.issuer_create_credential_offer( - self.profile.wallet.handle, credential_definition_id - ) - return True - except IndyError as err: - if err.error_code not in ( - ErrorCode.CommonInvalidStructure, - ErrorCode.WalletItemNotFound, - ): - raise IndyErrorHandler.wrap_error( - err, - "Error when checking wallet for credential definition", - IndyIssuerError, - ) from err - # recognized error signifies no such cred def in wallet: pass - return False - - async def create_and_store_credential_definition( - self, - origin_did: str, - schema: dict, - signature_type: str = None, - tag: str = None, - support_revocation: bool = False, - ) -> Tuple[str, str]: - """Create a new credential definition and store it in the wallet. - - Args: - origin_did: the DID issuing the credential definition - schema: the schema used as a basis - signature_type: the credential definition signature type (default 'CL') - tag: the credential definition tag - support_revocation: whether to enable revocation for this credential def - - Returns: - A tuple of the credential definition ID and JSON - - """ - - with IndyErrorHandler( - "Error when creating credential definition", IndyIssuerError - ): - ( - credential_definition_id, - credential_definition_json, - ) = await indy.anoncreds.issuer_create_and_store_credential_def( - self.profile.wallet.handle, - origin_did, - json.dumps(schema), - tag or DEFAULT_CRED_DEF_TAG, - signature_type or DEFAULT_SIGNATURE_TYPE, - json.dumps({"support_revocation": support_revocation}), - ) - return (credential_definition_id, credential_definition_json) - - async def create_credential_offer(self, credential_definition_id: str) -> str: - """Create a credential offer for the given credential definition id. - - Args: - credential_definition_id: The credential definition to create an offer for - - Returns: - The created credential offer - - """ - with IndyErrorHandler( - "Exception when creating credential offer", IndyIssuerError - ): - credential_offer_json = await indy.anoncreds.issuer_create_credential_offer( - self.profile.wallet.handle, credential_definition_id - ) - - return credential_offer_json - - async def create_credential( - self, - schema: dict, - credential_offer: dict, - credential_request: dict, - credential_values: dict, - rev_reg_id: str = None, - tails_file_path: str = None, - ) -> Tuple[str, str]: - """Create a credential. - - Args: - schema: Schema to create credential for - credential_offer: Credential Offer to create credential for - credential_request: Credential request to create credential for - credential_values: Values to go in credential - rev_reg_id: ID of the revocation registry - tails_file_path: Path to the local tails file - - Returns: - A tuple of created credential and revocation id - - """ - - encoded_values = {} - schema_attributes = schema["attrNames"] - for attribute in schema_attributes: - # Ensure every attribute present in schema to be set. - # Extraneous attribute names are ignored. - try: - credential_value = credential_values[attribute] - except KeyError: - raise IndyIssuerError( - "Provided credential values are missing a value " - + f"for the schema attribute '{attribute}'" - ) - - encoded_values[attribute] = {} - encoded_values[attribute]["raw"] = str(credential_value) - encoded_values[attribute]["encoded"] = encode(credential_value) - - tails_reader_handle = ( - await create_tails_reader(tails_file_path) - if tails_file_path is not None - else None - ) - - try: - ( - credential_json, - cred_rev_id, - _, # rev_reg_delta_json only for ISSUANCE_ON_DEMAND, excluded by design - ) = await indy.anoncreds.issuer_create_credential( - self.profile.wallet.handle, - json.dumps(credential_offer), - json.dumps(credential_request), - json.dumps(encoded_values), - rev_reg_id, - tails_reader_handle, - ) - except AnoncredsRevocationRegistryFullError: - LOGGER.warning( - "Revocation registry %s is full: cannot create credential", - rev_reg_id, - ) - raise IndyIssuerRevocationRegistryFullError( - f"Revocation registry {rev_reg_id} is full" - ) - except IndyError as err: - raise IndyErrorHandler.wrap_error( - err, "Error when issuing credential", IndyIssuerError - ) from err - except StorageError as err: - LOGGER.warning( - ( - "Created issuer cred rev record for " - "Could not store issuer cred rev record for " - "rev reg id %s, cred rev id %s: %s" - ), - rev_reg_id, - cred_rev_id, - err.roll_up, - ) - - return (credential_json, cred_rev_id) - - async def revoke_credentials( - self, - cred_def_id: str, - rev_reg_id: str, - tails_file_path: str, - cred_rev_ids: Sequence[str], - ) -> Tuple[str, Sequence[str]]: - """Revoke a set of credentials in a revocation registry. - - Args: - cred_def_id: ID of the credential definition - rev_reg_id: ID of the revocation registry - tails_file_path: path to the local tails file - cred_rev_ids: sequences of credential indexes in the revocation registry - - Returns: - Tuple with the combined revocation delta, list of cred rev ids not revoked - - """ - failed_crids = set() - tails_reader_handle = await create_tails_reader(tails_file_path) - - result_json = None - for cred_rev_id in set(cred_rev_ids): - with IndyErrorHandler( - "Exception when revoking credential", IndyIssuerError - ): - try: - delta_json = await indy.anoncreds.issuer_revoke_credential( - self.profile.wallet.handle, - tails_reader_handle, - rev_reg_id, - cred_rev_id, - ) - except IndyError as err: - if err.error_code == ErrorCode.AnoncredsInvalidUserRevocId: - LOGGER.error( - ( - "Abstaining from revoking credential on " - "rev reg id %s, cred rev id=%s: " - "already revoked or not yet issued" - ), - rev_reg_id, - cred_rev_id, - ) - else: - LOGGER.error( - IndyErrorHandler.wrap_error( - err, "Revocation error", IndyIssuerError - ).roll_up - ) - failed_crids.add(int(cred_rev_id)) - continue - except StorageError as err: - LOGGER.warning( - ( - "Revoked credential on rev reg id %s, cred rev id %s " - "without corresponding issuer cred rev record: %s" - ), - rev_reg_id, - cred_rev_id, - err.roll_up, - ) - # carry on with delta merge; record is best-effort - - if result_json: - result_json = await self.merge_revocation_registry_deltas( - result_json, delta_json - ) - else: - result_json = delta_json - - return (result_json, [str(rev_id) for rev_id in sorted(failed_crids)]) - - async def merge_revocation_registry_deltas( - self, fro_delta: str, to_delta: str - ) -> str: - """Merge revocation registry deltas. - - Args: - fro_delta: original delta in JSON format - to_delta: incoming delta in JSON format - - Returns: - Merged delta in JSON format - - """ - - return await indy.anoncreds.issuer_merge_revocation_registry_deltas( - fro_delta, to_delta - ) - - async def create_and_store_revocation_registry( - self, - origin_did: str, - cred_def_id: str, - revoc_def_type: str, - tag: str, - max_cred_num: int, - tails_base_path: str, - ) -> Tuple[str, str, str]: - """Create a new revocation registry and store it in the wallet. - - Args: - origin_did: the DID issuing the revocation registry - cred_def_id: the identifier of the related credential definition - revoc_def_type: the revocation registry type (default CL_ACCUM) - tag: the unique revocation registry tag - max_cred_num: the number of credentials supported in the registry - tails_base_path: where to store the tails file - - Returns: - A tuple of the revocation registry ID, JSON, and entry JSON - - """ - - tails_writer = await create_tails_writer(tails_base_path) - - with IndyErrorHandler( - "Exception when creating revocation registry", IndyIssuerError - ): - ( - rev_reg_id, - rev_reg_def_json, - rev_reg_entry_json, - ) = await indy.anoncreds.issuer_create_and_store_revoc_reg( - self.profile.wallet.handle, - origin_did, - revoc_def_type, - tag, - cred_def_id, - json.dumps( - { - "issuance_type": "ISSUANCE_BY_DEFAULT", - "max_cred_num": max_cred_num, - } - ), - tails_writer, - ) - return (rev_reg_id, rev_reg_def_json, rev_reg_entry_json) diff --git a/aries_cloudagent/indy/sdk/profile.py b/aries_cloudagent/indy/sdk/profile.py deleted file mode 100644 index 71b6530b38..0000000000 --- a/aries_cloudagent/indy/sdk/profile.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Manage Indy-SDK profile interaction.""" - -import asyncio -import logging -from typing import Any, Mapping -import warnings -from weakref import finalize, ref - -from ...cache.base import BaseCache -from ...config.injection_context import InjectionContext -from ...config.provider import ClassProvider -from ...core.error import ProfileError -from ...core.profile import Profile, ProfileManager, ProfileSession -from ...ledger.base import BaseLedger -from ...ledger.indy import IndySdkLedger, IndySdkLedgerPool -from ...storage.base import BaseStorage, BaseStorageSearch -from ...storage.vc_holder.base import VCHolder -from ...utils.multi_ledger import get_write_ledger_config_for_profile -from ...wallet.base import BaseWallet -from ...wallet.indy import IndySdkWallet -from ..holder import IndyHolder -from ..issuer import IndyIssuer -from ..verifier import IndyVerifier -from .wallet_setup import IndyOpenWallet, IndyWalletConfig - -LOGGER = logging.getLogger(__name__) - - -class IndySdkProfile(Profile): - """Provide access to Indy profile interaction methods.""" - - BACKEND_NAME = "indy" - - def __init__( - self, - opened: IndyOpenWallet, - context: InjectionContext = None, - ): - """Create a new IndyProfile instance.""" - super().__init__(context=context, name=opened.name, created=opened.created) - self.opened = opened - self.ledger_pool: IndySdkLedgerPool = None - self.init_ledger_pool() - self.bind_providers() - self._finalizer = self._make_finalizer(opened) - - @property - def name(self) -> str: - """Accessor for the profile name.""" - return self.opened.name - - @property - def wallet(self) -> IndyOpenWallet: - """Accessor for the opened wallet instance.""" - return self.opened - - def init_ledger_pool(self): - """Initialize the ledger pool.""" - if self.settings.get("ledger.disabled"): - LOGGER.info("Ledger support is disabled") - return - - if self.settings.get("ledger.genesis_transactions"): - self.ledger_pool = self.context.inject(IndySdkLedgerPool, self.settings) - - def bind_providers(self): - """Initialize the profile-level instance providers.""" - injector = self._context.injector - - injector.bind_provider( - BaseStorageSearch, - ClassProvider("aries_cloudagent.storage.indy.IndySdkStorage", self.opened), - ) - - injector.bind_provider( - IndyHolder, - ClassProvider( - "aries_cloudagent.indy.sdk.holder.IndySdkHolder", self.opened - ), - ) - injector.bind_provider( - IndyIssuer, - ClassProvider("aries_cloudagent.indy.sdk.issuer.IndySdkIssuer", ref(self)), - ) - - injector.bind_provider( - VCHolder, - ClassProvider( - "aries_cloudagent.storage.vc_holder.indy.IndySdkVCHolder", self.opened - ), - ) - if ( - self.settings.get("ledger.ledger_config_list") - and len(self.settings.get("ledger.ledger_config_list")) >= 1 - ): - write_ledger_config = get_write_ledger_config_for_profile( - settings=self.settings - ) - cache = self.context.injector.inject_or(BaseCache) - injector.bind_provider( - BaseLedger, - ClassProvider( - IndySdkLedger, - IndySdkLedgerPool( - write_ledger_config.get("pool_name") - or write_ledger_config.get("id"), - keepalive=write_ledger_config.get("keepalive"), - cache=cache, - genesis_transactions=write_ledger_config.get( - "genesis_transactions" - ), - read_only=write_ledger_config.get("read_only"), - socks_proxy=write_ledger_config.get("socks_proxy"), - ), - ref(self), - ), - ) - self.settings["ledger.write_ledger"] = write_ledger_config.get("id") - if ( - "endorser_alias" in write_ledger_config - and "endorser_did" in write_ledger_config - ): - self.settings["endorser.endorser_alias"] = write_ledger_config.get( - "endorser_alias" - ) - self.settings["endorser.endorser_public_did"] = write_ledger_config.get( - "endorser_did" - ) - elif self.ledger_pool: - injector.bind_provider( - BaseLedger, ClassProvider(IndySdkLedger, self.ledger_pool, ref(self)) - ) - if self.ledger_pool or self.settings.get("ledger.ledger_config_list"): - injector.bind_provider( - IndyVerifier, - ClassProvider( - "aries_cloudagent.indy.sdk.verifier.IndySdkVerifier", - ref(self), - ), - ) - - def session(self, context: InjectionContext = None) -> "ProfileSession": - """Start a new interactive session with no transaction support requested.""" - return IndySdkProfileSession(self, context=context) - - def transaction(self, context: InjectionContext = None) -> "ProfileSession": - """Start a new interactive session with commit and rollback support. - - If the current backend does not support transactions, then commit - and rollback operations of the session will not have any effect. - """ - return IndySdkProfileSession(self, context=context) - - async def close(self): - """Close the profile instance.""" - if self.opened: - await self.opened.close() - self.opened = None - - def _make_finalizer(self, opened: IndyOpenWallet) -> finalize: - """Return a finalizer for this profile. - - See docs for weakref.finalize for more details on behavior of finalizers. - """ - - async def _closer(opened: IndyOpenWallet): - try: - await opened.close() - except Exception: - LOGGER.exception("Failed to close wallet from finalizer") - - def _finalize(opened: IndyOpenWallet): - LOGGER.debug("Profile finalizer called; closing wallet") - asyncio.get_event_loop().create_task(_closer(opened)) - - return finalize(self, _finalize, opened) - - async def remove(self): - """Remove the profile associated with this instance.""" - if not self.opened: - raise ProfileError("Wallet must be opened to remove profile") - - self.opened.config.auto_remove = True - await self.close() - - -class IndySdkProfileSession(ProfileSession): - """An active connection to the profile management backend.""" - - def __init__( - self, - profile: Profile, - *, - context: InjectionContext = None, - settings: Mapping[str, Any] = None - ): - """Create a new IndySdkProfileSession instance.""" - super().__init__(profile=profile, context=context, settings=settings) - - async def _setup(self): - """Create the session or transaction connection, if needed.""" - injector = self._context.injector - injector.bind_provider( - BaseWallet, ClassProvider(IndySdkWallet, self.profile.opened) - ) - injector.bind_provider( - BaseStorage, - ClassProvider( - "aries_cloudagent.storage.indy.IndySdkStorage", self.profile.opened - ), - ) - - -class IndySdkProfileManager(ProfileManager): - """Manager for Indy-SDK wallets.""" - - async def provision( - self, context: InjectionContext, config: Mapping[str, Any] = None - ) -> Profile: - """Provision a new instance of a profile.""" - indy_config = IndyWalletConfig(config) - opened = await indy_config.create_wallet() - return IndySdkProfile(opened, context) - - async def open( - self, context: InjectionContext, config: Mapping[str, Any] = None - ) -> Profile: - """Open an instance of an existing profile.""" - warnings.warn( - "Indy wallet type is deprecated, use Askar instead; see: " - "https://aca-py.org/main/deploying/IndySDKtoAskarMigration/", - DeprecationWarning, - ) - LOGGER.warning( - "Indy wallet type is deprecated, use Askar instead; see: " - "https://aca-py.org/main/deploying/IndySDKtoAskarMigration/", - ) - - indy_config = IndyWalletConfig(config) - opened = await indy_config.open_wallet() - return IndySdkProfile(opened, context) diff --git a/aries_cloudagent/indy/sdk/tests/test_holder.py b/aries_cloudagent/indy/sdk/tests/test_holder.py deleted file mode 100644 index f9a3084958..0000000000 --- a/aries_cloudagent/indy/sdk/tests/test_holder.py +++ /dev/null @@ -1,603 +0,0 @@ -import json -import pytest - -from aries_cloudagent.tests import mock -from unittest import IsolatedAsyncioTestCase - -import indy.anoncreds - -from indy.error import IndyError, ErrorCode - -from ...holder import IndyHolder, IndyHolderError - -from .. import holder as test_module - - -@pytest.mark.indy -class TestIndySdkHolder(IsolatedAsyncioTestCase): - def setUp(self): - mock_ledger = mock.MagicMock( - get_credential_definition=mock.MagicMock(return_value={"value": {}}), - get_revoc_reg_delta=mock.CoroutineMock( - return_value=( - {"value": {"...": "..."}}, - 1234567890, - ) - ), - ) - mock_ledger.__aenter__ = mock.CoroutineMock(return_value=mock_ledger) - self.ledger = mock_ledger - self.wallet = mock.MagicMock() - - self.holder = test_module.IndySdkHolder(self.wallet) - assert "IndySdkHolder" in str(self.holder) - - @mock.patch("indy.anoncreds.prover_create_credential_req") - async def test_create_credential_request(self, mock_create_credential_req): - mock_create_credential_req.return_value = ("{}", "[]") - - cred_req_json, cred_req_meta_json = await self.holder.create_credential_request( - "credential_offer", "credential_definition", "did" - ) - - mock_create_credential_req.assert_called_once_with( - self.wallet.handle, - "did", - json.dumps("credential_offer"), - json.dumps("credential_definition"), - self.wallet.master_secret_id, - ) - - assert (json.loads(cred_req_json), json.loads(cred_req_meta_json)) == ({}, []) - - @mock.patch("indy.anoncreds.prover_store_credential") - async def test_store_credential(self, mock_store_cred): - mock_store_cred.return_value = "cred_id" - - cred_id = await self.holder.store_credential( - "credential_definition", "credential_data", "credential_request_metadata" - ) - - mock_store_cred.assert_called_once_with( - wallet_handle=self.wallet.handle, - cred_id=None, - cred_req_metadata_json=json.dumps("credential_request_metadata"), - cred_json=json.dumps("credential_data"), - cred_def_json=json.dumps("credential_definition"), - rev_reg_def_json=None, - ) - - assert cred_id == "cred_id" - - @mock.patch("indy.anoncreds.prover_store_credential") - async def test_store_credential_with_mime_types(self, mock_store_cred): - with mock.patch.object( - test_module, "IndySdkStorage", mock.MagicMock() - ) as mock_storage: - mock_storage.return_value = mock.MagicMock(add_record=mock.CoroutineMock()) - - mock_store_cred.return_value = "cred_id" - - CRED_DATA = {"values": {"cameo": "d29yZCB1cA=="}} - cred_id = await self.holder.store_credential( - "credential_definition", - CRED_DATA, - "credential_request_metadata", - {"cameo": "image/png"}, - ) - - mock_store_cred.assert_called_once_with( - wallet_handle=self.wallet.handle, - cred_id=None, - cred_req_metadata_json=json.dumps("credential_request_metadata"), - cred_json=json.dumps(CRED_DATA), - cred_def_json=json.dumps("credential_definition"), - rev_reg_def_json=None, - ) - mock_storage.return_value.add_record.assert_called_once() - - assert cred_id == "cred_id" - - @mock.patch("indy.non_secrets.get_wallet_record") - async def test_get_credential_attrs_mime_types(self, mock_nonsec_get_wallet_record): - cred_id = "credential_id" - dummy_tags = {"a": "1", "b": "2"} - dummy_rec = { - "type": IndyHolder.RECORD_TYPE_MIME_TYPES, - "id": cred_id, - "value": "value", - "tags": dummy_tags, - } - mock_nonsec_get_wallet_record.return_value = json.dumps(dummy_rec) - - mime_types = await self.holder.get_mime_type(cred_id) - - mock_nonsec_get_wallet_record.assert_called_once_with( - self.wallet.handle, - dummy_rec["type"], - f"{IndyHolder.RECORD_TYPE_MIME_TYPES}::{dummy_rec['id']}", - json.dumps( - {"retrieveType": False, "retrieveValue": True, "retrieveTags": True} - ), - ) - - assert mime_types == dummy_tags - - @mock.patch("indy.non_secrets.get_wallet_record") - async def test_get_credential_attr_mime_type(self, mock_nonsec_get_wallet_record): - cred_id = "credential_id" - dummy_tags = {"a": "1", "b": "2"} - dummy_rec = { - "type": IndyHolder.RECORD_TYPE_MIME_TYPES, - "id": cred_id, - "value": "value", - "tags": dummy_tags, - } - mock_nonsec_get_wallet_record.return_value = json.dumps(dummy_rec) - - a_mime_type = await self.holder.get_mime_type(cred_id, "a") - - mock_nonsec_get_wallet_record.assert_called_once_with( - self.wallet.handle, - dummy_rec["type"], - f"{IndyHolder.RECORD_TYPE_MIME_TYPES}::{dummy_rec['id']}", - json.dumps( - {"retrieveType": False, "retrieveValue": True, "retrieveTags": True} - ), - ) - - assert a_mime_type == dummy_tags["a"] - - @mock.patch("indy.non_secrets.get_wallet_record") - async def test_get_credential_attr_mime_type_x(self, mock_nonsec_get_wallet_record): - cred_id = "credential_id" - dummy_tags = {"a": "1", "b": "2"} - dummy_rec = { - "type": IndyHolder.RECORD_TYPE_MIME_TYPES, - "id": cred_id, - "value": "value", - "tags": dummy_tags, - } - mock_nonsec_get_wallet_record.side_effect = test_module.StorageError() - - assert await self.holder.get_mime_type(cred_id, "a") is None - - @mock.patch("indy.anoncreds.prover_search_credentials") - @mock.patch("indy.anoncreds.prover_fetch_credentials") - @mock.patch("indy.anoncreds.prover_close_credentials_search") - async def test_get_credentials( - self, mock_close_cred_search, mock_fetch_credentials, mock_search_credentials - ): - SIZE = 300 - mock_search_credentials.return_value = ("search_handle", 350) - mock_fetch_credentials.side_effect = [ - json.dumps([0] * test_module.IndySdkHolder.CHUNK), - json.dumps([1] * (SIZE % test_module.IndySdkHolder.CHUNK)), - ] - - credentials = await self.holder.get_credentials(0, SIZE, {}) - mock_search_credentials.assert_called_once_with( - self.wallet.handle, json.dumps({}) - ) - - assert mock_fetch_credentials.call_count == 2 - mock_close_cred_search.assert_called_once_with("search_handle") - - assert len(credentials) == SIZE - - mock_fetch_credentials.side_effect = [ - json.dumps([0] * test_module.IndySdkHolder.CHUNK), - json.dumps([1] * (SIZE % test_module.IndySdkHolder.CHUNK)), - ] - credentials = await self.holder.get_credentials(0, 0, {}) # 0 defaults to all - assert len(credentials) == SIZE - - @mock.patch("indy.anoncreds.prover_search_credentials") - @mock.patch("indy.anoncreds.prover_fetch_credentials") - @mock.patch("indy.anoncreds.prover_close_credentials_search") - async def test_get_credentials_seek( - self, mock_close_cred_search, mock_fetch_credentials, mock_search_credentials - ): - mock_search_credentials.return_value = ("search_handle", 3) - mock_fetch_credentials.return_value = "[1,2,3]" - - credentials = await self.holder.get_credentials(2, 3, {}) - assert mock_fetch_credentials.call_args_list == [ - (("search_handle", 2),), - (("search_handle", 3),), - ] - - @mock.patch("indy.anoncreds.prover_search_credentials_for_proof_req") - @mock.patch("indy.anoncreds.prover_fetch_credentials_for_proof_req") - @mock.patch("indy.anoncreds.prover_close_credentials_search_for_proof_req") - async def test_get_credentials_for_presentation_request_by_reft( - self, - mock_prover_close_credentials_search_for_proof_req, - mock_prover_fetch_credentials_for_proof_req, - mock_prover_search_credentials_for_proof_req, - ): - SIZE = 300 - SKIP = 50 - mock_prover_search_credentials_for_proof_req.return_value = "search_handle" - mock_prover_fetch_credentials_for_proof_req.side_effect = [ - json.dumps( - [ - {"cred_info": {"referent": f"skip-{i}", "rev_reg_id": None}} - for i in range(SKIP) - ] - ), - json.dumps( - [ - { - "cred_info": { - "referent": f"reft-{i}", - "rev_reg_id": None if i % 2 else "dummy-rrid", - } - } - for i in range(test_module.IndyHolder.CHUNK) - ] - ), - json.dumps( - [ - { - "cred_info": { - "referent": f"reft-{test_module.IndyHolder.CHUNK + i}", - "rev_reg_id": None, - } - } - for i in range(SIZE % test_module.IndyHolder.CHUNK) - ] - ), - ] - - PROOF_REQ = { - "requested_attributes": {"attr_0_uuid": {"...": "..."}}, - "requested_predicates": {"pred_0_uuid": {"...": "..."}}, - } - credentials = ( - await self.holder.get_credentials_for_presentation_request_by_referent( - PROOF_REQ, - ("asdb",), - 50, - SIZE, - {"extra": "query"}, - ) - ) - - mock_prover_search_credentials_for_proof_req.assert_called_once_with( - self.wallet.handle, - json.dumps(PROOF_REQ), - json.dumps({"extra": "query"}), - ) - - assert mock_prover_fetch_credentials_for_proof_req.call_count == 3 - mock_prover_close_credentials_search_for_proof_req.assert_called_once_with( - "search_handle" - ) - - assert len(credentials) == SIZE - assert all( - not c["cred_info"]["rev_reg_id"] - for c in credentials[ - 0 : len(credentials) - (test_module.IndyHolder.CHUNK // 2) - ] - ) # irrevocable first - assert all( - c["cred_info"]["rev_reg_id"] - for c in credentials[-test_module.IndyHolder.CHUNK // 2 :] - ) # revocable last - - @mock.patch("indy.anoncreds.prover_search_credentials_for_proof_req") - @mock.patch("indy.anoncreds.prover_fetch_credentials_for_proof_req") - @mock.patch("indy.anoncreds.prover_close_credentials_search_for_proof_req") - async def test_get_credentials_for_presentation_request_by_referent_default_refts( - self, - mock_prover_close_credentials_search_for_proof_req, - mock_prover_fetch_credentials_for_proof_req, - mock_prover_search_credentials_for_proof_req, - ): - mock_prover_search_credentials_for_proof_req.return_value = "search_handle" - mock_prover_fetch_credentials_for_proof_req.return_value = json.dumps( - [{"cred_info": {"referent": "asdb", "rev_reg_id": None}}] - ) - - PRES_REQ = { - "requested_attributes": { - "0_a_uuid": {"...": "..."}, - "1_b_uuid": {"...": "..."}, - }, - "requested_predicates": {"2_c_ge_80": {"...": "..."}}, - } - - credentials = ( - await self.holder.get_credentials_for_presentation_request_by_referent( - PRES_REQ, - None, - 2, - 3, - ) - ) - - mock_prover_search_credentials_for_proof_req.assert_called_once_with( - self.wallet.handle, json.dumps(PRES_REQ), json.dumps({}) - ) - - @mock.patch("indy.anoncreds.prover_get_credential") - async def test_get_credential(self, mock_get_cred): - mock_get_cred.return_value = "{}" - credential_json = await self.holder.get_credential("credential_id") - mock_get_cred.assert_called_once_with(self.wallet.handle, "credential_id") - - assert json.loads(credential_json) == {} - - @mock.patch("indy.anoncreds.prover_get_credential") - async def test_get_credential_not_found(self, mock_get_cred): - mock_get_cred.side_effect = IndyError(error_code=ErrorCode.WalletItemNotFound) - with self.assertRaises(test_module.WalletNotFoundError): - await self.holder.get_credential("credential_id") - - @mock.patch("indy.anoncreds.prover_get_credential") - async def test_get_credential_x(self, mock_get_cred): - mock_get_cred.side_effect = IndyError("unexpected failure") - - with self.assertRaises(test_module.IndyHolderError): - await self.holder.get_credential("credential_id") - - async def test_credential_revoked(self): - with mock.patch.object( # no creds revoked - self.holder, "get_credential", mock.CoroutineMock() - ) as mock_get_cred: - mock_get_cred.return_value = json.dumps( - { - "rev_reg_id": "dummy-rrid", - "cred_rev_id": "123", - "...": "...", - } - ) - result = await self.holder.credential_revoked(self.ledger, "credential_id") - assert not result - - with mock.patch.object( # cred not revocable - self.holder, "get_credential", mock.CoroutineMock() - ) as mock_get_cred: - mock_get_cred.return_value = json.dumps( - { - "rev_reg_id": None, - "cred_rev_id": None, - "...": "...", - } - ) - result = await self.holder.credential_revoked(self.ledger, "credential_id") - assert not result - - self.ledger.get_revoc_reg_delta = mock.CoroutineMock( - return_value=( - { - "value": { - "revoked": [1, 2, 3], - "...": "...", - } - }, - 1234567890, - ) - ) - with mock.patch.object( # cred not revoked - self.holder, "get_credential", mock.CoroutineMock() - ) as mock_get_cred: - mock_get_cred.return_value = json.dumps( - { - "rev_reg_id": "dummy-rrid", - "cred_rev_id": "123", - "...": "...", - } - ) - result = await self.holder.credential_revoked(self.ledger, "credential_id") - assert not result - - with mock.patch.object( # cred revoked - self.holder, "get_credential", mock.CoroutineMock() - ) as mock_get_cred: - mock_get_cred.return_value = json.dumps( - { - "rev_reg_id": "dummy-rrid", - "cred_rev_id": "2", - "...": "...", - } - ) - result = await self.holder.credential_revoked(self.ledger, "credential_id") - assert result - - @mock.patch("indy.anoncreds.prover_delete_credential") - @mock.patch("indy.non_secrets.get_wallet_record") - @mock.patch("indy.non_secrets.delete_wallet_record") - async def test_delete_credential( - self, - mock_nonsec_del_wallet_record, - mock_nonsec_get_wallet_record, - mock_prover_del_cred, - ): - mock_nonsec_get_wallet_record.return_value = json.dumps( - { - "type": "typ", - "id": "ident", - "value": "value", - "tags": {"a": json.dumps("1"), "b": json.dumps("2")}, - } - ) - - credential = await self.holder.delete_credential("credential_id") - - mock_prover_del_cred.assert_called_once_with( - self.wallet.handle, "credential_id" - ) - - @mock.patch("indy.anoncreds.prover_delete_credential") - @mock.patch("indy.non_secrets.get_wallet_record") - @mock.patch("indy.non_secrets.delete_wallet_record") - async def test_delete_credential_x( - self, - mock_nonsec_del_wallet_record, - mock_nonsec_get_wallet_record, - mock_prover_del_cred, - ): - mock_nonsec_get_wallet_record.side_effect = test_module.StorageNotFoundError() - mock_prover_del_cred.side_effect = IndyError( - error_code=ErrorCode.WalletItemNotFound - ) - - with self.assertRaises(test_module.WalletNotFoundError): - await self.holder.delete_credential("credential_id") - mock_prover_del_cred.assert_called_once_with( - self.wallet.handle, "credential_id" - ) - - mock_prover_del_cred.side_effect = IndyError( - error_code=ErrorCode.CommonInvalidParam1 - ) - with self.assertRaises(test_module.IndyHolderError): - await self.holder.delete_credential("credential_id") - assert mock_prover_del_cred.call_count == 2 - - @mock.patch("indy.anoncreds.prover_create_proof") - async def test_create_presentation(self, mock_create_proof): - mock_create_proof.return_value = "{}" - PROOF_REQ = { - "nonce": "1554990836", - "name": "proof_req", - "version": "0.0", - "requested_attributes": { - "20_legalname_uuid": { - "name": "legalName", - "restrictions": [ - {"cred_def_id": "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag"} - ], - } - }, - "requested_predicates": { - "21_jurisdictionid_GE_uuid": { - "name": "jurisdictionId", - "p_type": ">=", - "p_value": 1, - "restrictions": [ - {"cred_def_id": "WgWxqztrNooG92RXvxSTWv:3:CL:21:tag"} - ], - } - }, - } - - presentation_json = await self.holder.create_presentation( - PROOF_REQ, - "requested_credentials", - "schemas", - "credential_definitions", - ) - - mock_create_proof.assert_called_once_with( - self.wallet.handle, - json.dumps(PROOF_REQ), - json.dumps("requested_credentials"), - self.wallet.master_secret_id, - json.dumps("schemas"), - json.dumps("credential_definitions"), - json.dumps({}), - ) - - assert json.loads(presentation_json) == {} - - async def test_create_presentation_restr_attr_mismatch_x(self): - PROOF_REQS = [ - { - "nonce": "1554990836", - "name": "proof_req", - "version": "0.0", - "requested_attributes": { - "20_legalname_uuid": { - "name": "legalName", - "restrictions": [ - { - "cred_def_id": "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "attr::wrong::value": "Waffle Asteroid", - } - ], - } - }, - "requested_predicates": { - "21_jurisdictionid_GE_uuid": { - "name": "jurisdictionId", - "p_type": ">=", - "p_value": 1, - "restrictions": [ - {"cred_def_id": "WgWxqztrNooG92RXvxSTWv:3:CL:21:tag"} - ], - } - }, - }, - { - "nonce": "1554990836", - "name": "proof_req", - "version": "0.0", - "requested_attributes": { - "20_legalname_uuid": { - "names": ["legalName", "businessLang"], - "restrictions": [ - { - "cred_def_id": "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag", - "attr::wrong::value": "Waffle Asteroid", - } - ], - } - }, - "requested_predicates": { - "21_jurisdictionid_GE_uuid": { - "name": "jurisdictionId", - "p_type": ">=", - "p_value": 1, - "restrictions": [ - {"cred_def_id": "WgWxqztrNooG92RXvxSTWv:3:CL:21:tag"} - ], - } - }, - }, - ] - - for proof_req in PROOF_REQS: - with self.assertRaises(IndyHolderError): - await self.holder.create_presentation( - proof_req, - "requested_credentials", - "schemas", - "credential_definitions", - ) - - async def test_create_revocation_state(self): - rr_state = { - "witness": {"omega": "1 ..."}, - "rev_reg": {"accum": "21 ..."}, - "timestamp": 1234567890, - } - - with mock.patch.object( - test_module, "create_tails_reader", mock.CoroutineMock() - ) as mock_create_tails_reader, mock.patch.object( - indy.anoncreds, "create_revocation_state", mock.CoroutineMock() - ) as mock_create_rr_state: - mock_create_rr_state.return_value = json.dumps(rr_state) - - cred_rev_id = "1" - rev_reg_def = {"def": 1} - rev_reg_delta = {"delta": 1} - timestamp = 1234567890 - tails_path = "/tmp/some.tails" - - result = await self.holder.create_revocation_state( - cred_rev_id, rev_reg_def, rev_reg_delta, timestamp, tails_path - ) - assert json.loads(result) == rr_state - - mock_create_rr_state.assert_awaited_once_with( - mock_create_tails_reader.return_value, - rev_reg_def_json=json.dumps(rev_reg_def), - cred_rev_id=cred_rev_id, - rev_reg_delta_json=json.dumps(rev_reg_delta), - timestamp=timestamp, - ) diff --git a/aries_cloudagent/indy/sdk/tests/test_issuer.py b/aries_cloudagent/indy/sdk/tests/test_issuer.py deleted file mode 100644 index d0946a65d7..0000000000 --- a/aries_cloudagent/indy/sdk/tests/test_issuer.py +++ /dev/null @@ -1,398 +0,0 @@ -import json -import pytest - -from unittest import mock -from unittest import IsolatedAsyncioTestCase - -from indy.error import ( - AnoncredsRevocationRegistryFullError, - ErrorCode, - IndyError, - WalletItemNotFound, -) - -from ....config.injection_context import InjectionContext -from ....indy.sdk.profile import IndySdkProfile -from ....indy.sdk.wallet_setup import IndyWalletConfig -from ....wallet.indy import IndySdkWallet -from ....ledger.indy import IndySdkLedgerPool - -from ...issuer import IndyIssuerRevocationRegistryFullError - -from .. import issuer as test_module - - -TEST_DID = "55GkHamhTU1ZbTbV2ab9DE" -SCHEMA_NAME = "resident" -SCHEMA_VERSION = "1.0" -SCHEMA_TXN = 1234 -SCHEMA_ID = f"{TEST_DID}:2:{SCHEMA_NAME}:{SCHEMA_VERSION}" -CRED_DEF_ID = f"{TEST_DID}:3:CL:{SCHEMA_TXN}:default" -REV_REG_ID = f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:0" -TEST_RR_DELTA = { - "ver": "1.0", - "value": {"prevAccum": "1 ...", "accum": "21 ...", "issued": [1, 2, 12, 42]}, -} - - -@pytest.mark.indy -class TestIndySdkIssuer(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.context = InjectionContext() - self.context.injector.bind_instance( - IndySdkLedgerPool, IndySdkLedgerPool("name") - ) - - self.wallet = await IndyWalletConfig( - { - "auto_recreate": True, - "auto_remove": True, - "key": await IndySdkWallet.generate_wallet_key(), - "key_derivation_method": "RAW", - "name": "test-wallet", - } - ).create_wallet() - with mock.patch.object(IndySdkProfile, "_make_finalizer"): - self.profile = IndySdkProfile(self.wallet, self.context) - self.issuer = test_module.IndySdkIssuer(self.profile) - - async def tearDown(self): - await self.profile.close() - - async def test_repr(self): - assert "IndySdkIssuer" in str(self.issuer) # cover __repr__ - - @mock.patch("indy.anoncreds.issuer_create_and_store_credential_def") - async def test_schema_cred_def(self, mock_indy_cred_def): - assert ( - self.issuer.make_schema_id(TEST_DID, SCHEMA_NAME, SCHEMA_VERSION) - == SCHEMA_ID - ) - - (s_id, schema_json) = await self.issuer.create_schema( - TEST_DID, - SCHEMA_NAME, - SCHEMA_VERSION, - ["name", "moniker", "genre", "effective"], - ) - assert s_id == SCHEMA_ID - schema = json.loads(schema_json) - schema["seqNo"] = SCHEMA_TXN - - assert ( - self.issuer.make_credential_definition_id(TEST_DID, schema, tag="default") - == CRED_DEF_ID - ) - - mock_indy_cred_def.return_value = ( - CRED_DEF_ID, - json.dumps({"dummy": "cred-def"}), - ) - assert (CRED_DEF_ID, json.dumps({"dummy": "cred-def"})) == ( - await self.issuer.create_and_store_credential_definition( - TEST_DID, schema, support_revocation=True - ) - ) - - @mock.patch("indy.anoncreds.issuer_create_credential_offer") - async def test_credential_definition_in_wallet(self, mock_indy_create_offer): - mock_indy_create_offer.return_value = {"sample": "offer"} - assert await self.issuer.credential_definition_in_wallet(CRED_DEF_ID) - - @mock.patch("indy.anoncreds.issuer_create_credential_offer") - async def test_credential_definition_in_wallet_no(self, mock_indy_create_offer): - mock_indy_create_offer.side_effect = WalletItemNotFound( - error_code=ErrorCode.WalletItemNotFound - ) - assert not await self.issuer.credential_definition_in_wallet(CRED_DEF_ID) - - @mock.patch("indy.anoncreds.issuer_create_credential_offer") - async def test_credential_definition_in_wallet_x(self, mock_indy_create_offer): - mock_indy_create_offer.side_effect = IndyError( - error_code=ErrorCode.WalletInvalidHandle - ) - with self.assertRaises(test_module.IndyIssuerError): - await self.issuer.credential_definition_in_wallet(CRED_DEF_ID) - - @mock.patch("indy.anoncreds.issuer_create_credential_offer") - async def test_create_credential_offer(self, mock_create_offer): - test_offer = {"test": "offer"} - test_cred_def_id = "test-cred-def-id" - mock_create_offer.return_value = json.dumps(test_offer) - mock_profile = mock.MagicMock() - issuer = test_module.IndySdkIssuer(mock_profile) - offer_json = await issuer.create_credential_offer(test_cred_def_id) - assert json.loads(offer_json) == test_offer - mock_create_offer.assert_called_once_with( - mock_profile.wallet.handle, test_cred_def_id - ) - - @mock.patch("indy.anoncreds.issuer_create_credential") - @mock.patch.object(test_module, "create_tails_reader", autospec=True) - @mock.patch("indy.anoncreds.issuer_revoke_credential") - @mock.patch("indy.anoncreds.issuer_merge_revocation_registry_deltas") - async def test_create_revoke_credentials( - self, - mock_indy_merge_rr_deltas, - mock_indy_revoke_credential, - mock_tails_reader, - mock_indy_create_credential, - ): - test_schema = {"attrNames": ["attr1"]} - test_offer = { - "schema_id": SCHEMA_ID, - "cred_def_id": CRED_DEF_ID, - "key_correctness_proof": {"c": "...", "xz_cap": "...", "xr_cap": ["..."]}, - "nonce": "...", - } - test_request = {"test": "request"} - test_values = {"attr1": "value1"} - test_cred = { - "schema_id": SCHEMA_ID, - "cred_def_id": CRED_DEF_ID, - "rev_reg_id": REV_REG_ID, - "values": {"attr1": {"raw": "value1", "encoded": "123456123899216581404"}}, - "signature": {"...": "..."}, - "signature_correctness_proof": {"...": "..."}, - "rev_reg": {"accum": "21 12E8..."}, - "witness": {"omega": "21 1369..."}, - } - test_cred_rev_ids = ["42", "54"] - test_rr_delta = TEST_RR_DELTA - mock_indy_create_credential.side_effect = [ - ( - json.dumps(test_cred), - cr_id, - test_rr_delta, - ) - for cr_id in test_cred_rev_ids - ] - - with self.assertRaises(test_module.IndyIssuerError): # missing attribute - cred_json, revoc_id = await self.issuer.create_credential( - test_schema, - test_offer, - test_request, - {}, - ) - - (cred_json, cred_rev_id) = await self.issuer.create_credential( # main line - test_schema, - test_offer, - test_request, - test_values, - REV_REG_ID, - "/tmp/tails/path/dummy", - ) - mock_indy_create_credential.assert_called_once() - ( - call_wallet, - call_offer, - call_request, - call_values, - call_etc1, - call_etc2, - ) = mock_indy_create_credential.call_args[0] - assert call_wallet is self.wallet.handle - assert json.loads(call_offer) == test_offer - assert json.loads(call_request) == test_request - values = json.loads(call_values) - assert "attr1" in values - - mock_indy_revoke_credential.return_value = json.dumps(TEST_RR_DELTA) - mock_indy_merge_rr_deltas.return_value = json.dumps(TEST_RR_DELTA) - (result, failed) = await self.issuer.revoke_credentials( - CRED_DEF_ID, - REV_REG_ID, - tails_file_path="dummy", - cred_rev_ids=test_cred_rev_ids, - ) - assert json.loads(result) == TEST_RR_DELTA - assert not failed - assert mock_indy_revoke_credential.call_count == 2 - mock_indy_merge_rr_deltas.assert_called_once() - - @mock.patch("indy.anoncreds.issuer_create_credential") - @mock.patch.object(test_module, "create_tails_reader", autospec=True) - @mock.patch("indy.anoncreds.issuer_revoke_credential") - @mock.patch("indy.anoncreds.issuer_merge_revocation_registry_deltas") - async def test_create_revoke_credentials_x( - self, - mock_indy_merge_rr_deltas, - mock_indy_revoke_credential, - mock_tails_reader, - mock_indy_create_credential, - ): - test_schema = {"attrNames": ["attr1"]} - test_offer = { - "schema_id": SCHEMA_ID, - "cred_def_id": CRED_DEF_ID, - "key_correctness_proof": {"c": "...", "xz_cap": "...", "xr_cap": ["..."]}, - "nonce": "...", - } - test_request = {"test": "request"} - test_values = {"attr1": "value1"} - test_cred = { - "schema_id": SCHEMA_ID, - "cred_def_id": CRED_DEF_ID, - "rev_reg_id": REV_REG_ID, - "values": {"attr1": {"raw": "value1", "encoded": "123456123899216581404"}}, - "signature": {"...": "..."}, - "signature_correctness_proof": {"...": "..."}, - "rev_reg": {"accum": "21 12E8..."}, - "witness": {"omega": "21 1369..."}, - } - test_cred_rev_ids = ["42", "54", "103"] - test_rr_delta = TEST_RR_DELTA - mock_indy_create_credential.side_effect = [ - ( - json.dumps(test_cred), - cr_id, - test_rr_delta, - ) - for cr_id in test_cred_rev_ids - ] - - with self.assertRaises(test_module.IndyIssuerError): # missing attribute - cred_json, revoc_id = await self.issuer.create_credential( - test_schema, - test_offer, - test_request, - {}, - ) - - (cred_json, cred_rev_id) = await self.issuer.create_credential( # main line - test_schema, - test_offer, - test_request, - test_values, - REV_REG_ID, - "/tmp/tails/path/dummy", - ) - mock_indy_create_credential.assert_called_once() - ( - call_wallet, - call_offer, - call_request, - call_values, - call_etc1, - call_etc2, - ) = mock_indy_create_credential.call_args[0] - assert call_wallet is self.wallet.handle - assert json.loads(call_offer) == test_offer - assert json.loads(call_request) == test_request - values = json.loads(call_values) - assert "attr1" in values - - def mock_revoke(_h, _t, _r, cred_rev_id): - if cred_rev_id == "42": - return json.dumps(TEST_RR_DELTA) - if cred_rev_id == "54": - raise IndyError( - error_code=ErrorCode.AnoncredsInvalidUserRevocId, - error_details={"message": "already revoked"}, - ) - raise IndyError( - error_code=ErrorCode.UnknownCryptoTypeError, - error_details={"message": "truly an outlier"}, - ) - - mock_indy_revoke_credential.side_effect = mock_revoke - mock_indy_merge_rr_deltas.return_value = json.dumps(TEST_RR_DELTA) - (result, failed) = await self.issuer.revoke_credentials( - CRED_DEF_ID, - REV_REG_ID, - tails_file_path="dummy", - cred_rev_ids=test_cred_rev_ids, - ) - assert json.loads(result) == TEST_RR_DELTA - assert failed == ["54", "103"] - assert mock_indy_revoke_credential.call_count == 3 - mock_indy_merge_rr_deltas.assert_not_called() - - @mock.patch("indy.anoncreds.issuer_create_credential") - @mock.patch.object(test_module, "create_tails_reader", autospec=True) - async def test_create_credential_rr_full( - self, - mock_tails_reader, - mock_indy_create_credential, - ): - test_schema = {"attrNames": ["attr1"]} - test_offer = { - "schema_id": SCHEMA_ID, - "cred_def_id": CRED_DEF_ID, - "key_correctness_proof": {"c": "...", "xz_cap": "...", "xr_cap": ["..."]}, - "nonce": "...", - } - test_request = {"test": "request"} - test_values = {"attr1": "value1"} - test_credential = {"test": "credential"} - test_cred_rev_id = "42" - test_rr_delta = TEST_RR_DELTA - mock_indy_create_credential.side_effect = AnoncredsRevocationRegistryFullError( - error_code=ErrorCode.AnoncredsRevocationRegistryFullError - ) - - with self.assertRaises(IndyIssuerRevocationRegistryFullError): - await self.issuer.create_credential( - test_schema, - test_offer, - test_request, - test_values, - ) - - @mock.patch("indy.anoncreds.issuer_create_credential") - @mock.patch.object(test_module, "create_tails_reader", autospec=True) - async def test_create_credential_x_indy( - self, - mock_tails_reader, - mock_indy_create_credential, - ): - test_schema = {"attrNames": ["attr1"]} - test_offer = { - "schema_id": SCHEMA_ID, - "cred_def_id": CRED_DEF_ID, - "key_correctness_proof": {"c": "...", "xz_cap": "...", "xr_cap": ["..."]}, - "nonce": "...", - } - test_request = {"test": "request"} - test_values = {"attr1": "value1"} - test_credential = {"test": "credential"} - test_cred_rev_id = "42" - test_rr_delta = TEST_RR_DELTA - - mock_indy_create_credential.side_effect = IndyError( - error_code=ErrorCode.WalletInvalidHandle - ) - - with self.assertRaises(test_module.IndyIssuerError): - await self.issuer.create_credential( - test_schema, - test_offer, - test_request, - test_values, - ) - - @mock.patch("indy.anoncreds.issuer_create_and_store_revoc_reg") - @mock.patch.object(test_module, "create_tails_writer", autospec=True) - async def test_create_and_store_revocation_registry( - self, mock_indy_tails_writer, mock_indy_rr - ): - mock_indy_rr.return_value = ("a", "b", "c") - ( - rr_id, - rrdef_json, - rre_json, - ) = await self.issuer.create_and_store_revocation_registry( - TEST_DID, CRED_DEF_ID, "CL_ACCUM", "rr-tag", 100, "/tmp/tails/path" - ) - assert (rr_id, rrdef_json, rre_json) == ("a", "b", "c") - - @mock.patch("indy.anoncreds.issuer_merge_revocation_registry_deltas") - async def test_merge_revocation_registry_deltas(self, mock_indy_merge): - mock_indy_merge.return_value = json.dumps({"net": "delta"}) - assert {"net": "delta"} == json.loads( - await self.issuer.merge_revocation_registry_deltas( - {"fro": "delta"}, {"to": "delta"} - ) - ) diff --git a/aries_cloudagent/indy/sdk/tests/test_profile.py b/aries_cloudagent/indy/sdk/tests/test_profile.py deleted file mode 100644 index 660641e4d7..0000000000 --- a/aries_cloudagent/indy/sdk/tests/test_profile.py +++ /dev/null @@ -1,130 +0,0 @@ -import pytest - -from aries_cloudagent.tests import mock - -from ....config.injection_context import InjectionContext -from ....core.error import ProfileError -from ....ledger.base import BaseLedger -from ....ledger.indy import IndySdkLedgerPool - -from ..profile import IndySdkProfile -from ..wallet_setup import IndyOpenWallet, IndyWalletConfig - -from .. import profile as test_module - - -@pytest.fixture -async def open_wallet(): - opened = IndyOpenWallet( - config=IndyWalletConfig({"name": "test-profile"}), - created=True, - handle=1, - master_secret_id="master-secret", - ) - with mock.patch.object(opened, "close", mock.CoroutineMock()): - yield opened - - -@pytest.fixture() -async def profile(open_wallet): - context = InjectionContext() - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - profile = IndySdkProfile(open_wallet, context) - - yield profile - - # Trigger finalizer before event loop fixture is closed - profile._finalizer() - - -@pytest.mark.asyncio -async def test_init_multi_ledger(open_wallet): - context = InjectionContext( - settings={ - "ledger.ledger_config_list": [ - { - "id": "BCovrinDev", - "is_production": True, - "is_write": True, - "endorser_did": "9QPa6tHvBHttLg6U4xvviv", - "endorser_alias": "endorser_dev", - "genesis_transactions": mock.MagicMock(), - }, - { - "id": "SovrinStagingNet", - "is_production": False, - "genesis_transactions": mock.MagicMock(), - }, - ] - } - ) - askar_profile = IndySdkProfile( - open_wallet, - context=context, - ) - - assert askar_profile.opened == open_wallet - assert askar_profile.settings["endorser.endorser_alias"] == "endorser_dev" - assert ( - askar_profile.settings["endorser.endorser_public_did"] - == "9QPa6tHvBHttLg6U4xvviv" - ) - assert (askar_profile.inject_or(BaseLedger)).pool_name == "BCovrinDev" - - -@pytest.mark.asyncio -async def test_properties(profile: IndySdkProfile): - assert profile.name == "test-profile" - assert profile.backend == "indy" - assert profile.wallet and profile.wallet.handle == 1 - - assert "IndySdkProfile" in str(profile) - assert profile.created - assert profile.wallet.created - assert profile.wallet.master_secret_id == "master-secret" - - with mock.patch.object(profile, "opened", False): - with pytest.raises(ProfileError): - await profile.remove() - - with mock.patch.object(profile.opened, "close", mock.CoroutineMock()): - await profile.remove() - assert profile.opened is None - - -def test_settings_genesis_transactions(open_wallet): - context = InjectionContext( - settings={"ledger.genesis_transactions": mock.MagicMock()} - ) - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - profile = IndySdkProfile(open_wallet, context) - - -def test_settings_ledger_config(open_wallet): - context = InjectionContext( - settings={ - "ledger.ledger_config_list": [ - mock.MagicMock(), - mock.MagicMock(), - ] - } - ) - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - profile = IndySdkProfile(open_wallet, context) - - -def test_read_only(open_wallet): - context = InjectionContext(settings={"ledger.read_only": True}) - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - ro_profile = IndySdkProfile(open_wallet, context) - - -def test_finalizer(open_wallet): - profile = IndySdkProfile(open_wallet) - assert profile - with mock.patch.object(test_module, "LOGGER", autospec=True) as mock_logger: - profile._finalizer() - assert mock_logger.debug.call_count == 1 - mock_logger.debug.assert_called_once_with( - "Profile finalizer called; closing wallet" - ) diff --git a/aries_cloudagent/indy/sdk/tests/test_util.py b/aries_cloudagent/indy/sdk/tests/test_util.py deleted file mode 100644 index fb587d8f40..0000000000 --- a/aries_cloudagent/indy/sdk/tests/test_util.py +++ /dev/null @@ -1,47 +0,0 @@ -import pytest - -from shutil import rmtree - -import indy.blob_storage - -from aries_cloudagent.tests import mock -from unittest import IsolatedAsyncioTestCase - -from ...util import indy_client_dir, generate_pr_nonce - -from ..util import create_tails_reader, create_tails_writer - - -@pytest.mark.indy -class TestIndyUtils(IsolatedAsyncioTestCase): - TAILS_HASH = "8UW1Sz5cqoUnK9hqQk7nvtKK65t7Chu3ui866J23sFyJ" - - def tearDown(self): - tails_dir = indy_client_dir("tails", create=False) - rmtree(tails_dir, ignore_errors=True) - - async def test_tails_reader(self): - tails_dir = indy_client_dir("tails", create=True) - tails_local = f"{tails_dir}/{TestIndyUtils.TAILS_HASH}" - - with open(tails_local, "a") as f: - print("1234123412431234", file=f) - - with mock.patch.object( - indy.blob_storage, "open_reader", mock.CoroutineMock() - ) as mock_blob_open_reader: - result = await create_tails_reader(tails_local) - assert result == mock_blob_open_reader.return_value - - rmtree(tails_dir, ignore_errors=True) - with self.assertRaises(FileNotFoundError): - await create_tails_reader(tails_local) - - async def test_tails_writer(self): - tails_dir = indy_client_dir("tails", create=True) - assert await create_tails_writer(tails_dir) - - rmtree(tails_dir, ignore_errors=True) - - async def test_nonce(self): - assert await generate_pr_nonce() diff --git a/aries_cloudagent/indy/sdk/tests/test_verifier.py b/aries_cloudagent/indy/sdk/tests/test_verifier.py deleted file mode 100644 index 17eed7044d..0000000000 --- a/aries_cloudagent/indy/sdk/tests/test_verifier.py +++ /dev/null @@ -1,597 +0,0 @@ -import json -import pytest - -from copy import deepcopy - -from aries_cloudagent.tests import mock -from unittest import IsolatedAsyncioTestCase -from indy.error import IndyError - -from ....core.in_memory import InMemoryProfile -from ....ledger.multiple_ledger.ledger_requests_executor import ( - IndyLedgerRequestsExecutor, -) - -from ..verifier import IndySdkVerifier - - -INDY_PROOF_REQ_NAME = { - "nonce": "15606741555044336341559", - "name": "proof_req", - "version": "0.0", - "requested_attributes": { - "19_uuid": { - "name": "Preferred Name", - "restrictions": [{"cred_def_id": "LjgpST2rjsoxYegQDRm7EL:3:CL:19:tag"}], - } - }, - "requested_predicates": {}, - "non_revoked": {"from": 1579892963, "to": 1579892963}, -} -INDY_PROOF_NAME = { - "proof": { - "proofs": [ - { - "primary_proof": { - "eq_proof": { - "revealed_attrs": { - "preferredname": "94607763023542937648705576709896212619553924110058781320304650334433495169960" - }, - "a_prime": "...", - "e": "...", - "v": "...", - "m": {"master_secret": "...", "musthave": "..."}, - "m2": "...", - }, - "ge_proofs": [], - }, - "non_revoc_proof": None, - } - ], - "aggregated_proof": {"c_hash": "...", "c_list": [[1, 152, 172, 159]]}, - }, - "requested_proof": { - "revealed_attrs": { - "19_uuid": { - "sub_proof_index": 0, - "raw": "Chicken Hawk", - "encoded": "94607763023542937648705576709896212619553924110058781320304650334433495169960", - } - }, - "self_attested_attrs": {}, - "unrevealed_attrs": {}, - "predicates": {}, - }, - "identifiers": [ - { - "schema_id": "LjgpST2rjsoxYegQDRm7EL:2:non-revo:1579888926.0", - "cred_def_id": "LjgpST2rjsoxYegQDRm7EL:3:CL:19:tag", - "rev_reg_id": "LjgpST2rjsoxYegQDRm7EL:4:LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag:CL_ACCUM:0", - "timestamp": 1579892963, - } - ], -} - -INDY_PROOF_REQ_PRED_NAMES = { - "nonce": "12301197819298309547817", - "name": "proof_req", - "version": "0.0", - "requested_attributes": { - "18_uuid": { - "names": [ - "effectiveDate", - "jurisdictionId", - "endDate", - "legalName", - "orgTypeId", - ], - "restrictions": [{"cred_def_id": "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag"}], - "non_revoked": {"from": 1579892963, "to": 1579892963}, - } - }, - "requested_predicates": { - "18_id_GE_uuid": { - "name": "id", - "p_type": ">=", - "p_value": 4, - "restrictions": [{"cred_def_id": "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag"}], - "non_revoked": {"from": 1579892963, "to": 1579892963}, - }, - "18_busid_GE_uuid": { - "name": "busId", - "p_type": ">=", - "p_value": 11198760, - "restrictions": [{"cred_def_id": "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag"}], - "non_revoked": {"from": 1579892963, "to": 1579892963}, - }, - }, -} - -INDY_PROOF_PRED_NAMES = { - "proof": { - "proofs": [ - { - "primary_proof": { - "eq_proof": { - "revealed_attrs": { - "effectivedate": "29898645652691622994103043707423726726370719600737126045061047957925549204159", - "enddate": "102987336249554097029535212322581322789799900648198034993379397001115665086549", - "jurisdictionid": "1", - "legalname": "106000828387368179355563788886235175190145445419967766011746391966411797095112", - "orgtypeid": "2", - }, - "a_prime": "15004053730967415956444780426929245426212215338862984979494015601906315582840747306504594441147441231491693951307278868750626954115009843921712832446544313561614118651520859494434080523236571958503756731624044004577892061145780190353067193496632483652558392939182437813999901132281095751156851493821923092362225145694407463842363472935374563198989094026343235461171230866928987229440390088485243428084237480758852248249770191814659757536925909483623366615812343227994433513635227834136882757394235805747686707186194818800509367263735891033464810268941516104197138946893490285348940539273855011764023467736767154303840", - "e": "337235637061060569047727820825037317712308782222370290484075504679799877011498224496826887984625822621748120654975531604507028064312710", - "v": "1404574530639210172781710601270953478414552186112985513475784201805119858770941821816370201652610496512142438596496007803868074196519046400754620766301997215969127187833134416898687892635798644651536667962849945968608408680347359937747715599800353850666709655353571249823190377976481837350280859973291095846106817174217510384400072134061086282647508604512946800721425580606901739211324799734725830882957974114794011791236006123406974194631084620463349145771704097181067806553409326685136263002734388842031423620455266365851581883497063570426034222596154597920580362387253753317413050267993785894175555001456331223234400596625730555935567545822248399326429854362227165802672227905967242505077485029657064067770641969647406371744932313880132835781524174868335472062214928455106355639700336515509695339440337721239602256387991397127509846614577908429409389332146746885470613002111095896313068354016587589778644661193149185049", - "m": { - "master_secret": "268741899404098839327031223989228937242803085201179726908056281850709820406283972250249379228789368664433583241086441517910928033266925485611584652328638784395957058632060633630", - "busid": "2687197004064133543257369626470144380098036289489284489320086515026620206692616047425976133587124290887441908383692364439260071404270430528078491104384060203570606253676528361400", - "id": "6686713960986576137959547581149682718587656100042127047852747024276212127252400140409890726173570723819441146289878657604374865560165489933600436341950054222778208816245032311193", - }, - "m2": "1180732317592917288409508571561928260151012766032216949553655321777067495114084046539503538100319204468787213948625648828272873800122130063408401311370987", - }, - "ge_proofs": [ - { - "u": { - "0": "15775609194986735920510151800942995799222803216082415810148803816296803079801357496664353906579826995829149362968465835795491271435248746459334118965204125314594582971550392227954", - "2": "5303150256152520023495176881750201750170184894234097909710215547442554470805609846521764595898911334528530696240025838754931022084938196723161868181531727845300439592437899863887", - "3": "3356711078459696620189646681109895593397921422760359051406583045001333345458592898545852513866307624143916692556089833035405496562577023756005223378326300905996972689863856066875", - "1": "9999991890173781186974768504758157527548652482914116775165195164578745484991479122468109103928320060297494255214338396428491092606606051561499468708339979065194763516537003502062", - }, - "r": { - "DELTA": "1435090146724677611480872211988213747514582597135551797832629955760022689079479873681839403744643599039883834204615937515288097736927712499250203649611222666450687692819628191366070914555251320872315378202337414304735555708434851449005494065128333408295370378734399236857073675785782330461793283646613324794741612075132251003819809779185772616720952264615331933630593162857145006881047266047864525898689246790061753791575361871922643386721142202508891103428155725164118848489256450446385140752308548079412012057535799088600334139468432242004848224338715577616491890083382988533414746224157485737168172255373805848589505587117269746736884630803388115258573174965628402748672653106950672620945656786479587908733067646954", - "2": "1256008392268065460119207279471943768595068414860014149178278435399371400930962253759162888062020269227529923329167742931240473191566641468995735758696802138379814852469042293401437137956018945170092817785505583108837356735852654194156319248319729732466320634812831259869290012647072233952795236462156213752008019954481267917886292498492055846838619027167304191382208021540244250507570410888356448310603088364351895116324030484480320862223729665151544010778941061938440283026451178172286282191179070116776836046514823559685428369094109958537683453915206656040875788707049636999992215238431149080323327641760705766913474027428111260788981734182250598573031877786769378931547452684486997457718460021235476398326984192784", - "0": "1106819115015372998825208031668263762670044285179584053573615157030968633235403572251376628759852167093842839880087972608252236859761641404161906797947710722723839146345785722305071566665021165225324459369096631275993154775947819333058721055644040212303789659749985973470385218248603826878093609862767077416104661216163222886987607841769251824856950498177308519655255200045046063857789306026581362754683975573850037114774186895901788964351934533171525025276070855270632786783019588176816118329221122549398793872640055312900842112041891936494042853798319986365192512964078607266631918748545903216736690057842950922926661694759259530843862322858400156976838479950950178486526234308178957984785053903260967594398611911474", - "3": "1344309321242892215222847471501532826517184846819833677474602495849657271930678291855112591971466462816524573183554788643533109793416521709602069842696124889558288092186793062177006244758779556603409762571362221142413760629539533275654542467194539359678435299002566931998816165917234259226849828723125451685169672272552524344813036153633311318760938874320338280443847065712732394378892985736654998112090834297537844732478643713076998558297751199030671616253345870616092528684635775411928128373368327191277066131632614473951005152162823879892345970535505519113833062530738837915987508410926372810518540478552946670006272356196419957933718303344632112441115930033837912179851905872564389256853587645059720488720795906498", - "1": "601693817301763663113031272722721908754633550776510238958619960119672962844730314111437951375084589705366750107667669458320527173785853103929038523863706300574327283273485302578112396814149563941340189390051835244496748959403476105143355455812570759887645896592114448469303958006046663589996470308366068555479184906610439541373120510006128200782324694975090482529033281708168823833732457689747330091963586305323138559502300486975246644545238909598413498252470653544977963083975726047754021026165970401681664501179243533611966433308438886961268871140737772352678991735861225177227793364352974323624694500485545573621034350559474030565509027433457718466600471361048730018443642651540442802817989514889987171548775560085", - }, - "mj": "2687197004064133543257369626470144380098036289489284489320086515026620206692616047425976133587124290887441908383692364439260071404270430528078491104384060203570606253676528361400", - "alpha": "55264634475788812054149982413198771839810724235465324658821557285735947681415835295178267002738090787902834904063083682990582592095393028970773939852521059447360650213986737569312363077820486616943853008592650537183003498185887824618357246364458614494253289122927160626742649252943244636915456680482390825080294565093068093917997001255757200832353046300646785756652682640188703523223073037006585218839054980180609464837830370157522462983934135435603408143309318659202555550473599548567996557919032937165600303958449173855781262863161799425917680286809410314205550551542955745937751254083650878398344461109371177805333303453760504594222290495116260958547048583654306199387054245295488649024179114894686831993370968945510894767150406222332165620064150891563554498413420757277508788138394747656372783710437243804659113648361274361422790365575", - "t": { - "2": "1276353167840913477021397624773394332173592088650367702185572394040398533199538101776458275797662881371280361310311170677242402214354355702620614537036611922064060504606618126681639882263139365680565350790281701009940301284340534766480451762902788628875609130151618956111512660983755135355570760793108220842022869639781026918247205511538713530652099730605791686827103126406846076633375908411453922078354225032716111673736810973402770388177401531928271370790938081733309345905963052715943136682338494175330354955277424030755355371412956250746882945100461786601740318616758180741835591171045104436982446340050589105952", - "0": "52506109491039096251755479392960889070840776962363540274456217953760113102006029814040519995494713986268145627084927516727099691151450378385140332116480118436738261593744184296007314732823898043080011956933010369575980799348117283597824162615912372823633177749168952698401203464607973674241038357379577293158404669765882906589960120865518413803711729942613061301420107178603192154873722316947550106277771120767826035047479123749931790881679576800340417944013614994751361795012191068369383577242249201927422484806926120532089036692818076818060938822432774203557319821915034796962936855918437128832683302834778450852076", - "1": "113031374658594175812052384858113115052077482873081996361152721528334589441352531310470368095073157716273853401381658707580502108484382463859531044307244944300120928991532655473230562771713806228238940140492981669914382036157400059197253018428984542349187927786210979478008036674432605219414300881116700073904513558719492127462395417843765324361843076852973933175787635618464392198807598044268223652564648024618437362752148593227485835178720349721798423100634521510710239416375840314170338898512726956877281625226003452828033987655579773273571285524048285234475184043290899568731903112287738739915600509899766360789888", - "DELTA": "48234611140928682288937615809872962358698394776719271528059766394227502012090856649758227578113306604028516575292703546202775777621049060595611852517094547384541819122623967215175704296901562660240718967260151010609870475975072516070346770954330313963878747194405855012585768501635077016535846206257741952202337055842434195875166686634891739392750890861333875772954056854354284061793365725202163447856793288398072711194949704852318180275797984445814279136152858759907525062790250675184786142234427994860090376938644764493873560454829155051260697226196758890394411645758956396137763703934929680277278644873416013261035", - "3": "89994605437763910628730772379416923861874648327020237340785010128698483324987645925227420742287632948945347297152300219419713493590120999381541274609870183955909628256613851122899039933589797230083354701292272442523280565440597314568786864750837443422276701528731625877274094155541825495114060437788769205202442723879088098866185978654728309516302335284177924161235100925296934812127598754913984676011716654143603885735997160890946409226842054727795290304131313120189570773196857529159798597569189742987994905034522112705638567954037460125246215182613760404547369876267284411245030884496328403051974209422359756153509", - }, - "predicate": { - "attr_name": "busid", - "p_type": "GE", - "value": 11198760, - }, - }, - { - "u": { - "0": "13639548796026429922431564475630909149287414026758460721805236736313279517016438050089911517098811596997747189614439260518531845477684148307804856579405503329745365642794423965550", - "2": "12692415150154152887167590190910159618471206042982658652940787170770193806407265717354418163057121876574358366510055892372348735991661901637525227498965237677355250159501068181772", - "3": "6699159556719214469836363462599679663866420825429540116943002714507804742697411533141864346616123740789790632843719915716457061440487115732563925309886301301835201778554620543295", - "1": "2018654799729593932888298230804022878883145101317651811950082851492082577094184498971399238402895197739207931768086301073280634251050932415705600476284738694155135236800581664160", - }, - "r": { - "1": "825587756964975640126314737718300012891046538726331178577448524710910340957817679849290109848304786342311186386453239759474660538454793939540876256076287017677140704068118361949660090673111340635478762304690817532764517905140299716866605223450803768338360729151901747687349983483402342999368967231581939563361347289212973086454185400770130710116840233323953976914342262402301362679497329671787598650893202541829399630505463177921655009726556920408538662140155815031458475909120161960047235187953148398737965729023268444789967620657212914775071615366971436269789139928904779054710447116218434690464549160131819794059427689273427325814904354192089075836597740878803445045080385629565176143354201573860707045668850877586", - "2": "1466408189748340763973829793343949568330918709265623621614464341218317503955515434953266875378586538446326464353600075579788794127665478299651259465473747112701101990004860122720151191106445704432013015062973865716673386400413561687311954374930156679604666267815298214479078026652043482916898087471155683856282470644588563159648375551108786970597383143516158031628710096807215305878905007543811401502472821013567629888746492557864905681554913361277548019219082051265255078152509205293776781790132507115787621452248689332496610099725566623311760857590035073594921664074567131690599897210005475078142722295326868452002437292574903183037228401231409631285848202575278151773369676950274790626198680132560950102001994557758", - "0": "993248502537248262082444202395290853332499246354083708269674970707520839045168624341335318664418224639164402187209309139427257892643191846187663592057257899679944076599283980872521437340751206357777926871742796186382563827967273141200749480590415594087209691507734426984052841712131263160951495974745152392404724577427973267669378931113495076274617344076060846279028767371296979484895771867209047720463195305161885422275388748188299814182891315332800557749699941587327916028930365349641271736635219800975554147836564077611147631789530042925759823398087582121686407890628257624663383236878047170688254415445440912626941967028065807021170264150964938678824504194752040131898249057197187446968567390619785928296680096859", - "3": "353677912339120670248802964352055631737613331947764251954000578577314223482877266750851861467829550374246392637478716468616296688578414836737374015352059254057436572686513161681724599053168679581126352074962010335889993562619355121275432902043064229165956511160994192882167562213269670332262473472293819501037932879123080023576285854568501212240875918139761976977842939660466373041805369493971290555885442554468124891943099059169515428968196495673746803133324864149723509564523971808556630671471618581233229134929554792186889060256901637092067130348403992303346483664985586122149628146304160243882639275298266216270358565584574585823864941692911554602002331492551293859949912337984877479524597804956696499812250631744", - "DELTA": "725960022886687948013207416539699149371621853290822104811918058808196468403337509381122781137942343897440199450987104988666229964851227549448628470704889721866971126265999067769808855341632931627785927114398786533559660381398895352266657934136549351825103362166280268159652759301507640976500533521688660251972577237532256300306442315564311264115224457865178259661593100327194825492692234619818096596609477148829377559407992257373097100180145505767561403356284282388735420784241021016181364636135275395790815788682767997871662899508826815736302921531147145381730507095314577476550947092200539059112480501048978059997520366967856033897452966490827003353334313372398949710717623991939354590550708881302450618430658953556", - }, - "mj": "6686713960986576137959547581149682718587656100042127047852747024276212127252400140409890726173570723819441146289878657604374865560165489933600436341950054222778208816245032311193", - "alpha": "54312418620368017823413444392364697511954099195960921763340447211826963863335156427497199363801396644023918819073111783414569539781613083737557451917064836287105910850065875966424715696905777194811070071499968289670886194094119693198719726955718469770568556811780821894560508716495412081426259363632509089906620904790770113618876886851367553577555822830824722182497761114967889600421573310792308390968429341290356015872285765321156360499004114406293720515635636721256956836801168192621092752489119545742530767529595705696014856308531466145146269599634259697543058622520958051728230537251854292098956994695268415292349999637984082162556184322623578612708830627477718675001902228134597558345283147625462346943667586065769392740787755841399970302076200764539143397370091692013055692886714129148712005056929884477612627289722508451690081998890", - "t": { - "3": "76842650074027971332631982512373611181628371639695357946107030911055453488768447213460618917725534086368162318588252003797289252473279448248400376609193928062810022884201102892017821282461806593568305060473753735848560048445524907113838106958747793434918052694775405184619214354190540002998204225798499364075579094271521191419027986291013493577021803670203051346914082929873231509819450163988354047777312127725561922611471445963909565688013793926876707562644935391518355932605047591545917637465241017629839541260483606708345518662351776889719949822005165906622964213143757683950646046295114922019124075069329268061942", - "2": "104991045405164906316724339229643785709360971949973916361929774804163421784479300621496063132861029493850348596359070365652827572699577454378465299784873962729586537933990712981855548459986825452865420618489151243413027040820258308949176618728507177438646401022030966936494703173837070422031040550750643315987178063356959004909489540688791639398005266908038895531691252968451136025538449648159989963830846794193607106472742567850015960071634812903985081979755017126350806404047244177458032873066418448813920685609285163826032405474833353441325867090653794998832828049943461795570528006274431422907140560130037296666626", - "0": "93372553202246858510371387492221683266873274595585378473760313800346458391438909787465170333251843314544241604938410847073082151810448655558028921073676767770394665021417900636520680814177493616534162641758512946743051333557436759523671912141418810442158225543010238061117969558203880853763255647243160765086932831295304550412607848190595598510980669944139696363322475177492264636536910201776020324858798972778323663795303339939472573927415127166116444898790357846635883222746031584554927383535016321617425087697872601850303134185636960112124926520878185699975818343081756286170638877967660840814776000077787223928056", - "1": "88156501142569212422367853754801651086852287000049991938144173063936879655987557042149874374683234911554850776079721311154420204826376746982087019508277766132575858575556680538019849786965963718649479179859820106973881788608463705644074554956818391872137271784803047333543321479251515998725336896820211102747123583803854741248907240437683401575881169746704849524328003061107258995982062254548684849294595639491104266371155934951313704136302996039897528196270331875472554549327417349243990461246127383357748906616773662459665620147625796186736530089927957522542298814250937114283836911153790542409683746775259226224961", - "DELTA": "6178997688515360528852083990605883033892934661031543684879979804577432521872124008044788245406591933749401429548633356472853716766388636618335206416158216292785839570827245139150787585027801572977051847786797012358936548986405917266204321163760568135873831346087680040040301251630530064206552793273933549993844498438010903850013120770715837075880286264742500598494248510064600863411203212869270221192303957773402376357672080257393369434247825409313396018869267942811592657266119556377402842108726474978400793026037873416208879964428023321485607453655856252140587803891157033568210852205447175844430607889546700526279", - }, - "predicate": { - "attr_name": "id", - "p_type": "GE", - "value": 4, - }, - }, - ], - }, - "non_revoc_proof": { - "x_list": { - "rho": "12B28F49BF5F2CDA105B904CD594EB5B5F558025CDDB7D0F3057D19830F81694", - "r": "010F2B872DC4BECAE085D9FA1FB98184C3E00181A678F2B256140482B4DEDFCE", - "r_prime": "1AAF1AB0071B64FE22AC42219962B9ABA02147B09DFDC4C1FD088E6706D312FC", - "r_prime_prime": "1630D0800ADE33816BCA96EE89EC1E312BE80220510FAFAAC52BED567B187017", - "r_prime_prime_prime": "14D06B2F7B369880821DAAFD40D74FE3B495EE3A7CB7E937FDC4E9707330F301", - "o": "147F16718A0CCB2EC137ECA3952C801FB2281B06CB4ADC24092CE31CA2EAC5AD", - "o_prime": "14F4668810341353E0C25485C4F3CF459BCB69DD74FF73376A23ACAA550E09C5", - "m": "0EAC754EE9AC81C495AC3BB302511034965965AF337BC4F43850A1636E70314E", - "m_prime": "07CA764055E9920E7C4417C3A63BF610C145F25D618A41DAC785210D7320F0EF", - "t": "199D84A1133FB103E4E77CC6F917A12355AD4A29DCCC24999A2C67EBD62B5306", - "t_prime": "07154F39841E3D75E1E07179875B94D655E3BDD4A349E0BBAA43422CC527AACB", - "m2": "0D4380FF8ACDC21611BC8AB83127950441DA42A49A347BEC6F32044F033D3017", - "s": "0A65AE9D0C0D4CDAA5D4EECB48BC6DFD2279BD2C040AC0D9011918A9E0A7A866", - "c": "0ABFC02DDF76995C48CADEE8447665EB933446FEC42E7074FB11720E141CFC07", - }, - "c_list": { - "e": "6 418D8713ED93CD8C065EA42D110C581C2CE07A58771077B1C2016E53AA2E7461 4 2032A4917D0877B9723CDCD82B32AC96C534B0CAA5ED2EE3FFD605214511CB1F 4 0D8E5DA074651A0DE91F86F663F071EA4D4CD4CBA438F7A4D182C8D23D01B485", - "d": "6 37635F35298D224C0E3F01EB06DC8AC1D8A7E048027162077E0204801F22FF94 4 1E64440E13B08BD249B5C35B637C70BDA471926F5F3896400ED25EDA4678B73D 4 3A5BB704B473894CD54C91D1D159A7BD8FA8092545F93D1BC195D52D3EC96EDE", - "a": "6 6000DC780B9D7C71575A328DE2BACB78A2737E9C1CE64BC8BCE98BD8486EAAB4 4 39555F38DB15EC820DA3A7A61820F831A003D414D4A0EF60D1D37ABD8B5E1070 4 25FBA1AD320F02D9082118E978B4FE261280951BCE1FED15F65771AE04F8E270", - "g": "6 5D293948EF43989ACBB8262B8C7F10A551AD71190D70B9AAA62943C4FE6A4C42 4 2B3E1ED0A00163DCA9AD9B69DDA124290CF8F580F60595B5E9D506A3C0D9A903 4 29C2B6F7AD7F7B223FC40BD9C1147FCE831B26ACB477855C9D3EABD7B0341342", - "w": "21 1371C921AE2377A1CD9F0D3E863B09487B6DFC0DC5F2BA32133E4F5EF2ACA5641 21 10B84BA9167755980B1DCD97AB698D56E3C9CDCBE7A85F0776A2C96B3BE9519BE 6 6676ADACEC607381F87211DAE4DE6A630B74FAF580DBC383D8450C7852BC09C4 4 379C9A4FF46DEBF21223823D5B2323F7A56A394157E94DB95914A9E5BB27FAEC 6 7121D621C85D9BA22371A0862909FF25198F0EF690207AEE3910FB0E0A7A4F62 4 1C052A0276360F0D8AEBA71BD65ECB233FFDB700F031EA03146CF00BC2F2D5B6", - "s": "21 1272F477F5A0F83CCB316DA088F6A6A12C131D0DC9BC699023F534624B8EE255A 21 13816855011465BE2E8972F52EE4692873A763513A764BD92B8B7CBBBAA27D7E8 6 7B190F599B5F0EA53802135BBD655B080743FE60CC22329F69770D6B765F0AAA 4 2AAA191CA59348C6A920BD1D1AE37A7C96F424B6D8E921B54EA5C1C7C56297AA 6 80254CA5DFBAD3C39BC757534922FBD0846AB86500D5D168109EB6B8A9D2BE33 4 1CC93B3769A7BE2AF52CCE391D2BB57F9D907F530038EF84B3EC4AB54D62D872", - "u": "21 11E538813B74EFC8676EF5AC87AA05A0FF58913B7C68E264FCF5ED0D57F6BC781 21 12EE7BE65E15CF4C500E2D92DB02670FBD8B51C6BD0B35AE139E9CE9658B15CC2 6 856B3C0C152F75A449AD73DFAD7DFD87A99AAA606E3D8E392D765E3C987D7B47 4 34245F01BD7C4144DBEBE7AB35303BF02FB5717EC6B080BC9C2C930D929D4ED7 6 8113F127D8762B174DCB32AEE370297BF7CFCCF797510B53D53564AEC9105909 4 3B2434AD9AB4E7ABA7125800D14470A098AE04FA523CB60A6FFF62D371B95E13", - }, - }, - } - ], - "aggregated_proof": { - "c_hash": "37672016063516849654668323232510746418703126727195560560658262517075578769045", - "c_list": [ - [4, 0, 0, 0, 0, 0], - [4, 17, 153, 0, 0, 0, 0], - [4, 0, 0, 0, 0, 0, 0, 0], - [4, 1, 134, 126, 0, 0, 0, 0], - [10, 250, 248, 125, 158, 54, 165, 91, 59, 1], - [4, 167, 169, 22, 44], - [31, 254, 53], - [118, 218, 1, 27, 51, 96], - [1, 254, 120, 236], - [3, 127, 97, 134, 148, 32, 128], - [10, 124, 191, 32], - [32, 59, 96, 254, 165], - [195, 171, 64, 72, 40, 235], - [2, 175, 185, 172, 248], - [2, 152, 166, 185, 65], - [3, 63, 176, 24, 2], - [2, 96, 182, 196, 220, 182, 246], - [48, 242, 116, 58, 18, 199], - ], - }, - }, - "requested_proof": { - "revealed_attrs": {}, - "revealed_attr_groups": { - "18_uuid": { - "sub_proof_index": 0, - "values": { - "effectiveDate": { - "raw": "2018-01-01", - "encoded": "29898645652691622994103043707423726726370719600737126045061047957925549204159", - }, - "endDate": { - "raw": "", - "encoded": "102987336249554097029535212322581322789799900648198034993379397001115665086549", - }, - "jurisdictionId": {"raw": "1", "encoded": "1"}, - "legalName": { - "raw": "Flan Nebula", - "encoded": "106000828387368179355563788886235175190145445419967766011746391966411797095112", - }, - "orgTypeId": {"raw": "2", "encoded": "2"}, - }, - } - }, - "self_attested_attrs": {}, - "unrevealed_attrs": {}, - "predicates": { - "18_busid_GE_uuid": {"sub_proof_index": 0}, - "18_id_GE_uuid": {"sub_proof_index": 0}, - }, - }, - "identifiers": [ - { - "schema_id": "LjgpST2rjsoxYegQDRm7EL:2:bc-reg:1.0", - "cred_def_id": "LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag", - "rev_reg_id": "LjgpST2rjsoxYegQDRm7EL:4:LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag:CL_ACCUM:0", - "timestamp": 1579892963, - } - ], -} - -REV_REG_DEFS = { - "LjgpST2rjsoxYegQDRm7EL:4:LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag:CL_ACCUM:0": { - "txnTime": 1500000000 - } -} - - -@pytest.mark.indy -class TestIndySdkVerifier(IsolatedAsyncioTestCase): - def setUp(self): - self.ledger = mock.MagicMock( - get_credential_definition=mock.CoroutineMock( - return_value={ - "...": "...", - "value": { - "revocation": { - "g": "1 ...", - "g_dash": "1 ...", - "h": "1 ...", - "h0": "1 ...", - "h1": "1 ...", - "h2": "1 ...", - "htilde": "1 ...", - "h_cap": "1 ...", - "u": "1 ...", - "pk": "1 ...", - "y": "1 ...", - } - }, - } - ) - ) - mock_profile = InMemoryProfile.test_profile() - context = mock_profile.context - context.injector.bind_instance( - IndyLedgerRequestsExecutor, IndyLedgerRequestsExecutor(mock_profile) - ) - self.verifier = IndySdkVerifier(mock_profile) - assert repr(self.verifier) == "" - - @mock.patch("indy.anoncreds.verifier_verify_proof") - async def test_verify_presentation(self, mock_verify): - mock_verify.return_value = "val" - - with mock.patch.object( - self.verifier, "pre_verify", mock.CoroutineMock() - ) as mock_pre_verify, mock.patch.object( - self.verifier, "non_revoc_intervals", mock.MagicMock() - ) as mock_non_revox, mock.patch.object( - IndyLedgerRequestsExecutor, "get_ledger_for_identifier" - ) as mock_get_ledger: - mock_get_ledger.return_value = (None, self.ledger) - INDY_PROOF_REQ_X = deepcopy(INDY_PROOF_REQ_PRED_NAMES) - (verified, msgs) = await self.verifier.verify_presentation( - INDY_PROOF_REQ_X, - INDY_PROOF_PRED_NAMES, - "schemas", - {"LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag": {"value": {"revocation": {}}}}, - REV_REG_DEFS, - "rev_reg_entries", - ) - - mock_verify.assert_called_once_with( - json.dumps(INDY_PROOF_REQ_X), - json.dumps(INDY_PROOF_PRED_NAMES), - json.dumps("schemas"), - json.dumps( - {"LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag": {"value": {"revocation": {}}}} - ), - json.dumps(REV_REG_DEFS), - json.dumps("rev_reg_entries"), - ) - - assert verified == "val" - - @mock.patch("indy.anoncreds.verifier_verify_proof") - async def test_verify_presentation_x_indy(self, mock_verify): - mock_verify.side_effect = IndyError(error_code=1) - - with mock.patch.object( - self.verifier, "pre_verify", mock.CoroutineMock() - ) as mock_pre_verify, mock.patch.object( - self.verifier, "non_revoc_intervals", mock.MagicMock() - ) as mock_non_revox, mock.patch.object( - IndyLedgerRequestsExecutor, "get_ledger_for_identifier" - ) as mock_get_ledger: - mock_get_ledger.return_value = ("test", self.ledger) - (verified, msgs) = await self.verifier.verify_presentation( - INDY_PROOF_REQ_NAME, - INDY_PROOF_NAME, - "schemas", - {"LjgpST2rjsoxYegQDRm7EL:3:CL:19:tag": {"value": {}}}, - REV_REG_DEFS, - "rev_reg_entries", - ) - - mock_verify.assert_called_once_with( - json.dumps(INDY_PROOF_REQ_NAME), - json.dumps(INDY_PROOF_NAME), - json.dumps("schemas"), - json.dumps({"LjgpST2rjsoxYegQDRm7EL:3:CL:19:tag": {"value": {}}}), - json.dumps(REV_REG_DEFS), - json.dumps("rev_reg_entries"), - ) - - assert not verified - - @mock.patch("indy.anoncreds.verifier_verify_proof") - async def test_check_encoding_attr(self, mock_verify): - with mock.patch.object( - IndyLedgerRequestsExecutor, "get_ledger_for_identifier" - ) as mock_get_ledger: - mock_get_ledger.return_value = (None, self.ledger) - mock_verify.return_value = True - (verified, msgs) = await self.verifier.verify_presentation( - INDY_PROOF_REQ_NAME, - INDY_PROOF_NAME, - "schemas", - {"LjgpST2rjsoxYegQDRm7EL:3:CL:19:tag": {"value": {}}}, - REV_REG_DEFS, - "rev_reg_entries", - ) - - mock_verify.assert_called_once_with( - json.dumps(INDY_PROOF_REQ_NAME), - json.dumps(INDY_PROOF_NAME), - json.dumps("schemas"), - json.dumps({"LjgpST2rjsoxYegQDRm7EL:3:CL:19:tag": {"value": {}}}), - json.dumps(REV_REG_DEFS), - json.dumps("rev_reg_entries"), - ) - assert verified is True - assert len(msgs) == 1 - assert "TS_OUT_NRI::19_uuid" in msgs - - @mock.patch("indy.anoncreds.verifier_verify_proof") - async def test_check_encoding_attr_tamper_raw(self, mock_verify): - INDY_PROOF_X = deepcopy(INDY_PROOF_NAME) - INDY_PROOF_X["requested_proof"]["revealed_attrs"]["19_uuid"][ - "raw" - ] = "Mock chicken" - with mock.patch.object( - IndyLedgerRequestsExecutor, "get_ledger_for_identifier" - ) as mock_get_ledger: - mock_get_ledger.return_value = ("test", self.ledger) - (verified, msgs) = await self.verifier.verify_presentation( - INDY_PROOF_REQ_NAME, - INDY_PROOF_X, - "schemas", - {"LjgpST2rjsoxYegQDRm7EL:3:CL:19:tag": {"value": {}}}, - REV_REG_DEFS, - "rev_reg_entries", - ) - - mock_verify.assert_not_called() - - assert verified is False - assert len(msgs) == 2 - assert "TS_OUT_NRI::19_uuid" in msgs - assert ( - "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs - ) - - @mock.patch("indy.anoncreds.verifier_verify_proof") - async def test_check_encoding_attr_tamper_encoded(self, mock_verify): - INDY_PROOF_X = deepcopy(INDY_PROOF_NAME) - INDY_PROOF_X["requested_proof"]["revealed_attrs"]["19_uuid"][ - "encoded" - ] = "1234567890" - with mock.patch.object( - IndyLedgerRequestsExecutor, "get_ledger_for_identifier" - ) as mock_get_ledger: - mock_get_ledger.return_value = (None, self.ledger) - (verified, msgs) = await self.verifier.verify_presentation( - INDY_PROOF_REQ_NAME, - INDY_PROOF_X, - "schemas", - {"LjgpST2rjsoxYegQDRm7EL:3:CL:19:tag": {"value": {}}}, - REV_REG_DEFS, - "rev_reg_entries", - ) - - mock_verify.assert_not_called() - - assert verified is False - assert len(msgs) == 2 - assert "TS_OUT_NRI::19_uuid" in msgs - assert ( - "VALUE_ERROR::Encoded representation mismatch for 'Preferred Name'" in msgs - ) - - @mock.patch("indy.anoncreds.verifier_verify_proof") - async def test_check_pred_names(self, mock_verify): - with mock.patch.object( - IndyLedgerRequestsExecutor, "get_ledger_for_identifier" - ) as mock_get_ledger: - mock_get_ledger.return_value = ("test", self.ledger) - mock_verify.return_value = True - INDY_PROOF_REQ_X = deepcopy(INDY_PROOF_REQ_PRED_NAMES) - (verified, msgs) = await self.verifier.verify_presentation( - INDY_PROOF_REQ_X, - INDY_PROOF_PRED_NAMES, - "schemas", - {"LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag": {"value": {"revocation": {}}}}, - REV_REG_DEFS, - "rev_reg_entries", - ) - - mock_verify.assert_called_once_with( - json.dumps(INDY_PROOF_REQ_X), - json.dumps(INDY_PROOF_PRED_NAMES), - json.dumps("schemas"), - json.dumps( - {"LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag": {"value": {"revocation": {}}}} - ), - json.dumps(REV_REG_DEFS), - json.dumps("rev_reg_entries"), - ) - - assert verified is True - assert len(msgs) == 3 - assert "TS_OUT_NRI::18_uuid" in msgs - assert "TS_OUT_NRI::18_id_GE_uuid" in msgs - assert "TS_OUT_NRI::18_busid_GE_uuid" in msgs - - @mock.patch("indy.anoncreds.verifier_verify_proof") - async def test_check_pred_names_tamper_pred_value(self, mock_verify): - INDY_PROOF_X = deepcopy(INDY_PROOF_PRED_NAMES) - INDY_PROOF_X["proof"]["proofs"][0]["primary_proof"]["ge_proofs"][0][ - "predicate" - ]["value"] = 0 - with mock.patch.object( - IndyLedgerRequestsExecutor, "get_ledger_for_identifier" - ) as mock_get_ledger: - mock_get_ledger.return_value = (None, self.ledger) - (verified, msgs) = await self.verifier.verify_presentation( - deepcopy(INDY_PROOF_REQ_PRED_NAMES), - INDY_PROOF_X, - "schemas", - {"LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag": {"value": {}}}, - REV_REG_DEFS, - "rev_reg_entries", - ) - - mock_verify.assert_not_called() - - assert verified is False - assert len(msgs) == 4 - assert "RMV_RFNT_NRI::18_uuid" in msgs - assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs - assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs - assert ( - "VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid" - in msgs - ) - - @mock.patch("indy.anoncreds.verifier_verify_proof") - async def test_check_pred_names_tamper_pred_req_attr(self, mock_verify): - INDY_PROOF_REQ_X = deepcopy(INDY_PROOF_REQ_PRED_NAMES) - INDY_PROOF_REQ_X["requested_predicates"]["18_busid_GE_uuid"]["name"] = "dummy" - with mock.patch.object( - IndyLedgerRequestsExecutor, "get_ledger_for_identifier" - ) as mock_get_ledger: - mock_get_ledger.return_value = (None, self.ledger) - (verified, msgs) = await self.verifier.verify_presentation( - INDY_PROOF_REQ_X, - INDY_PROOF_PRED_NAMES, - "schemas", - {"LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag": {"value": {}}}, - REV_REG_DEFS, - "rev_reg_entries", - ) - - mock_verify.assert_not_called() - - assert verified is False - assert len(msgs) == 4 - assert "RMV_RFNT_NRI::18_uuid" in msgs - assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs - assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs - assert ( - "VALUE_ERROR::Timestamp on sub-proof #0 is superfluous vs. requested attribute group 18_uuid" - in msgs - ) - - @mock.patch("indy.anoncreds.verifier_verify_proof") - async def test_check_pred_names_tamper_attr_groups(self, mock_verify): - INDY_PROOF_X = deepcopy(INDY_PROOF_PRED_NAMES) - INDY_PROOF_X["requested_proof"]["revealed_attr_groups"]["x_uuid"] = ( - INDY_PROOF_X["requested_proof"]["revealed_attr_groups"].pop("18_uuid") - ) - with mock.patch.object( - IndyLedgerRequestsExecutor, "get_ledger_for_identifier" - ) as mock_get_ledger: - mock_get_ledger.return_value = ("test", self.ledger) - (verified, msgs) = await self.verifier.verify_presentation( - deepcopy(INDY_PROOF_REQ_PRED_NAMES), - INDY_PROOF_X, - "schemas", - {"LjgpST2rjsoxYegQDRm7EL:3:CL:18:tag": {"value": {}}}, - REV_REG_DEFS, - "rev_reg_entries", - ) - - mock_verify.assert_not_called() - - assert verified is False - assert len(msgs) == 3 - assert "RMV_RFNT_NRI::18_busid_GE_uuid" in msgs - assert "RMV_RFNT_NRI::18_id_GE_uuid" in msgs - assert "VALUE_ERROR::Missing requested attribute group 18_uuid" in msgs diff --git a/aries_cloudagent/indy/sdk/tests/test_wallet_plugin.py b/aries_cloudagent/indy/sdk/tests/test_wallet_plugin.py deleted file mode 100644 index 1ca4f752f5..0000000000 --- a/aries_cloudagent/indy/sdk/tests/test_wallet_plugin.py +++ /dev/null @@ -1,125 +0,0 @@ -from unittest import mock -from unittest import IsolatedAsyncioTestCase - - -from .. import wallet_plugin as test_module - - -class TestWalletCrypto(IsolatedAsyncioTestCase): - def setUp(self): - test_module.LOADED = False - - async def test_file_ext(self): - assert test_module.file_ext() - - def test_load_postgres_plugin(self): - storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}' - storage_creds = '{"account":"test"}' - mock_stg_lib = mock.MagicMock( - postgresstorage_init=mock.MagicMock(return_value=0), - init_storagetype=mock.MagicMock(return_value=0), - ) - with mock.patch.object( - test_module.cdll, "LoadLibrary", mock.Mock() - ) as mock_load: - mock_load.return_value = mock_stg_lib - test_module.load_postgres_plugin(storage_config, storage_creds) - - assert test_module.LOADED - - def test_load_postgres_plugin_init_x_raise(self): - storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}' - storage_creds = '{"account":"test"}' - mock_stg_lib = mock.MagicMock( - postgresstorage_init=mock.MagicMock(return_value=2) - ) - with mock.patch.object( - test_module.cdll, "LoadLibrary", mock.Mock() - ) as mock_load: - mock_load.return_value = mock_stg_lib - with self.assertRaises(OSError) as context: - test_module.load_postgres_plugin( - storage_config, storage_creds, raise_exc=True - ) - assert "unable to load postgres" in str(context.exception) - - def test_load_postgres_plugin_init_x_exit(self): - storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}' - storage_creds = '{"account":"test"}' - mock_stg_lib = mock.MagicMock( - postgresstorage_init=mock.MagicMock(return_value=2) - ) - with mock.patch.object( - test_module.cdll, "LoadLibrary", mock.Mock() - ) as mock_load: - mock_load.return_value = mock_stg_lib - with self.assertRaises(SystemExit): - test_module.load_postgres_plugin( - storage_config, storage_creds, raise_exc=False - ) - - def test_load_postgres_plugin_config_x_raise(self): - storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}' - storage_creds = '{"account":"test"}' - mock_stg_lib = mock.MagicMock( - postgresstorage_init=mock.MagicMock(return_value=0), - init_storagetype=mock.MagicMock(return_value=2), - ) - with mock.patch.object( - test_module.cdll, "LoadLibrary", mock.Mock() - ) as mock_load: - mock_load.return_value = mock_stg_lib - with self.assertRaises(OSError) as context: - test_module.load_postgres_plugin( - storage_config, storage_creds, raise_exc=True - ) - assert "unable to configure postgres" in str(context.exception) - - def test_load_postgres_plugin_config_x_exit(self): - storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}' - storage_creds = '{"account":"test"}' - mock_stg_lib = mock.MagicMock( - postgresstorage_init=mock.MagicMock(return_value=0), - init_storagetype=mock.MagicMock(return_value=2), - ) - with mock.patch.object( - test_module.cdll, "LoadLibrary", mock.Mock() - ) as mock_load: - mock_load.return_value = mock_stg_lib - with self.assertRaises(SystemExit): - test_module.load_postgres_plugin( - storage_config, storage_creds, raise_exc=False - ) - - def test_load_postgres_plugin_bad_json_x_raise(self): - storage_config = '{"wallet_scheme":"MultiWalletSingleTable"}' - storage_creds = '"account":"test"' - mock_stg_lib = mock.MagicMock( - postgresstorage_init=mock.MagicMock(return_value=0), - init_storagetype=mock.MagicMock(return_value=2), - ) - with mock.patch.object( - test_module.cdll, "LoadLibrary", mock.Mock() - ) as mock_load: - mock_load.return_value = mock_stg_lib - with self.assertRaises(OSError) as context: - test_module.load_postgres_plugin( - storage_config, storage_creds, raise_exc=True - ) - assert "Invalid stringified JSON input" in str(context.exception) - - def test_load_postgres_plugin_bad_json_x_exit(self): - storage_config = '"wallet_scheme":"MultiWalletSingleTable"' - storage_creds = '{"account":"test"}' - mock_stg_lib = mock.MagicMock( - postgresstorage_init=mock.MagicMock(return_value=0), - init_storagetype=mock.MagicMock(return_value=2), - ) - with mock.patch.object( - test_module.cdll, "LoadLibrary", mock.Mock() - ) as mock_load: - mock_load.return_value = mock_stg_lib - with self.assertRaises(SystemExit): - test_module.load_postgres_plugin( - storage_config, storage_creds, raise_exc=False - ) diff --git a/aries_cloudagent/indy/sdk/util.py b/aries_cloudagent/indy/sdk/util.py deleted file mode 100644 index 549d06e965..0000000000 --- a/aries_cloudagent/indy/sdk/util.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Indy utilities.""" - -import json - -from pathlib import Path - -import indy.blob_storage - - -async def create_tails_reader(tails_file_path: str) -> int: - """Get a handle for the blob_storage file reader.""" - tails_file_path = Path(tails_file_path) - - if not tails_file_path.exists(): - raise FileNotFoundError("Tails file does not exist.") - - tails_reader_config = json.dumps( - { - "base_dir": str(tails_file_path.parent.absolute()), - "file": str(tails_file_path.name), - } - ) - return await indy.blob_storage.open_reader("default", tails_reader_config) - - -async def create_tails_writer(tails_base_dir: str) -> int: - """Get a handle for the blob_storage file writer.""" - tails_writer_config = json.dumps({"base_dir": tails_base_dir, "uri_pattern": ""}) - return await indy.blob_storage.open_writer("default", tails_writer_config) diff --git a/aries_cloudagent/indy/sdk/verifier.py b/aries_cloudagent/indy/sdk/verifier.py deleted file mode 100644 index 81bdc5a601..0000000000 --- a/aries_cloudagent/indy/sdk/verifier.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Indy SDK verifier implementation.""" - -import json -import logging - -from typing import Tuple - -import indy.anoncreds -from indy.error import IndyError - -from ...core.profile import Profile - -from ..verifier import IndyVerifier, PresVerifyMsg - -LOGGER = logging.getLogger(__name__) - - -class IndySdkVerifier(IndyVerifier): - """Indy-SDK verifier implementation.""" - - def __init__(self, profile: Profile): - """Initialize an IndyVerifier instance. - - Args: - profile: Active Profile instance - - """ - self.profile = profile - - async def verify_presentation( - self, - pres_req, - pres, - schemas, - credential_definitions, - rev_reg_defs, - rev_reg_entries, - ) -> Tuple[bool, list]: - """Verify a presentation. - - Args: - pres_req: Presentation request data - pres: Presentation data - schemas: Schema data - credential_definitions: credential definition data - rev_reg_defs: revocation registry definitions - rev_reg_entries: revocation registry entries - """ - - LOGGER.debug(f">>> received presentation: {pres}") - LOGGER.debug(f">>> for pres_req: {pres_req}") - msgs = [] - try: - msgs += self.non_revoc_intervals(pres_req, pres, credential_definitions) - msgs += await self.check_timestamps( - self.profile, pres_req, pres, rev_reg_defs - ) - msgs += await self.pre_verify(pres_req, pres) - except ValueError as err: - s = str(err) - msgs.append(f"{PresVerifyMsg.PRES_VALUE_ERROR.value}::{s}") - LOGGER.error( - f"Presentation on nonce={pres_req['nonce']} " - f"cannot be validated: {str(err)}" - ) - return (False, msgs) - - LOGGER.debug(f">>> verifying presentation: {pres}") - LOGGER.debug(f">>> for pres_req: {pres_req}") - try: - verified = await indy.anoncreds.verifier_verify_proof( - json.dumps(pres_req), - json.dumps(pres), - json.dumps(schemas), - json.dumps(credential_definitions), - json.dumps(rev_reg_defs), - json.dumps(rev_reg_entries), - ) - except IndyError as err: - s = str(err) - msgs.append(f"{PresVerifyMsg.PRES_VERIFY_ERROR.value}::{s}") - LOGGER.exception( - f"Validation of presentation on nonce={pres_req['nonce']} " - "failed with error" - ) - verified = False - - return (verified, msgs) diff --git a/aries_cloudagent/indy/sdk/wallet_plugin.py b/aries_cloudagent/indy/sdk/wallet_plugin.py deleted file mode 100644 index 8dbdb24775..0000000000 --- a/aries_cloudagent/indy/sdk/wallet_plugin.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Utility for loading Postgres wallet plug-in.""" - -import logging -import platform -import json -from ctypes import cdll, c_char_p - -EXTENSION = {"darwin": ".dylib", "linux": ".so", "win32": ".dll", "windows": ".dll"} -LOADED = False -LOGGER = logging.getLogger(__name__) - - -def file_ext(): - """Determine file extension based on platform.""" - your_platform = platform.system().lower() - return EXTENSION[your_platform] if (your_platform in EXTENSION) else ".so" - - -def load_postgres_plugin(storage_config, storage_creds, raise_exc=False): - """Load postgres dll and configure postgres wallet.""" - global LOADED, LOGGER - - if not LOADED: - LOGGER.info( - "Checking input postgres storage_config and storage_creds arguments" - ) - try: - json.loads(storage_config) - json.loads(storage_creds) - except json.decoder.JSONDecodeError: - LOGGER.error( - "Invalid stringified JSON input, check storage_config and storage_creds" - ) - if raise_exc: - raise OSError( - "Invalid stringified JSON input, " - "check storage_config and storage_creds" - ) - else: - raise SystemExit(1) - - LOGGER.info("Initializing postgres wallet") - stg_lib = cdll.LoadLibrary("libindystrgpostgres" + file_ext()) - result = stg_lib.postgresstorage_init() - if result != 0: - LOGGER.error("Error unable to load postgres wallet storage: %s", result) - if raise_exc: - raise OSError(f"Error unable to load postgres wallet storage: {result}") - else: - raise SystemExit(1) - if "wallet_scheme" in storage_config: - c_config = c_char_p(storage_config.encode("utf-8")) - c_credentials = c_char_p(storage_creds.encode("utf-8")) - result = stg_lib.init_storagetype(c_config, c_credentials) - if result != 0: - LOGGER.error("Error unable to configure postgres stg: %s", result) - if raise_exc: - raise OSError(f"Error unable to configure postgres stg: {result}") - else: - raise SystemExit(1) - LOADED = True - - LOGGER.info("Success, loaded postgres wallet storage") diff --git a/aries_cloudagent/indy/sdk/wallet_setup.py b/aries_cloudagent/indy/sdk/wallet_setup.py deleted file mode 100644 index 59e8dc3921..0000000000 --- a/aries_cloudagent/indy/sdk/wallet_setup.py +++ /dev/null @@ -1,233 +0,0 @@ -"""Indy-SDK wallet setup and configuration.""" - -import json -import logging - -from typing import Any, Mapping - -import indy.anoncreds -import indy.did -import indy.crypto -import indy.wallet - -from indy.error import IndyError, ErrorCode - -from ...core.error import ProfileError, ProfileDuplicateError, ProfileNotFoundError -from ...core.profile import Profile - -from .error import IndyErrorHandler -from .wallet_plugin import load_postgres_plugin - -LOGGER = logging.getLogger(__name__) - - -class IndyWalletConfig: - """A helper class for handling Indy-SDK wallet configuration.""" - - DEFAULT_FRESHNESS = False - DEFAULT_KEY = "" - DEFAULT_KEY_DERIVATION = "ARGON2I_MOD" - DEFAULT_STORAGE_TYPE = None - - KEY_DERIVATION_RAW = "RAW" - KEY_DERIVATION_ARGON2I_INT = "ARGON2I_INT" - KEY_DERIVATION_ARGON2I_MOD = "ARGON2I_MOD" - - def __init__(self, config: Mapping[str, Any] = None): - """Initialize an `IndySdkWalletConfig` instance. - - Args: - config: {name, key, seed, did, auto_recreate, auto_remove, - storage_type, storage_config, storage_creds} - - """ - - config = config or {} - self.auto_recreate = config.get("auto_recreate", False) - self.auto_remove = config.get("auto_remove", False) - self.freshness_time = config.get("freshness_time", self.DEFAULT_FRESHNESS) - self.key = config.get("key", self.DEFAULT_KEY) - self.key_derivation_method = ( - config.get("key_derivation_method") or self.DEFAULT_KEY_DERIVATION - ) - # self.rekey = config.get("rekey") - # self.rekey_derivation_method = config.get("rekey_derivation_method") - self.name = config.get("name") or Profile.DEFAULT_NAME - self.storage_type = config.get("storage_type") or self.DEFAULT_STORAGE_TYPE - self.storage_config = config.get("storage_config", None) - self.storage_creds = config.get("storage_creds", None) - - if self.storage_type == "postgres_storage": - load_postgres_plugin(self.storage_config, self.storage_creds) - - @property - def wallet_config(self) -> dict: - """Accessor for the Indy wallet config.""" - ret = { - "id": self.name, - "freshness_time": self.freshness_time, - "storage_type": self.storage_type, - } - if self.storage_config is not None: - ret["storage_config"] = json.loads(self.storage_config) - return ret - - @property - def wallet_access(self) -> dict: - """Accessor the Indy wallet access info.""" - ret = {"key": self.key, "key_derivation_method": self.key_derivation_method} - # if self.rekey: - # ret["rekey"] = self.rekey - # if self.rekey_derivation_method: - # ret["rekey_derivation_method"] = self.rekey_derivation_method - if self.storage_creds is not None: - ret["storage_credentials"] = json.loads(self.storage_creds) - return ret - - async def create_wallet(self) -> "IndyOpenWallet": - """Create a new wallet. - - Raises: - ProfileDuplicateError: If there was an existing wallet with the same name - ProfileError: If there was a problem removing the wallet - ProfileError: If there was another libindy error - - """ - if self.auto_recreate: - try: - await self.remove_wallet() - except ProfileNotFoundError: - pass - try: - await indy.wallet.create_wallet( - config=json.dumps(self.wallet_config), - credentials=json.dumps(self.wallet_access), - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletAlreadyExistsError: - raise IndyErrorHandler.wrap_error( - x_indy, - f"Cannot create wallet '{self.name}', already exists", - ProfileDuplicateError, - ) from x_indy - raise IndyErrorHandler.wrap_error( - x_indy, - f"Error creating wallet '{self.name}'", - ProfileError, - ) from x_indy - - try: - return await self.open_wallet(created=True) - except ProfileNotFoundError as err: - raise ProfileError( - f"Wallet '{self.name}' not found after creation" - ) from err - - async def remove_wallet(self): - """Remove an existing wallet. - - Raises: - ProfileNotFoundError: If the wallet could not be found - ProfileError: If there was another libindy error - - """ - try: - await indy.wallet.delete_wallet( - config=json.dumps(self.wallet_config), - credentials=json.dumps(self.wallet_access), - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletNotFoundError: - raise IndyErrorHandler.wrap_error( - x_indy, - f"Wallet '{self.name}' not found", - ProfileNotFoundError, - ) from x_indy - raise IndyErrorHandler.wrap_error( - x_indy, f"Error removing wallet '{self.name}'", ProfileError - ) from x_indy - - async def open_wallet(self, created: bool = False) -> "IndyOpenWallet": - """Open wallet, removing and/or creating it if so configured. - - Raises: - ProfileError: If wallet not found after creation - ProfileNotFoundError: If the wallet is not found - ProfileError: If the wallet is already open - ProfileError: If there is another libindy error - - """ - handle = None - - while True: - try: - handle = await indy.wallet.open_wallet( - config=json.dumps(self.wallet_config), - credentials=json.dumps(self.wallet_access), - ) - # if self.rekey: - # self.key = self.rekey - # self.rekey = None - # if self.rekey_derivation_method: - # self.key_derivation_method = self.rekey_derivation_method - # self.rekey_derivation_method = None - break - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletNotFoundError: - raise IndyErrorHandler.wrap_error( - x_indy, f"Wallet '{self.name}' not found", ProfileNotFoundError - ) from x_indy - elif x_indy.error_code == ErrorCode.WalletAlreadyOpenedError: - raise IndyErrorHandler.wrap_error( - x_indy, f"Wallet '{self.name}' is already open", ProfileError - ) from x_indy - else: - raise IndyErrorHandler.wrap_error( - x_indy, f"Error opening wallet '{self.name}'", ProfileError - ) from x_indy - - LOGGER.info("Creating master secret...") - try: - master_secret_id = await indy.anoncreds.prover_create_master_secret( - handle, self.name - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.AnoncredsMasterSecretDuplicateNameError: - LOGGER.info("Master secret already exists") - master_secret_id = self.name - else: - raise IndyErrorHandler.wrap_error( - x_indy, f"Wallet '{self.name}' error", ProfileError - ) from x_indy - - return IndyOpenWallet(self, created, handle, master_secret_id) - - -class IndyOpenWallet: - """Handle and metadata for an opened Indy wallet.""" - - def __init__( - self, - config: IndyWalletConfig, - created, - handle, - master_secret_id: str, - ): - """Create a new IndyOpenWallet instance.""" - self.config = config - self.created = created - self.handle = handle - self.master_secret_id = master_secret_id - - @property - def name(self) -> str: - """Accessor for the opened wallet name.""" - return self.config.name - - async def close(self): - """Close previously-opened wallet, removing it if so configured.""" - if self.handle: - await indy.wallet.close_wallet(self.handle) - self.handle = None - if self.config.auto_remove: - await self.config.remove_wallet() diff --git a/aries_cloudagent/indy/tests/test_verifier.py b/aries_cloudagent/indy/tests/test_verifier.py index 54c836d42a..a9bfef2734 100644 --- a/aries_cloudagent/indy/tests/test_verifier.py +++ b/aries_cloudagent/indy/tests/test_verifier.py @@ -1,5 +1,3 @@ -import pytest - from copy import deepcopy from time import time @@ -306,7 +304,6 @@ async def verify_presentation( raise NotImplementedError() -@pytest.mark.indy class TestIndySdkVerifier(IsolatedAsyncioTestCase): def setUp(self): self.ledger = mock.MagicMock( diff --git a/aries_cloudagent/ledger/base.py b/aries_cloudagent/ledger/base.py index 1ebc8926c5..509dc48d4b 100644 --- a/aries_cloudagent/ledger/base.py +++ b/aries_cloudagent/ledger/base.py @@ -643,7 +643,7 @@ async def send_schema_anoncreds( try: legacy_indy_registry = LegacyIndyRegistry() resp = await legacy_indy_registry.txn_submit( - self.profile, + self, schema_req, sign=True, sign_did=public_info, diff --git a/aries_cloudagent/ledger/indy.py b/aries_cloudagent/ledger/indy.py deleted file mode 100644 index 9dc18cbc7a..0000000000 --- a/aries_cloudagent/ledger/indy.py +++ /dev/null @@ -1,1293 +0,0 @@ -"""Indy ledger implementation.""" - -import asyncio -import json -import logging -import tempfile -from datetime import date, datetime -from io import StringIO -from os import path -from time import time -from typing import TYPE_CHECKING, List, Optional, Tuple - -import indy.ledger -import indy.pool -from indy.error import ErrorCode, IndyError - -from ..cache.base import BaseCache -from ..config.base import BaseInjector, BaseProvider, BaseSettings -from ..indy.sdk.error import IndyErrorHandler -from ..storage.base import StorageRecord -from ..storage.indy import IndySdkStorage -from ..utils import sentinel -from ..wallet.base import BaseWallet -from ..wallet.did_info import DIDInfo -from ..wallet.did_posture import DIDPosture -from ..wallet.error import WalletNotFoundError -from ..wallet.util import full_verkey -from .base import BaseLedger, Role -from .endpoint_type import EndpointType -from .error import ( - BadLedgerRequestError, - ClosedPoolError, - LedgerConfigError, - LedgerError, - LedgerTransactionError, -) -from .util import TAA_ACCEPTED_RECORD_TYPE - -if TYPE_CHECKING: - from ..indy.sdk.profile import IndySdkProfile - -LOGGER = logging.getLogger(__name__) - -GENESIS_TRANSACTION_FILE = "indy_genesis_transactions.txt" - - -def _normalize_txns(txns: str) -> str: - """Normalize a set of genesis transactions.""" - lines = StringIO() - for line in txns.splitlines(): - line = line.strip() - if line: - lines.write(line) - lines.write("\n") - return lines.getvalue() - - -class IndySdkLedgerPoolProvider(BaseProvider): - """Indy ledger pool provider which keys off the selected pool name.""" - - def provide(self, settings: BaseSettings, injector: BaseInjector): - """Create and open the pool instance.""" - - pool_name = settings.get("ledger.pool_name", "default") - keepalive = int(settings.get("ledger.keepalive", 5)) - read_only = bool(settings.get("ledger.read_only", False)) - socks_proxy = settings.get("ledger.socks_proxy") - - if read_only: - LOGGER.warning("Note: setting ledger to read-only mode") - - genesis_transactions = settings.get("ledger.genesis_transactions") - cache = injector.inject_or(BaseCache) - - ledger_pool = IndySdkLedgerPool( - pool_name, - keepalive=keepalive, - cache=cache, - genesis_transactions=genesis_transactions, - read_only=read_only, - socks_proxy=socks_proxy, - ) - - return ledger_pool - - -class IndySdkLedgerPool: - """Indy ledger manager class.""" - - def __init__( - self, - name: str, - *, - checked: bool = False, - keepalive: int = 0, - cache: BaseCache = None, - cache_duration: int = 600, - genesis_transactions: str = None, - read_only: bool = False, - socks_proxy: str = None, - ): - """Initialize an IndySdkLedgerPool instance. - - Args: - name: The Indy pool ledger configuration name - keepalive: How many seconds to keep the ledger open - cache: The cache instance to use - cache_duration: The TTL for ledger cache entries - genesis_transactions: The ledger genesis transaction as a string - read_only: Prevent any ledger write operations - socks_proxy: Specifies socks proxy for ZMQ to connect to ledger pool - """ - self.checked = checked - self.opened = False - self.ref_count = 0 - self.ref_lock = asyncio.Lock() - self.keepalive = keepalive - self.close_task: asyncio.Future = None - self.cache = cache - self.cache_duration = cache_duration - self.genesis_transactions = genesis_transactions - self.genesis_txns_cache = genesis_transactions - self.handle = None - self.name = name - self.taa_cache = None - self.read_only = read_only - self.socks_proxy = socks_proxy - - @property - def genesis_txns(self) -> str: - """Get the configured genesis transactions.""" - if not self.genesis_txns_cache: - try: - txn_path = path.join( - tempfile.gettempdir(), f"{self.name}_{GENESIS_TRANSACTION_FILE}" - ) - self.genesis_txns_cache = _normalize_txns(open(txn_path).read()) - except FileNotFoundError: - raise LedgerConfigError( - "Pool config '%s' not found", self.name - ) from None - return self.genesis_txns_cache - - async def create_pool_config( - self, genesis_transactions: str, recreate: bool = False - ): - """Create the pool ledger configuration.""" - - # indy-sdk requires a file to pass the pool configuration - # the file path includes the pool name to avoid conflicts - txn_path = path.join( - tempfile.gettempdir(), f"{self.name}_{GENESIS_TRANSACTION_FILE}" - ) - with open(txn_path, "w") as genesis_file: - genesis_file.write(genesis_transactions) - pool_config = json.dumps({"genesis_txn": txn_path}) - - if await self.check_pool_config(): - if recreate: - LOGGER.debug("Removing existing ledger config") - await indy.pool.delete_pool_ledger_config(self.name) - else: - raise LedgerConfigError( - "Ledger pool configuration already exists: %s", self.name - ) - - LOGGER.debug("Creating pool ledger config") - with IndyErrorHandler( - "Exception creating pool ledger config", LedgerConfigError - ): - await indy.pool.create_pool_ledger_config(self.name, pool_config) - - async def check_pool_config(self) -> bool: - """Check if a pool config has been created.""" - pool_names = {cfg["pool"] for cfg in await indy.pool.list_pools()} - return self.name in pool_names - - async def open(self): - """Open the pool ledger, creating it if necessary.""" - - if self.genesis_transactions: - await self.create_pool_config(self.genesis_transactions, True) - self.genesis_transactions = None - self.checked = True - elif not self.checked: - if not await self.check_pool_config(): - raise LedgerError("Ledger pool configuration has not been created") - self.checked = True - - # We only support proto ver 2 - with IndyErrorHandler( - "Exception setting ledger protocol version", LedgerConfigError - ): - await indy.pool.set_protocol_version(2) - - with IndyErrorHandler( - f"Exception opening pool ledger {self.name}", LedgerConfigError - ): - pool_config = json.dumps({}) - if self.socks_proxy is not None: - pool_config = json.dumps({"socks_proxy": self.socks_proxy}) - LOGGER.debug("Open pool with config: %s", pool_config) - self.handle = await indy.pool.open_pool_ledger(self.name, pool_config) - self.opened = True - - async def close(self): - """Close the pool ledger.""" - if self.opened: - exc = None - for _attempt in range(3): - try: - await indy.pool.close_pool_ledger(self.handle) - except IndyError as err: - await asyncio.sleep(0.01) - exc = err - continue - - self.handle = None - self.opened = False - exc = None - break - - if exc: - LOGGER.error("Exception closing pool ledger") - self.ref_count += 1 # if we are here, we should have self.ref_lock - self.close_task = None - raise IndyErrorHandler.wrap_error( - exc, "Exception closing pool ledger", LedgerError - ) - - async def context_open(self): - """Open the ledger if necessary and increase the number of active references.""" - async with self.ref_lock: - if self.close_task: - self.close_task.cancel() - if not self.opened: - LOGGER.debug("Opening the pool ledger") - await self.open() - self.ref_count += 1 - - async def context_close(self): - """Release the reference and schedule closing of the pool ledger.""" - - async def closer(timeout: int): - """Close the pool ledger after a timeout.""" - await asyncio.sleep(timeout) - async with self.ref_lock: - if not self.ref_count: - LOGGER.debug("Closing pool ledger after timeout") - await self.close() - - async with self.ref_lock: - self.ref_count -= 1 - if not self.ref_count: - if self.keepalive: - self.close_task = asyncio.ensure_future(closer(self.keepalive)) - else: - await self.close() - - -class IndySdkLedger(BaseLedger): - """Indy ledger class.""" - - BACKEND_NAME = "indy" - - def __init__( - self, - pool: IndySdkLedgerPool, - profile: "IndySdkProfile", - ): - """Initialize an IndySdkLedger instance. - - Args: - pool: The pool instance handling the raw ledger connection - profile: The IndySdkProfile instance - """ - self.pool = pool - self.profile = profile - - @property - def pool_handle(self): - """Accessor for the ledger pool handle.""" - return self.pool.handle - - @property - def pool_name(self) -> str: - """Accessor for the ledger pool name.""" - return self.pool.name - - @property - def read_only(self) -> bool: - """Accessor for the ledger read-only flag.""" - return self.pool.read_only - - async def is_ledger_read_only(self) -> bool: - """Check if ledger is read-only including TAA.""" - if self.read_only: - return self.read_only - # if TAA is required and not accepted we should be in read-only mode - taa = await self.get_txn_author_agreement() - if taa["taa_required"]: - taa_acceptance = await self.get_latest_txn_author_acceptance() - if "mechanism" not in taa_acceptance: - return True - return self.read_only - - async def __aenter__(self) -> "IndySdkLedger": - """Context manager entry. - - Returns: - The current instance - - """ - await super().__aenter__() - await self.pool.context_open() - return self - - async def __aexit__(self, exc_type, exc, tb): - """Context manager exit.""" - await self.pool.context_close() - await super().__aexit__(exc_type, exc, tb) - - async def get_wallet_public_did(self) -> DIDInfo: - """Fetch the public DID from the wallet.""" - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - return await wallet.get_public_did() - - async def _endorse( - self, - request_json: str, - endorse_did: DIDInfo = None, - ) -> str: - if not self.pool.handle: - raise ClosedPoolError( - f"Cannot endorse request with closed pool '{self.pool.name}'" - ) - - public_info = endorse_did if endorse_did else await self.get_wallet_public_did() - if not public_info: - raise BadLedgerRequestError( - "Cannot endorse transaction without a public DID" - ) - endorsed_request_json = await indy.ledger.multi_sign_request( - self.profile.wallet.handle, public_info.did, request_json - ) - return endorsed_request_json - - async def _submit( - self, - request_json: str, - sign: bool = None, - taa_accept: bool = None, - sign_did: DIDInfo = sentinel, - write_ledger: bool = True, - ) -> str: - """Sign and submit request to ledger. - - Args: - request_json: The json string to submit - sign: whether or not to sign the request - taa_accept: whether to apply TAA acceptance to the (signed, write) request - sign_did: override the signing DID - write_ledger: skip the request submission - - """ - - if not self.pool.handle: - raise ClosedPoolError( - f"Cannot sign and submit request to closed pool '{self.pool.name}'" - ) - - if sign is None or sign: - if sign_did is sentinel: - sign_did = await self.get_wallet_public_did() - if sign is None: - sign = bool(sign_did) - - if taa_accept is None and sign: - taa_accept = True - - if sign: - if not sign_did: - raise BadLedgerRequestError("Cannot sign request without a public DID") - if taa_accept: - acceptance = await self.get_latest_txn_author_acceptance() - if acceptance: - # flake8 and black 23.1.0 check collision fix - # fmt: off - request_json = await ( - indy.ledger.append_txn_author_agreement_acceptance_to_request( - request_json, - acceptance["text"], - acceptance["version"], - acceptance["digest"], - acceptance["mechanism"], - acceptance["time"], - ) - ) - # fmt: on - if write_ledger: - submit_op = indy.ledger.sign_and_submit_request( - self.pool.handle, - self.profile.wallet.handle, - sign_did.did, - request_json, - ) - else: - # multi-sign, since we expect this to get endorsed later - submit_op = indy.ledger.multi_sign_request( - self.profile.wallet.handle, sign_did.did, request_json - ) - else: - submit_op = indy.ledger.submit_request(self.pool.handle, request_json) - - with IndyErrorHandler( - "Exception raised by ledger transaction", LedgerTransactionError - ): - request_result_json = await submit_op - - if sign and not write_ledger: - return request_result_json - - request_result = json.loads(request_result_json) - - operation = request_result.get("op", "") - - if operation in ("REQNACK", "REJECT"): - raise LedgerTransactionError( - f"Ledger rejected transaction request: {request_result['reason']}" - ) - - elif operation == "REPLY": - return request_result_json - - else: - raise LedgerTransactionError( - f"Unexpected operation code from ledger: {operation}" - ) - - async def txn_endorse( - self, - request_json: str, - endorse_did: DIDInfo = None, - ) -> str: - """Endorse a (signed) ledger transaction.""" - return await self._endorse(request_json, endorse_did=endorse_did) - - async def txn_submit( - self, - request_json: str, - sign: bool = None, - taa_accept: bool = None, - sign_did: DIDInfo = sentinel, - write_ledger: bool = True, - ) -> str: - """Submit a signed (and endorsed) transaction to the ledger.""" - return await self._submit( - request_json, - sign=sign, - taa_accept=taa_accept, - sign_did=sign_did, - write_ledger=write_ledger, - ) - - async def _create_schema_request( - self, - public_info: DIDInfo, - schema_json: str, - write_ledger: bool = True, - endorser_did: str = None, - ): - """Create the ledger request for publishing a schema.""" - with IndyErrorHandler("Exception building schema request", LedgerError): - request_json = await indy.ledger.build_schema_request( - public_info.did, schema_json - ) - - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - - return request_json - - async def _create_revoc_reg_def_request( - self, - public_info: DIDInfo, - revoc_reg_def_json: str, - write_ledger: bool = True, - endorser_did: str = None, - ): - """Create the ledger request for publishing a revocation registry definition.""" - with IndyErrorHandler("Exception building revoc reg def request", LedgerError): - request_json = await indy.ledger.build_revoc_reg_def_request( - public_info.did, revoc_reg_def_json - ) - - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - - return request_json - - async def get_schema(self, schema_id: str) -> dict: - """Get a schema from the cache if available, otherwise fetch from the ledger. - - Args: - schema_id: The schema id (or stringified sequence number) to retrieve - - """ - if self.pool.cache: - result = await self.pool.cache.get(f"schema::{schema_id}") - if result: - return result - - if schema_id.isdigit(): - return await self.fetch_schema_by_seq_no(int(schema_id)) - else: - return await self.fetch_schema_by_id(schema_id) - - async def fetch_schema_by_id(self, schema_id: str) -> dict: - """Get schema from ledger. - - Args: - schema_id: The schema id (or stringified sequence number) to retrieve - - Returns: - Indy schema dict - - """ - - public_info = await self.get_wallet_public_did() - public_did = public_info.did if public_info else None - - with IndyErrorHandler("Exception building schema request", LedgerError): - request_json = await indy.ledger.build_get_schema_request( - public_did, schema_id - ) - - response_json = await self._submit(request_json, sign_did=public_info) - response = json.loads(response_json) - if not response["result"]["seqNo"]: - # schema not found - return None - - with IndyErrorHandler("Exception parsing schema response", LedgerError): - _, parsed_schema_json = await indy.ledger.parse_get_schema_response( - response_json - ) - - parsed_response = json.loads(parsed_schema_json) - if parsed_response and self.pool.cache: - await self.pool.cache.set( - [f"schema::{schema_id}", f"schema::{response['result']['seqNo']}"], - parsed_response, - self.pool.cache_duration, - ) - - return parsed_response - - async def fetch_schema_by_seq_no(self, seq_no: int) -> dict: - """Fetch a schema by its sequence number. - - Args: - seq_no: schema ledger sequence number - - Returns: - Indy schema dict - - """ - # get txn by sequence number, retrieve schema identifier components - request_json = await indy.ledger.build_get_txn_request( - None, None, seq_no=seq_no - ) - response = json.loads(await self._submit(request_json)) - - # transaction data format assumes node protocol >= 1.4 (circa 2018-07) - data_txn = (response["result"].get("data", {}) or {}).get("txn", {}) - if data_txn.get("type", None) == "101": # marks indy-sdk schema txn type - (origin_did, name, version) = ( - data_txn["metadata"]["from"], - data_txn["data"]["data"]["name"], - data_txn["data"]["data"]["version"], - ) - schema_id = f"{origin_did}:2:{name}:{version}" - return await self.get_schema(schema_id) - - raise LedgerTransactionError( - f"Could not get schema from ledger for seq no {seq_no}" - ) - - async def _create_credential_definition_request( - self, - public_info: DIDInfo, - credential_definition_json: str, - write_ledger: bool = True, - endorser_did: str = None, - ): - """Create the ledger request for publishing a credential definition.""" - with IndyErrorHandler("Exception building cred def request", LedgerError): - request_json = await indy.ledger.build_cred_def_request( - public_info.did, credential_definition_json - ) - - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - - return request_json - - async def get_credential_definition(self, credential_definition_id: str) -> dict: - """Get a credential definition from the cache if available, otherwise the ledger. - - Args: - credential_definition_id: The schema id of the schema to fetch cred def for - - """ - if self.pool.cache: - result = await self.pool.cache.get( - f"credential_definition::{credential_definition_id}" - ) - if result: - return result - - return await self.fetch_credential_definition(credential_definition_id) - - async def fetch_credential_definition(self, credential_definition_id: str) -> dict: - """Get a credential definition from the ledger by id. - - Args: - credential_definition_id: The cred def id of the cred def to fetch - - """ - - public_info = await self.get_wallet_public_did() - public_did = public_info.did if public_info else None - - with IndyErrorHandler("Exception building cred def request", LedgerError): - request_json = await indy.ledger.build_get_cred_def_request( - public_did, credential_definition_id - ) - - response_json = await self._submit(request_json, sign_did=public_info) - - with IndyErrorHandler("Exception parsing cred def response", LedgerError): - try: - ( - _, - parsed_credential_definition_json, - ) = await indy.ledger.parse_get_cred_def_response(response_json) - parsed_response = json.loads(parsed_credential_definition_json) - except IndyError as error: - if error.error_code == ErrorCode.LedgerNotFound: - parsed_response = None - else: - raise - - if parsed_response and self.pool.cache: - await self.pool.cache.set( - f"credential_definition::{credential_definition_id}", - parsed_response, - self.pool.cache_duration, - ) - - return parsed_response - - async def credential_definition_id2schema_id(self, credential_definition_id): - """From a credential definition, get the identifier for its schema. - - Args: - credential_definition_id: The identifier of the credential definition - from which to identify a schema - """ - - # scrape schema id or sequence number from cred def id - tokens = credential_definition_id.split(":") - if len(tokens) == 8: # node protocol >= 1.4: cred def id has 5 or 8 tokens - return ":".join(tokens[3:7]) # schema id spans 0-based positions 3-6 - - # get txn by sequence number, retrieve schema identifier components - seq_no = tokens[3] - return (await self.get_schema(seq_no))["id"] - - async def get_key_for_did(self, did: str) -> str: - """Fetch the verkey for a ledger DID. - - Args: - did: The DID to look up on the ledger or in the cache - """ - nym = self.did_to_nym(did) - public_info = await self.get_wallet_public_did() - public_did = public_info.did if public_info else None - with IndyErrorHandler("Exception building nym request", LedgerError): - request_json = await indy.ledger.build_get_nym_request(public_did, nym) - response_json = await self._submit(request_json, sign_did=public_info) - data_json = (json.loads(response_json))["result"]["data"] - return full_verkey(did, json.loads(data_json)["verkey"]) if data_json else None - - async def get_all_endpoints_for_did(self, did: str) -> dict: - """Fetch all endpoints for a ledger DID. - - Args: - did: The DID to look up on the ledger or in the cache - """ - nym = self.did_to_nym(did) - public_info = await self.get_wallet_public_did() - public_did = public_info.did if public_info else None - with IndyErrorHandler("Exception building attribute request", LedgerError): - request_json = await indy.ledger.build_get_attrib_request( - public_did, nym, "endpoint", None, None - ) - response_json = await self._submit(request_json, sign_did=public_info) - data_json = json.loads(response_json)["result"]["data"] - - if data_json: - endpoints = json.loads(data_json).get("endpoint", None) - else: - endpoints = None - - return endpoints - - async def get_endpoint_for_did( - self, did: str, endpoint_type: EndpointType = None - ) -> str: - """Fetch the endpoint for a ledger DID. - - Args: - did: The DID to look up on the ledger or in the cache - endpoint_type: The type of the endpoint. If none given, returns all - """ - - if not endpoint_type: - endpoint_type = EndpointType.ENDPOINT - nym = self.did_to_nym(did) - public_info = await self.get_wallet_public_did() - public_did = public_info.did if public_info else None - with IndyErrorHandler("Exception building attribute request", LedgerError): - request_json = await indy.ledger.build_get_attrib_request( - public_did, nym, "endpoint", None, None - ) - response_json = await self._submit(request_json, sign_did=public_info) - data_json = json.loads(response_json)["result"]["data"] - if data_json: - endpoint = json.loads(data_json).get("endpoint", None) - address = endpoint.get(endpoint_type.indy, None) if endpoint else None - else: - address = None - - return address - - async def update_endpoint_for_did( - self, - did: str, - endpoint: str, - endpoint_type: EndpointType = None, - write_ledger: bool = True, - endorser_did: str = None, - routing_keys: List[str] = None, - ) -> bool: - """Check and update the endpoint on the ledger. - - Args: - did: The ledger DID - endpoint: The endpoint address - endpoint_type: The type of the endpoint - """ - public_info = await self.get_wallet_public_did() - if not public_info: - raise BadLedgerRequestError( - "Cannot update endpoint at ledger without a public DID" - ) - - if not endpoint_type: - endpoint_type = EndpointType.ENDPOINT - - all_exist_endpoints = await self.get_all_endpoints_for_did(did) - exist_endpoint_of_type = ( - all_exist_endpoints.get(endpoint_type.indy, None) - if all_exist_endpoints - else None - ) - - if exist_endpoint_of_type != endpoint: - if await self.is_ledger_read_only(): - raise LedgerError( - "Error cannot update endpoint when ledger is in read only mode, " - "or TAA is required and not accepted" - ) - - nym = self.did_to_nym(did) - - attr_json = await self._construct_attr_json( - endpoint, endpoint_type, all_exist_endpoints, routing_keys - ) - - with IndyErrorHandler("Exception building attribute request", LedgerError): - request_json = await indy.ledger.build_attrib_request( - nym, nym, None, attr_json, None - ) - - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - resp = await self._submit( - request_json, - sign=True, - sign_did=public_info, - write_ledger=write_ledger, - ) - if not write_ledger: - return {"signed_txn": resp} - - await self._submit(request_json, True, True) - return True - - return False - - async def register_nym( - self, - did: str, - verkey: str, - alias: str = None, - role: str = None, - write_ledger: bool = True, - endorser_did: str = None, - ) -> Tuple[bool, dict]: - """Register a nym on the ledger. - - Args: - did: DID to register on the ledger. - verkey: The verification key of the keypair. - alias: Human-friendly alias to assign to the DID. - role: For permissioned ledgers, what role should the new DID have. - """ - if await self.is_ledger_read_only(): - raise LedgerError( - "Error cannot register nym when ledger is in read only mode, " - "or TAA is required and not accepted" - ) - - public_info = await self.get_wallet_public_did() - if not public_info: - raise WalletNotFoundError( - f"Cannot register NYM to ledger: wallet {self.profile.name} " - "has no public DID" - ) - with IndyErrorHandler("Exception building nym request", LedgerError): - request_json = await indy.ledger.build_nym_request( - public_info.did, did, verkey, alias, role - ) - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - resp = await self._submit( - request_json, sign=True, sign_did=public_info, write_ledger=write_ledger - ) # let ledger raise on insufficient privilege - if not write_ledger: - return True, {"signed_txn": resp} - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - try: - did_info = await wallet.get_local_did(did) - except WalletNotFoundError: - pass # registering another user's NYM - else: - metadata = {**did_info.metadata, **DIDPosture.POSTED.metadata} - await wallet.replace_local_did_metadata(did, metadata) - return True, None - - async def get_nym_role(self, did: str) -> Role: - """Return the role of the input public DID's NYM on the ledger. - - Args: - did: DID to query for role on the ledger. - """ - public_info = await self.get_wallet_public_did() - public_did = public_info.did if public_info else None - - with IndyErrorHandler("Exception building get-nym request", LedgerError): - request_json = await indy.ledger.build_get_nym_request(public_did, did) - - response_json = await self._submit(request_json) - response = json.loads(response_json) - nym_data = json.loads(response["result"]["data"]) - if not nym_data: - raise BadLedgerRequestError(f"DID {did} is not public") - - return Role.get(nym_data["role"]) - - def nym_to_did(self, nym: str) -> str: - """Format a nym with the ledger's DID prefix.""" - if nym: - # remove any existing prefix - nym = self.did_to_nym(nym) - return f"did:sov:{nym}" - - async def build_and_return_get_nym_request( - self, submitter_did: Optional[str], target_did: str - ) -> str: - """Build GET_NYM request and return request_json.""" - with IndyErrorHandler("Exception building nym request", LedgerError): - request_json = await indy.ledger.build_get_nym_request( - submitter_did, target_did - ) - return request_json - - async def submit_get_nym_request(self, request_json: str) -> str: - """Submit GET_NYM request to ledger and return response_json.""" - response_json = await self._submit(request_json) - return response_json - - async def rotate_public_did_keypair(self, next_seed: str = None) -> None: - """Rotate keypair for public DID: create new key, submit to ledger, update wallet. - - Args: - next_seed: seed for incoming ed25519 keypair (default random) - """ - # generate new key - public_info = await self.get_wallet_public_did() - public_did = public_info.did - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - verkey = await wallet.rotate_did_keypair_start(public_did, next_seed) - - # submit to ledger (retain role and alias) - nym = self.did_to_nym(public_did) - with IndyErrorHandler("Exception building nym request", LedgerError): - request_json = await indy.ledger.build_get_nym_request(public_did, nym) - - response_json = await self._submit(request_json) - data = json.loads((json.loads(response_json))["result"]["data"]) - if not data: - raise BadLedgerRequestError( - f"Ledger has no public DID for wallet {self.profile.name}" - ) - seq_no = data["seqNo"] - - with IndyErrorHandler("Exception building get-txn request", LedgerError): - txn_req_json = await indy.ledger.build_get_txn_request(None, None, seq_no) - - txn_resp_json = await self._submit(txn_req_json) - txn_resp = json.loads(txn_resp_json) - txn_resp_data = txn_resp["result"]["data"] - if not txn_resp_data: - raise BadLedgerRequestError( - f"Bad or missing ledger NYM transaction for DID {public_did}" - ) - txn_data_data = txn_resp_data["txn"]["data"] - role_token = Role.get(txn_data_data.get("role")).token() - alias = txn_data_data.get("alias") - await self.register_nym(public_did, verkey, role_token, alias) - - # update wallet - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - await wallet.rotate_did_keypair_apply(public_did) - - async def get_txn_author_agreement(self, reload: bool = False) -> dict: - """Get the current transaction author agreement, fetching it if necessary.""" - if not self.pool.taa_cache or reload: - self.pool.taa_cache = await self.fetch_txn_author_agreement() - return self.pool.taa_cache - - async def fetch_txn_author_agreement(self) -> dict: - """Fetch the current AML and TAA from the ledger.""" - public_info = await self.get_wallet_public_did() - public_did = public_info.did if public_info else None - - get_aml_req = await indy.ledger.build_get_acceptance_mechanisms_request( - public_did, None, None - ) - response_json = await self._submit(get_aml_req, sign_did=public_info) - aml_found = (json.loads(response_json))["result"]["data"] - - get_taa_req = await indy.ledger.build_get_txn_author_agreement_request( - public_did, None - ) - response_json = await self._submit(get_taa_req, sign_did=public_info) - taa_found = (json.loads(response_json))["result"]["data"] - taa_required = bool(taa_found and taa_found["text"]) - if taa_found: - taa_found["digest"] = self.taa_digest( - taa_found["version"], taa_found["text"] - ) - - return { - "aml_record": aml_found, - "taa_record": taa_found, - "taa_required": taa_required, - } - - async def get_indy_storage(self) -> IndySdkStorage: - """Get an IndySdkStorage instance for the current wallet.""" - return IndySdkStorage(self.profile.wallet) - - def taa_rough_timestamp(self) -> int: - """Get a timestamp accurate to the day. - - Anything more accurate is a privacy concern. - """ - return int( - datetime.combine( - date.today(), datetime.min.time(), datetime.timezone.utc - ).timestamp() - ) - - async def accept_txn_author_agreement( - self, taa_record: dict, mechanism: str, accept_time: int = None - ): - """Save a new record recording the acceptance of the TAA.""" - if not accept_time: - accept_time = self.taa_rough_timestamp() - acceptance = { - "text": taa_record["text"], - "version": taa_record["version"], - "digest": taa_record["digest"], - "mechanism": mechanism, - "time": accept_time, - } - record = StorageRecord( - TAA_ACCEPTED_RECORD_TYPE, - json.dumps(acceptance), - {"pool_name": self.pool.name}, - ) - storage = await self.get_indy_storage() - await storage.add_record(record) - if self.pool.cache: - cache_key = ( - TAA_ACCEPTED_RECORD_TYPE - + "::" - + self.profile.name - + "::" - + self.pool.name - + "::" - ) - await self.pool.cache.set(cache_key, acceptance, self.pool.cache_duration) - - async def get_latest_txn_author_acceptance(self) -> dict: - """Look up the latest TAA acceptance.""" - cache_key = ( - TAA_ACCEPTED_RECORD_TYPE - + "::" - + self.profile.name - + "::" - + self.pool.name - + "::" - ) - acceptance = self.pool.cache and await self.pool.cache.get(cache_key) - if not acceptance: - storage = await self.get_indy_storage() - tag_filter = {"pool_name": self.pool.name} - found = await storage.find_all_records(TAA_ACCEPTED_RECORD_TYPE, tag_filter) - if found: - records = [json.loads(record.value) for record in found] - records.sort(key=lambda v: v["time"], reverse=True) - acceptance = records[0] - else: - acceptance = {} - if self.pool.cache: - await self.pool.cache.set( - cache_key, acceptance, self.pool.cache_duration - ) - return acceptance - - async def get_revoc_reg_def(self, revoc_reg_id: str) -> dict: - """Get revocation registry definition by ID; augment with ledger timestamp.""" - public_info = await self.get_wallet_public_did() - try: - fetch_req = await indy.ledger.build_get_revoc_reg_def_request( - public_info and public_info.did, revoc_reg_id - ) - response_json = await self._submit(fetch_req, sign_did=public_info) - ( - found_id, - found_def_json, - ) = await indy.ledger.parse_get_revoc_reg_def_response(response_json) - found_def = json.loads(found_def_json) - found_def["txnTime"] = json.loads(response_json)["result"]["txnTime"] - - except IndyError as e: - LOGGER.error( - f"get_revoc_reg_def failed with revoc_reg_id={revoc_reg_id} - " - f"{e.error_code}: {getattr(e, 'message', '[no message]')}" - ) - raise e - - assert found_id == revoc_reg_id - return found_def - - async def get_revoc_reg_entry(self, revoc_reg_id: str, timestamp: int): - """Get revocation registry entry by revocation registry ID and timestamp.""" - public_info = await self.get_wallet_public_did() - with IndyErrorHandler("Exception fetching rev reg entry", LedgerError): - try: - fetch_req = await indy.ledger.build_get_revoc_reg_request( - public_info and public_info.did, revoc_reg_id, timestamp - ) - response_json = await self._submit(fetch_req, sign_did=public_info) - ( - found_id, - found_reg_json, - ledger_timestamp, - ) = await indy.ledger.parse_get_revoc_reg_response(response_json) - except IndyError as e: - LOGGER.error( - f"get_revoc_reg_entry failed with revoc_reg_id={revoc_reg_id} - " - f"{e.error_code}: {getattr(e, 'message', '[no message]')}" - ) - raise e - assert found_id == revoc_reg_id - return json.loads(found_reg_json), ledger_timestamp - - async def get_revoc_reg_delta( - self, revoc_reg_id: str, fro=0, to=None - ) -> Tuple[dict, int]: - """Look up a revocation registry delta by ID. - - :param revoc_reg_id revocation registry id - :param fro earliest EPOCH time of interest - :param to latest EPOCH time of interest - - :returns delta response, delta timestamp - """ - if to is None: - to = int(time()) - public_info = await self.get_wallet_public_did() - with IndyErrorHandler("Exception building rev reg delta request", LedgerError): - fetch_req = await indy.ledger.build_get_revoc_reg_delta_request( - public_info and public_info.did, - revoc_reg_id, - 0 if fro == to else fro, - to, - ) - response_json = await self._submit(fetch_req, sign_did=public_info) - with IndyErrorHandler( - ( - "Exception parsing rev reg delta response " - "(interval ends before rev reg creation?)" - ), - LedgerError, - ): - ( - found_id, - found_delta_json, - delta_timestamp, - ) = await indy.ledger.parse_get_revoc_reg_delta_response(response_json) - assert found_id == revoc_reg_id - return json.loads(found_delta_json), delta_timestamp - - async def send_revoc_reg_def( - self, - revoc_reg_def: dict, - issuer_did: str = None, - write_ledger: bool = True, - endorser_did: str = None, - ) -> dict: - """Publish a revocation registry definition to the ledger.""" - # NOTE - issuer DID could be extracted from the revoc_reg_def ID - if issuer_did: - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - did_info = await wallet.get_local_did(issuer_did) - else: - did_info = await self.get_wallet_public_did() - if not did_info: - raise LedgerTransactionError( - "No issuer DID found for revocation registry definition" - ) - - if self.profile.context.settings.get("wallet.type") == "askar-anoncreds": - from aries_cloudagent.anoncreds.default.legacy_indy.registry import ( - LegacyIndyRegistry, - ) - - rev_reg_def_req = await self._create_revoc_reg_def_request( - did_info, - json.dumps(revoc_reg_def), - write_ledger=write_ledger, - endorser_did=endorser_did, - ) - legacy_indy_registry = LegacyIndyRegistry() - - resp = await legacy_indy_registry.txn_submit( - self.profile, - rev_reg_def_req, - sign=True, - sign_did=did_info, - write_ledger=write_ledger, - ) - - if not write_ledger: - return revoc_reg_def["id"], {"signed_txn": resp} - - try: - # parse sequence number out of response - seq_no = json.loads(resp)["result"]["txnMetadata"]["seqNo"] - return seq_no - except KeyError as err: - raise LedgerError( - "Failed to parse sequence number from ledger response" - ) from err - else: - with IndyErrorHandler("Exception building rev reg def", LedgerError): - request_json = await indy.ledger.build_revoc_reg_def_request( - did_info.did, json.dumps(revoc_reg_def) - ) - - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - resp = await self._submit( - request_json, True, sign_did=did_info, write_ledger=write_ledger - ) - - return {"result": resp} - - async def send_revoc_reg_entry( - self, - revoc_reg_id: str, - revoc_def_type: str, - revoc_reg_entry: dict, - issuer_did: str = None, - write_ledger: bool = True, - endorser_did: str = None, - ) -> dict: - """Publish a revocation registry entry to the ledger.""" - if issuer_did: - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - did_info = await wallet.get_local_did(issuer_did) - else: - did_info = await self.get_wallet_public_did() - if not did_info: - raise LedgerTransactionError( - "No issuer DID found for revocation registry entry" - ) - - if self.profile.context.settings.get("wallet.type") == "askar-anoncreds": - from aries_cloudagent.anoncreds.default.legacy_indy.registry import ( - LegacyIndyRegistry, - ) - - rev_reg_def_entry_req = await self._create_revoc_reg_def_request( - did_info, - json.dumps(revoc_reg_entry), - write_ledger=write_ledger, - endorser_did=endorser_did, - ) - legacy_indy_registry = LegacyIndyRegistry() - - resp = await legacy_indy_registry.txn_submit( - self.profile, - rev_reg_def_entry_req, - sign=True, - sign_did=did_info, - write_ledger=write_ledger, - ) - - if not write_ledger: - return rev_reg_def_entry_req["id"], {"signed_txn": resp} - - try: - # parse sequence number out of response - seq_no = json.loads(resp)["result"]["txnMetadata"]["seqNo"] - return seq_no - except KeyError as err: - raise LedgerError( - "Failed to parse sequence number from ledger response" - ) from err - else: - with IndyErrorHandler("Exception building rev reg entry", LedgerError): - request_json = await indy.ledger.build_revoc_reg_entry_request( - did_info.did, - revoc_reg_id, - revoc_def_type, - json.dumps(revoc_reg_entry), - ) - - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - - resp = await self._submit( - request_json, True, sign_did=did_info, write_ledger=write_ledger - ) - return {"result": resp} diff --git a/aries_cloudagent/ledger/indy_vdr.py b/aries_cloudagent/ledger/indy_vdr.py index 7444311343..1c04677e2d 100644 --- a/aries_cloudagent/ledger/indy_vdr.py +++ b/aries_cloudagent/ledger/indy_vdr.py @@ -1147,7 +1147,7 @@ async def send_revoc_reg_def( legacy_indy_registry = LegacyIndyRegistry() resp = await legacy_indy_registry.txn_submit( - self.profile, + self, rev_reg_def_req, sign=True, sign_did=did_info, @@ -1222,7 +1222,7 @@ async def send_revoc_reg_entry( legacy_indy_registry = LegacyIndyRegistry() resp = await legacy_indy_registry.txn_submit( - self.profile, + self, revoc_reg_entry_req, sign=True, sign_did=did_info, diff --git a/aries_cloudagent/ledger/multiple_ledger/indy_manager.py b/aries_cloudagent/ledger/multiple_ledger/indy_manager.py deleted file mode 100644 index 49c52355ab..0000000000 --- a/aries_cloudagent/ledger/multiple_ledger/indy_manager.py +++ /dev/null @@ -1,269 +0,0 @@ -"""Multiple IndySdkLedger Manager.""" - -import asyncio -import concurrent.futures -import json -import logging -from collections import OrderedDict -from typing import List, Mapping, Optional, Tuple - -from ...cache.base import BaseCache -from ...core.profile import Profile -from ...ledger.base import BaseLedger -from ...ledger.error import LedgerError -from ...wallet.crypto import did_is_self_certified -from ..indy import IndySdkLedger -from ..merkel_validation.domain_txn_handler import ( - get_proof_nodes, - prepare_for_state_read, -) -from ..merkel_validation.trie import SubTrie -from .base_manager import BaseMultipleLedgerManager, MultipleLedgerManagerError - -LOGGER = logging.getLogger(__name__) - - -class MultiIndyLedgerManager(BaseMultipleLedgerManager): - """Multiple Indy SDK Ledger Manager.""" - - def __init__( - self, - profile: Profile, - production_ledgers: Optional[OrderedDict] = None, - non_production_ledgers: Optional[OrderedDict] = None, - writable_ledgers: Optional[set] = None, - endorser_map: Optional[dict] = None, - cache_ttl: int = None, - ): - """Initialize MultiIndyLedgerManager. - - Args: - profile: The base profile for this manager - production_ledgers: production IndySdkLedger mapping - non_production_ledgers: non_production IndySdkLedger mapping - cache_ttl: Time in sec to persist did_ledger_id_resolver cache keys - - """ - self.profile = profile - self.production_ledgers = production_ledgers or OrderedDict() - self.non_production_ledgers = non_production_ledgers or OrderedDict() - self.writable_ledgers = writable_ledgers or set() - self.endorser_map = endorser_map or {} - self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) - self.cache_ttl = cache_ttl - - async def get_write_ledgers(self) -> List[str]: - """Return the write IndySdkLedger instance.""" - return list(self.writable_ledgers) - - def get_endorser_info_for_ledger(self, ledger_id: str) -> Optional[Tuple[str, str]]: - """Return endorser alias, did tuple for provided ledger, if available.""" - endorser_info = self.endorser_map.get(ledger_id) - if not endorser_info: - return None - return (endorser_info["endorser_alias"], endorser_info["endorser_did"]) - - async def get_ledger_inst_by_id(self, ledger_id: str) -> Optional[BaseLedger]: - """Return BaseLedger instance.""" - return self.production_ledgers.get( - ledger_id - ) or self.non_production_ledgers.get(ledger_id) - - async def get_prod_ledgers(self) -> Mapping: - """Return production ledgers mapping.""" - return self.production_ledgers - - async def get_nonprod_ledgers(self) -> Mapping: - """Return non_production ledgers mapping.""" - return self.non_production_ledgers - - async def get_ledger_id_by_ledger_pool_name(self, pool_name: str) -> str: - """Return ledger_id by ledger pool name.""" - multi_ledgers = self.production_ledgers | self.non_production_ledgers - for ledger_id, indy_vdr_ledger in multi_ledgers.items(): - if indy_vdr_ledger.pool_name == pool_name: - return ledger_id - raise MultipleLedgerManagerError( - f"Provided Ledger pool name {pool_name} not found " - "in either production_ledgers or non_production_ledgers" - ) - - async def _get_ledger_by_did( - self, - ledger_id: str, - did: str, - ) -> Optional[Tuple[str, IndySdkLedger, bool]]: - """Build and submit GET_NYM request and process response. - - Successful response return tuple with ledger_id, IndySdkLedger instance - and is_self_certified bool flag. Unsuccessful response return None. - - Args: - ledger_id: provided ledger_id to retrieve IndySdkLedger instance - from production_ledgers or non_production_ledgers - did: provided DID - - Return: - (str, IndySdkLedger, bool) or None - """ - try: - indy_sdk_ledger = None - if ledger_id in self.production_ledgers: - indy_sdk_ledger = self.production_ledgers.get(ledger_id) - else: - indy_sdk_ledger = self.non_production_ledgers.get(ledger_id) - async with indy_sdk_ledger: - request = await indy_sdk_ledger.build_and_return_get_nym_request( - None, did - ) - response_json = await asyncio.wait_for( - indy_sdk_ledger.submit_get_nym_request(request), 10 - ) - response = json.loads(response_json) - data = response.get("result", {}).get("data") - if not data: - LOGGER.warning(f"Did {did} not posted to ledger {ledger_id}") - return None - if isinstance(data, str): - data = json.loads(data) - if not await SubTrie.verify_spv_proof( - expected_value=prepare_for_state_read(response), - proof_nodes=get_proof_nodes(response), - ): - LOGGER.warning( - f"State Proof validation failed for Did {did} " - f"and ledger {ledger_id}" - ) - return None - if did_is_self_certified(did, data.get("verkey")): - return (ledger_id, indy_sdk_ledger, True) - return (ledger_id, indy_sdk_ledger, False) - except asyncio.TimeoutError: - LOGGER.exception( - f"get-nym request timedout for Did {did} and " - f"ledger {ledger_id}, reply not received within 10 sec" - ) - return None - except LedgerError as err: - LOGGER.error( - "Exception when building and submitting get-nym request, " - f"for Did {did} and ledger {ledger_id}, {err}" - ) - return None - - async def lookup_did_in_configured_ledgers( - self, did: str, cache_did: bool = True - ) -> Tuple[str, IndySdkLedger]: - """Lookup given DID in configured ledgers in parallel.""" - self.cache = self.profile.inject_or(BaseCache) - cache_key = f"did_ledger_id_resolver::{did}" - if bool(cache_did and self.cache and await self.cache.get(cache_key)): - cached_ledger_id = await self.cache.get(cache_key) - if cached_ledger_id in self.production_ledgers: - return (cached_ledger_id, self.production_ledgers.get(cached_ledger_id)) - elif cached_ledger_id in self.non_production_ledgers: - return ( - cached_ledger_id, - self.non_production_ledgers.get(cached_ledger_id), - ) - else: - raise MultipleLedgerManagerError( - f"cached ledger_id {cached_ledger_id} not found in either " - "production_ledgers or non_production_ledgers" - ) - applicable_prod_ledgers = {"self_certified": {}, "non_self_certified": {}} - applicable_non_prod_ledgers = {"self_certified": {}, "non_self_certified": {}} - ledger_ids = list(self.production_ledgers.keys()) + list( - self.non_production_ledgers.keys() - ) - coro_futures = { - self.executor.submit(self._get_ledger_by_did, ledger_id, did): ledger_id - for ledger_id in ledger_ids - } - for coro_future in concurrent.futures.as_completed(coro_futures): - result = await coro_future.result() - if result: - applicable_ledger_id = result[0] - applicable_ledger_inst = result[1] - is_self_certified = result[2] - if applicable_ledger_id in self.production_ledgers: - insert_key = list(self.production_ledgers).index( - applicable_ledger_id - ) - if is_self_certified: - applicable_prod_ledgers["self_certified"][insert_key] = ( - applicable_ledger_id, - applicable_ledger_inst, - ) - else: - applicable_prod_ledgers["non_self_certified"][insert_key] = ( - applicable_ledger_id, - applicable_ledger_inst, - ) - else: - insert_key = list(self.non_production_ledgers).index( - applicable_ledger_id - ) - if is_self_certified: - applicable_non_prod_ledgers["self_certified"][insert_key] = ( - applicable_ledger_id, - applicable_ledger_inst, - ) - else: - applicable_non_prod_ledgers["non_self_certified"][ - insert_key - ] = (applicable_ledger_id, applicable_ledger_inst) - applicable_prod_ledgers["self_certified"] = OrderedDict( - sorted(applicable_prod_ledgers.get("self_certified").items()) - ) - applicable_prod_ledgers["non_self_certified"] = OrderedDict( - sorted(applicable_prod_ledgers.get("non_self_certified").items()) - ) - applicable_non_prod_ledgers["self_certified"] = OrderedDict( - sorted(applicable_non_prod_ledgers.get("self_certified").items()) - ) - applicable_non_prod_ledgers["non_self_certified"] = OrderedDict( - sorted(applicable_non_prod_ledgers.get("non_self_certified").items()) - ) - if len(applicable_prod_ledgers.get("self_certified")) > 0: - successful_ledger_inst = list( - applicable_prod_ledgers.get("self_certified").values() - )[0] - if cache_did and self.cache: - await self.cache.set( - cache_key, successful_ledger_inst[0], self.cache_ttl - ) - return successful_ledger_inst - elif len(applicable_non_prod_ledgers.get("self_certified")) > 0: - successful_ledger_inst = list( - applicable_non_prod_ledgers.get("self_certified").values() - )[0] - if cache_did and self.cache: - await self.cache.set( - cache_key, successful_ledger_inst[0], self.cache_ttl - ) - return successful_ledger_inst - elif len(applicable_prod_ledgers.get("non_self_certified")) > 0: - successful_ledger_inst = list( - applicable_prod_ledgers.get("non_self_certified").values() - )[0] - if cache_did and self.cache: - await self.cache.set( - cache_key, successful_ledger_inst[0], self.cache_ttl - ) - return successful_ledger_inst - elif len(applicable_non_prod_ledgers.get("non_self_certified")) > 0: - successful_ledger_inst = list( - applicable_non_prod_ledgers.get("non_self_certified").values() - )[0] - if cache_did and self.cache: - await self.cache.set( - cache_key, successful_ledger_inst[0], self.cache_ttl - ) - return successful_ledger_inst - else: - raise MultipleLedgerManagerError( - f"DID {did} not found in any of the ledgers total: " - f"(production: {len(self.production_ledgers)}, " - f"non_production: {len(self.non_production_ledgers)})" - ) diff --git a/aries_cloudagent/ledger/multiple_ledger/manager_provider.py b/aries_cloudagent/ledger/multiple_ledger/manager_provider.py index 3a6799034f..13a88a79fd 100644 --- a/aries_cloudagent/ledger/multiple_ledger/manager_provider.py +++ b/aries_cloudagent/ledger/multiple_ledger/manager_provider.py @@ -20,12 +20,6 @@ class MultiIndyLedgerManagerProvider(BaseProvider): """Multiple Indy ledger support manager provider.""" MANAGER_TYPES = { - "basic": ( - DeferLoad( - "aries_cloudagent.ledger.multiple_ledger." - "indy_manager.MultiIndyLedgerManager" - ) - ), "askar-profile": ( DeferLoad( "aries_cloudagent.ledger.multiple_ledger." @@ -34,10 +28,6 @@ class MultiIndyLedgerManagerProvider(BaseProvider): ), } LEDGER_TYPES = { - "basic": { - "pool": DeferLoad("aries_cloudagent.ledger.indy.IndySdkLedgerPool"), - "ledger": DeferLoad("aries_cloudagent.ledger.indy.IndySdkLedger"), - }, "askar-profile": { "pool": DeferLoad("aries_cloudagent.ledger.indy_vdr.IndyVdrLedgerPool"), "ledger": DeferLoad("aries_cloudagent.ledger.indy_vdr.IndyVdrLedger"), @@ -52,14 +42,11 @@ def __init__(self, root_profile): def provide(self, settings: BaseSettings, injector: BaseInjector): """Create the multiple Indy ledger manager instance.""" - if self.root_profile.BACKEND_NAME == "indy": - manager_type = "basic" - elif self.root_profile.BACKEND_NAME == "askar": + if self.root_profile.BACKEND_NAME == "askar": manager_type = "askar-profile" else: raise MultipleLedgerManagerError( - "MultiIndyLedgerManagerProvider expects an IndySdkProfile [indy] " - " or AskarProfile [indy_vdr] as root_profile" + f"Unexpected wallet backend: {self.root_profile.BACKEND_NAME}" ) if manager_type not in self._inst: @@ -68,102 +55,53 @@ def provide(self, settings: BaseSettings, injector: BaseInjector): ledger_class = self.LEDGER_TYPES[manager_type]["ledger"] LOGGER.info("Create multiple Indy ledger manager: %s", manager_type) try: - if manager_type == "basic": - indy_sdk_production_ledgers = OrderedDict() - indy_sdk_non_production_ledgers = OrderedDict() - ledger_config_list = settings.get_value("ledger.ledger_config_list") - ledger_endorser_map = {} - write_ledgers = set() - for config in ledger_config_list: - keepalive = config.get("keepalive") - read_only = config.get("read_only") - socks_proxy = config.get("socks_proxy") - genesis_transactions = config.get("genesis_transactions") - cache = injector.inject_or(BaseCache) - ledger_id = config.get("id") - pool_name = config.get("pool_name") - ledger_is_production = config.get("is_production") - ledger_is_write = config.get("is_write") - ledger_endorser_alias = config.get("endorser_alias") - ledger_endorser_did = config.get("endorser_did") - ledger_pool = pool_class( - pool_name, - keepalive=keepalive, - cache=cache, - genesis_transactions=genesis_transactions, - read_only=read_only, - socks_proxy=socks_proxy, - ) - ledger_instance = ledger_class( - pool=ledger_pool, - profile=self.root_profile, - ) - if ledger_is_write: - write_ledgers.add(ledger_id) - if ledger_is_production: - indy_sdk_production_ledgers[ledger_id] = ledger_instance - else: - indy_sdk_non_production_ledgers[ledger_id] = ledger_instance - if ledger_endorser_alias and ledger_endorser_did: - ledger_endorser_map[ledger_id] = { - "endorser_alias": ledger_endorser_alias, - "endorser_did": ledger_endorser_did, - } - self._inst[manager_type] = manager_class( - self.root_profile, - production_ledgers=indy_sdk_production_ledgers, - non_production_ledgers=indy_sdk_non_production_ledgers, - writable_ledgers=write_ledgers, - endorser_map=ledger_endorser_map, + indy_vdr_production_ledgers = OrderedDict() + indy_vdr_non_production_ledgers = OrderedDict() + ledger_config_list = settings.get_value("ledger.ledger_config_list") + ledger_endorser_map = {} + write_ledgers = set() + for config in ledger_config_list: + keepalive = config.get("keepalive") + read_only = config.get("read_only") + socks_proxy = config.get("socks_proxy") + genesis_transactions = config.get("genesis_transactions") + cache = injector.inject_or(BaseCache) + ledger_id = config.get("id") + pool_name = config.get("pool_name") + ledger_is_production = config.get("is_production") + ledger_is_write = config.get("is_write") + ledger_endorser_alias = config.get("endorser_alias") + ledger_endorser_did = config.get("endorser_did") + ledger_pool = pool_class( + pool_name, + keepalive=keepalive, + cache=cache, + genesis_transactions=genesis_transactions, + read_only=read_only, + socks_proxy=socks_proxy, ) - else: - indy_vdr_production_ledgers = OrderedDict() - indy_vdr_non_production_ledgers = OrderedDict() - ledger_config_list = settings.get_value("ledger.ledger_config_list") - ledger_endorser_map = {} - write_ledgers = set() - for config in ledger_config_list: - keepalive = config.get("keepalive") - read_only = config.get("read_only") - socks_proxy = config.get("socks_proxy") - genesis_transactions = config.get("genesis_transactions") - cache = injector.inject_or(BaseCache) - ledger_id = config.get("id") - pool_name = config.get("pool_name") - ledger_is_production = config.get("is_production") - ledger_is_write = config.get("is_write") - ledger_endorser_alias = config.get("endorser_alias") - ledger_endorser_did = config.get("endorser_did") - ledger_pool = pool_class( - pool_name, - keepalive=keepalive, - cache=cache, - genesis_transactions=genesis_transactions, - read_only=read_only, - socks_proxy=socks_proxy, - ) - ledger_instance = ledger_class( - pool=ledger_pool, - profile=self.root_profile, - ) - if ledger_is_write: - write_ledgers.add(ledger_id) - if ledger_is_production: - indy_vdr_production_ledgers[ledger_id] = ledger_instance - else: - indy_vdr_non_production_ledgers[ledger_id] = ledger_instance - if ledger_endorser_alias and ledger_endorser_did: - ledger_endorser_map[ledger_id] = { - "endorser_alias": ledger_endorser_alias, - "endorser_did": ledger_endorser_did, - } - self._inst[manager_type] = manager_class( - self.root_profile, - production_ledgers=indy_vdr_production_ledgers, - non_production_ledgers=indy_vdr_non_production_ledgers, - writable_ledgers=write_ledgers, - endorser_map=ledger_endorser_map, + ledger_instance = ledger_class( + pool=ledger_pool, + profile=self.root_profile, ) + if ledger_is_write: + write_ledgers.add(ledger_id) + if ledger_is_production: + indy_vdr_production_ledgers[ledger_id] = ledger_instance + else: + indy_vdr_non_production_ledgers[ledger_id] = ledger_instance + if ledger_endorser_alias and ledger_endorser_did: + ledger_endorser_map[ledger_id] = { + "endorser_alias": ledger_endorser_alias, + "endorser_did": ledger_endorser_did, + } + self._inst[manager_type] = manager_class( + self.root_profile, + production_ledgers=indy_vdr_production_ledgers, + non_production_ledgers=indy_vdr_non_production_ledgers, + writable_ledgers=write_ledgers, + endorser_map=ledger_endorser_map, + ) except ClassNotFoundError as err: raise InjectionError( f"Unknown multiple Indy ledger manager type: {manager_type}" diff --git a/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_ledger_requests.py b/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_ledger_requests.py index cb7ebf0529..6b1295386b 100644 --- a/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_ledger_requests.py +++ b/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_ledger_requests.py @@ -8,7 +8,7 @@ BaseMultipleLedgerManager, MultipleLedgerManagerError, ) -from ...indy import IndySdkLedger, IndySdkLedgerPool +from ...indy_vdr import IndyVdrLedger, IndyVdrLedgerPool from ..ledger_requests_executor import IndyLedgerRequestsExecutor @@ -26,9 +26,7 @@ async def asyncSetUp(self): "genesis_transactions": "genesis_transactions", } ] - self.ledger = IndySdkLedger( - IndySdkLedgerPool("test_prod_1", checked=True), self.profile - ) + self.ledger = IndyVdrLedger(IndyVdrLedgerPool("test_prod_1"), self.profile) self.profile.context.injector.bind_instance( BaseMultipleLedgerManager, mock.MagicMock( diff --git a/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_manager.py b/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_manager.py deleted file mode 100644 index f280c8b06c..0000000000 --- a/aries_cloudagent/ledger/multiple_ledger/tests/test_indy_manager.py +++ /dev/null @@ -1,528 +0,0 @@ -import asyncio -from copy import deepcopy -import pytest -import json - -from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock - -from collections import OrderedDict - -from ....cache.base import BaseCache -from ....cache.in_memory import InMemoryCache -from ....core.in_memory import InMemoryProfile -from ....ledger.base import BaseLedger -from ....messaging.responder import BaseResponder - -from ...error import LedgerError -from ...indy import IndySdkLedger, IndySdkLedgerPool -from ...merkel_validation.tests.test_data import GET_NYM_REPLY - -from .. import indy_manager as test_module -from ..base_manager import MultipleLedgerManagerError -from ..indy_manager import MultiIndyLedgerManager - - -@pytest.mark.indy -class TestMultiIndyLedgerManager(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.profile = InMemoryProfile.test_profile(bind={BaseCache: InMemoryCache()}) - self.context = self.profile.context - setattr(self.context, "profile", self.profile) - self.responder = mock.CoroutineMock(send=mock.CoroutineMock()) - self.context.injector.bind_instance(BaseResponder, self.responder) - self.production_ledger = OrderedDict() - self.non_production_ledger = OrderedDict() - test_prod_ledger = IndySdkLedger( - IndySdkLedgerPool("test_prod_1", checked=True), self.profile - ) - writable_ledgers = set() - self.context.injector.bind_instance(BaseLedger, test_prod_ledger) - self.production_ledger["test_prod_1"] = test_prod_ledger - self.production_ledger["test_prod_2"] = IndySdkLedger( - IndySdkLedgerPool("test_prod_2", checked=True), self.profile - ) - self.non_production_ledger["test_non_prod_1"] = IndySdkLedger( - IndySdkLedgerPool("test_non_prod_1", checked=True), self.profile - ) - self.non_production_ledger["test_non_prod_2"] = IndySdkLedger( - IndySdkLedgerPool("test_non_prod_2", checked=True), self.profile - ) - writable_ledgers.add("test_prod_1") - writable_ledgers.add("test_prod_2") - self.manager = MultiIndyLedgerManager( - self.profile, - production_ledgers=self.production_ledger, - non_production_ledgers=self.non_production_ledger, - writable_ledgers=writable_ledgers, - ) - - def test_get_endorser_info_for_ledger(self): - writable_ledgers = set() - writable_ledgers.add("test_prod_1") - writable_ledgers.add("test_prod_2") - - endorser_info_map = {} - endorser_info_map["test_prod_1"] = { - "endorser_did": "test_public_did_1", - "endorser_alias": "endorser_1", - } - endorser_info_map["test_prod_2"] = { - "endorser_did": "test_public_did_2", - "endorser_alias": "endorser_2", - } - manager = MultiIndyLedgerManager( - self.profile, - production_ledgers=self.production_ledger, - non_production_ledgers=self.non_production_ledger, - writable_ledgers=writable_ledgers, - endorser_map=endorser_info_map, - ) - assert ( - "endorser_1" - ), "test_public_did_1" == manager.get_endorser_info_for_ledger("test_prod_1") - assert ( - "endorser_2" - ), "test_public_did_2" == manager.get_endorser_info_for_ledger("test_prod_2") - - async def test_get_write_ledgers(self): - ledger_ids = await self.manager.get_write_ledgers() - assert "test_prod_1" in ledger_ids - assert "test_prod_2" in ledger_ids - - async def test_get_write_ledger_from_base_ledger(self): - ledger_id = await self.manager.get_ledger_id_by_ledger_pool_name("test_prod_1") - assert ledger_id == "test_prod_1" - - async def test_set_profile_write_ledger(self): - writable_ledgers = set() - writable_ledgers.add("test_prod_1") - writable_ledgers.add("test_prod_2") - endorser_info_map = {} - endorser_info_map["test_prod_2"] = { - "endorser_did": "test_public_did_2", - "endorser_alias": "endorser_2", - } - manager = MultiIndyLedgerManager( - self.profile, - production_ledgers=self.production_ledger, - non_production_ledgers=self.non_production_ledger, - writable_ledgers=writable_ledgers, - endorser_map=endorser_info_map, - ) - profile = InMemoryProfile.test_profile() - assert not profile.inject_or(BaseLedger) - assert "test_prod_2" in manager.writable_ledgers - new_write_ledger_id = await manager.set_profile_write_ledger( - profile=profile, ledger_id="test_prod_2" - ) - assert new_write_ledger_id == "test_prod_2" - new_write_ledger = profile.inject_or(BaseLedger) - assert new_write_ledger.pool_name == "test_prod_2" - - async def test_set_profile_write_ledger_x(self): - profile = InMemoryProfile.test_profile() - with self.assertRaises(MultipleLedgerManagerError) as cm: - new_write_ledger_id = await self.manager.set_profile_write_ledger( - profile=profile, ledger_id="test_non_prod_1" - ) - assert "is not write configurable" in str(cm.exception.message) - - async def test_get_ledger_inst_by_id(self): - ledger_inst = await self.manager.get_ledger_inst_by_id("test_prod_2") - assert ledger_inst - ledger_inst = await self.manager.get_ledger_inst_by_id("test_non_prod_2") - assert ledger_inst - ledger_inst = await self.manager.get_ledger_inst_by_id("test_invalid") - assert not ledger_inst - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_ledger_by_did_self_cert_a( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(GET_NYM_REPLY) - mock_wait.return_value = mock_submit.return_value - ( - ledger_id, - ledger_inst, - is_self_certified, - ) = await self.manager._get_ledger_by_did( - "test_prod_1", "Av63wJYM7xYR4AiygYq4c3" - ) - assert ledger_id == "test_prod_1" - assert ledger_inst.pool.name == "test_prod_1" - assert is_self_certified - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_ledger_by_did_self_cert_b( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - self.non_production_ledger = OrderedDict() - self.non_production_ledger["test_non_prod_1"] = IndySdkLedger( - IndySdkLedgerPool("test_non_prod_1", checked=True), self.profile - ) - self.non_production_ledger["test_non_prod_2"] = IndySdkLedger( - IndySdkLedgerPool("test_non_prod_2", checked=True), self.profile - ) - self.manager = MultiIndyLedgerManager( - self.profile, - non_production_ledgers=self.non_production_ledger, - ) - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(GET_NYM_REPLY) - mock_wait.return_value = mock_submit.return_value - ( - ledger_id, - ledger_inst, - is_self_certified, - ) = await self.manager._get_ledger_by_did( - "test_non_prod_1", "Av63wJYM7xYR4AiygYq4c3" - ) - assert ledger_id == "test_non_prod_1" - assert ledger_inst.pool.name == "test_non_prod_1" - assert is_self_certified - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_ledger_by_did_not_self_cert( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - get_nym_reply = deepcopy(GET_NYM_REPLY) - get_nym_reply["result"]["data"] = json.dumps( - { - "dest": "Av63wJYM7xYR4AiygYq4c3", - "identifier": "V4SGRU86Z58d6TV7PBUe6f", - "role": "101", - "seqNo": 17794, - "txnTime": 1632262244, - "verkey": "ABUF7uxYTxZ6qYdZ4G9e1Gi", - } - ) - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait, mock.patch.object( - test_module.SubTrie, "verify_spv_proof", mock.CoroutineMock() - ) as mock_verify_spv_proof: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(get_nym_reply) - mock_wait.return_value = mock_submit.return_value - mock_verify_spv_proof.return_value = True - ( - ledger_id, - ledger_inst, - is_self_certified, - ) = await self.manager._get_ledger_by_did( - "test_prod_1", "Av63wJYM7xYR4AiygYq4c3" - ) - assert ledger_id == "test_prod_1" - assert ledger_inst.pool.name == "test_prod_1" - assert not is_self_certified - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_ledger_by_did_state_proof_not_valid( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - get_nym_reply = deepcopy(GET_NYM_REPLY) - get_nym_reply["result"]["data"]["verkey"] = "ABUF7uxYTxZ6qYdZ4G9e1Gi" - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(get_nym_reply) - mock_wait.return_value = mock_submit.return_value - assert not await self.manager._get_ledger_by_did( - "test_prod_1", "Av63wJYM7xYR4AiygYq4c3" - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_ledger_by_did_no_data( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - get_nym_reply = deepcopy(GET_NYM_REPLY) - get_nym_reply.get("result").pop("data") - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(get_nym_reply) - mock_wait.return_value = mock_submit.return_value - assert not await self.manager._get_ledger_by_did( - "test_prod_1", "Av63wJYM7xYR4AiygYq4c3" - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_ledger_by_did_timeout( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.side_effect = asyncio.TimeoutError - assert not await self.manager._get_ledger_by_did( - "test_prod_1", "Av63wJYM7xYR4AiygYq4c3" - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_ledger_by_did_ledger_error( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.side_effect = LedgerError - assert not await self.manager._get_ledger_by_did( - "test_prod_1", "Av63wJYM7xYR4AiygYq4c3" - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_lookup_did_in_configured_ledgers_self_cert_prod( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(GET_NYM_REPLY) - mock_wait.return_value = mock_submit.return_value - ( - ledger_id, - ledger_inst, - ) = await self.manager.lookup_did_in_configured_ledgers( - "Av63wJYM7xYR4AiygYq4c3", cache_did=True - ) - assert ledger_id == "test_prod_1" - assert ledger_inst.pool.name == "test_prod_1" - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_ledger_by_did_not_self_cert_not_self_cert_prod( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - get_nym_reply = deepcopy(GET_NYM_REPLY) - get_nym_reply["result"]["data"]["verkey"] = "ABUF7uxYTxZ6qYdZ4G9e1Gi" - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait, mock.patch.object( - test_module.SubTrie, "verify_spv_proof", mock.CoroutineMock() - ) as mock_verify_spv_proof: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(get_nym_reply) - mock_wait.return_value = mock_submit.return_value - mock_verify_spv_proof.return_value = True - ( - ledger_id, - ledger_inst, - ) = await self.manager.lookup_did_in_configured_ledgers( - "Av63wJYM7xYR4AiygYq4c3", cache_did=True - ) - assert ledger_id == "test_prod_1" - assert ledger_inst.pool.name == "test_prod_1" - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_lookup_did_in_configured_ledgers_self_cert_non_prod( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - self.non_production_ledger = OrderedDict() - self.non_production_ledger["test_non_prod_1"] = IndySdkLedger( - IndySdkLedgerPool("test_non_prod_1", checked=True), self.profile - ) - self.non_production_ledger["test_non_prod_2"] = IndySdkLedger( - IndySdkLedgerPool("test_non_prod_2", checked=True), self.profile - ) - self.manager = MultiIndyLedgerManager( - self.profile, - non_production_ledgers=self.non_production_ledger, - ) - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(GET_NYM_REPLY) - mock_wait.return_value = mock_submit.return_value - ( - ledger_id, - ledger_inst, - ) = await self.manager.lookup_did_in_configured_ledgers( - "Av63wJYM7xYR4AiygYq4c3", cache_did=True - ) - assert ledger_id == "test_non_prod_1" - assert ledger_inst.pool.name == "test_non_prod_1" - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_ledger_by_did_not_self_cert_not_self_cert_non_prod( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - self.non_production_ledger = OrderedDict() - self.non_production_ledger["test_non_prod_1"] = IndySdkLedger( - IndySdkLedgerPool("test_non_prod_1", checked=True), self.profile - ) - self.non_production_ledger["test_non_prod_2"] = IndySdkLedger( - IndySdkLedgerPool("test_non_prod_2", checked=True), self.profile - ) - self.manager = MultiIndyLedgerManager( - self.profile, - non_production_ledgers=self.non_production_ledger, - ) - get_nym_reply = deepcopy(GET_NYM_REPLY) - get_nym_reply["result"]["data"]["verkey"] = "ABUF7uxYTxZ6qYdZ4G9e1Gi" - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait, mock.patch.object( - test_module.SubTrie, "verify_spv_proof", mock.CoroutineMock() - ) as mock_verify_spv_proof: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(get_nym_reply) - mock_wait.return_value = mock_submit.return_value - mock_verify_spv_proof.return_value = True - ( - ledger_id, - ledger_inst, - ) = await self.manager.lookup_did_in_configured_ledgers( - "Av63wJYM7xYR4AiygYq4c3", cache_did=True - ) - assert ledger_id == "test_non_prod_1" - assert ledger_inst.pool.name == "test_non_prod_1" - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_lookup_did_in_configured_ledgers_x( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait, mock.patch.object( - test_module.SubTrie, "verify_spv_proof", mock.CoroutineMock() - ) as mock_verify_spv_proof: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(GET_NYM_REPLY) - mock_wait.return_value = mock_submit.return_value - mock_verify_spv_proof.return_value = False - with self.assertRaises(MultipleLedgerManagerError) as cm: - await self.manager.lookup_did_in_configured_ledgers( - "Av63wJYM7xYR4AiygYq4c3", cache_did=True - ) - assert "not found in any of the ledgers total: (production: " in cm - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_lookup_did_in_configured_ledgers_prod_not_cached( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - with mock.patch.object( - test_module.asyncio, "wait", mock.CoroutineMock() - ) as mock_wait: - mock_build_get_nym_req.return_value = mock.MagicMock() - mock_submit.return_value = json.dumps(GET_NYM_REPLY) - mock_wait.return_value = mock_submit.return_value - ( - ledger_id, - ledger_inst, - ) = await self.manager.lookup_did_in_configured_ledgers( - "Av63wJYM7xYR4AiygYq4c3", cache_did=False - ) - assert ledger_id == "test_prod_1" - assert ledger_inst.pool.name == "test_prod_1" - - async def test_lookup_did_in_configured_ledgers_cached_prod_ledger(self): - cache = InMemoryCache() - await cache.set("did_ledger_id_resolver::Av63wJYM7xYR4AiygYq4c3", "test_prod_2") - self.profile.context.injector.bind_instance(BaseCache, cache) - ( - ledger_id, - ledger_inst, - ) = await self.manager.lookup_did_in_configured_ledgers( - "Av63wJYM7xYR4AiygYq4c3", cache_did=True - ) - assert ledger_id == "test_prod_2" - assert ledger_inst.pool.name == "test_prod_2" - - async def test_lookup_did_in_configured_ledgers_cached_non_prod_ledger(self): - cache = InMemoryCache() - await cache.set( - "did_ledger_id_resolver::Av63wJYM7xYR4AiygYq4c3", "test_non_prod_2", None - ) - self.profile.context.injector.bind_instance(BaseCache, cache) - ( - ledger_id, - ledger_inst, - ) = await self.manager.lookup_did_in_configured_ledgers( - "Av63wJYM7xYR4AiygYq4c3", cache_did=True - ) - assert ledger_id == "test_non_prod_2" - assert ledger_inst.pool.name == "test_non_prod_2" - - async def test_lookup_did_in_configured_ledgers_cached_x(self): - cache = InMemoryCache() - await cache.set("did_ledger_id_resolver::Av63wJYM7xYR4AiygYq4c3", "invalid_id") - self.profile.context.injector.bind_instance(BaseCache, cache) - with self.assertRaises(MultipleLedgerManagerError) as cm: - await self.manager.lookup_did_in_configured_ledgers( - "Av63wJYM7xYR4AiygYq4c3", cache_did=True - ) - assert "cached ledger_id invalid_id not found in either" in cm - - def test_extract_did_from_identifier(self): - assert ( - self.manager.extract_did_from_identifier( - "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0" - ) - == "WgWxqztrNooG92RXvxSTWv" - ) - assert ( - self.manager.extract_did_from_identifier( - "WgWxqztrNooG92RXvxSTWv:3:CL:20:tag" - ) - == "WgWxqztrNooG92RXvxSTWv" - ) - assert ( - self.manager.extract_did_from_identifier("WgWxqztrNooG92RXvxSTWv") - == "WgWxqztrNooG92RXvxSTWv" - ) - assert ( - self.manager.extract_did_from_identifier("did:sov:WgWxqztrNooG92RXvxSTWv") - == "WgWxqztrNooG92RXvxSTWv" - ) - - async def test_get_production_ledgers(self): - assert len(await self.manager.get_prod_ledgers()) == 2 - - async def test_get_non_production_ledgers(self): - assert len(await self.manager.get_nonprod_ledgers()) == 2 diff --git a/aries_cloudagent/ledger/multiple_ledger/tests/test_manager_provider.py b/aries_cloudagent/ledger/multiple_ledger/tests/test_manager_provider.py index d6b8b70706..80091cfb01 100644 --- a/aries_cloudagent/ledger/multiple_ledger/tests/test_manager_provider.py +++ b/aries_cloudagent/ledger/multiple_ledger/tests/test_manager_provider.py @@ -1,15 +1,12 @@ import pytest -from unittest import mock from unittest import IsolatedAsyncioTestCase from ....askar.profile import AskarProfileManager from ....config.injection_context import InjectionContext from ....core.in_memory import InMemoryProfile -from ....indy.sdk.profile import IndySdkProfile -from ....indy.sdk.wallet_setup import IndyOpenWallet, IndyWalletConfig from ....ledger.base import BaseLedger -from ....ledger.indy import IndySdkLedgerPool, IndySdkLedger +from ....ledger.indy_vdr import IndyVdrLedger, IndyVdrLedgerPool from ..base_manager import MultipleLedgerManagerError from ..manager_provider import MultiIndyLedgerManagerProvider @@ -66,30 +63,6 @@ async def test_provide_invalid_manager(self): with self.assertRaises(MultipleLedgerManagerError): provider.provide(context.settings, context.injector) - @pytest.mark.indy - async def test_provide_indy_manager(self): - context = InjectionContext() - with mock.patch.object(IndySdkProfile, "_make_finalizer"): - profile = IndySdkProfile( - IndyOpenWallet( - config=IndyWalletConfig({"name": "test-profile"}), - created=True, - handle=1, - master_secret_id="master-secret", - ), - context, - ) - context.injector.bind_instance( - BaseLedger, IndySdkLedger(IndySdkLedgerPool("name"), profile) - ) - provider = MultiIndyLedgerManagerProvider(profile) - context.settings["ledger.ledger_config_list"] = LEDGER_CONFIG - context.settings["ledger.genesis_transactions"] = TEST_GENESIS_TXN - self.assertEqual( - provider.provide(context.settings, context.injector).__class__.__name__, - "MultiIndyLedgerManager", - ) - @pytest.mark.askar async def test_provide_askar_manager(self): context = InjectionContext() @@ -104,7 +77,7 @@ async def test_provide_askar_manager(self): }, ) context.injector.bind_instance( - BaseLedger, IndySdkLedger(IndySdkLedgerPool("name"), profile) + BaseLedger, IndyVdrLedger(IndyVdrLedgerPool("name"), profile) ) provider = MultiIndyLedgerManagerProvider(profile) context.settings["ledger.ledger_config_list"] = LEDGER_CONFIG diff --git a/aries_cloudagent/ledger/routes.py b/aries_cloudagent/ledger/routes.py index 4d1f270afe..8fbf8ee102 100644 --- a/aries_cloudagent/ledger/routes.py +++ b/aries_cloudagent/ledger/routes.py @@ -11,9 +11,9 @@ request_schema, response_schema, ) - from marshmallow import fields, validate +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..connections.models.conn_record import ConnRecord from ..messaging.models.base import BaseModelError @@ -262,6 +262,7 @@ class WriteLedgerRequestSchema(OpenAPISchema): @querystring_schema(CreateDidTxnForEndorserOptionSchema()) @querystring_schema(SchemaConnIdMatchInfoSchema()) @response_schema(TxnOrRegisterLedgerNymResponseSchema(), 200, description="") +@tenant_authentication async def register_ledger_nym(request: web.BaseRequest): """Request handler for registering a NYM with the ledger. @@ -425,6 +426,7 @@ async def register_ledger_nym(request: web.BaseRequest): ) @querystring_schema(QueryStringDIDSchema) @response_schema(GetNymRoleResponseSchema(), 200, description="") +@tenant_authentication async def get_nym_role(request: web.BaseRequest): """Request handler for getting the role from the NYM registration of a public DID. @@ -471,6 +473,7 @@ async def get_nym_role(request: web.BaseRequest): @docs(tags=["ledger"], summary="Rotate key pair for public DID.") @response_schema(LedgerModulesResultSchema(), 200, description="") +@tenant_authentication async def rotate_public_did_keypair(request: web.BaseRequest): """Request handler for rotating key pair associated with public DID. @@ -500,6 +503,7 @@ async def rotate_public_did_keypair(request: web.BaseRequest): ) @querystring_schema(QueryStringDIDSchema()) @response_schema(GetDIDVerkeyResponseSchema(), 200, description="") +@tenant_authentication async def get_did_verkey(request: web.BaseRequest): """Request handler for getting a verkey for a DID from the ledger. @@ -548,6 +552,7 @@ async def get_did_verkey(request: web.BaseRequest): ) @querystring_schema(QueryStringEndpointSchema()) @response_schema(GetDIDEndpointResponseSchema(), 200, description="") +@tenant_authentication async def get_did_endpoint(request: web.BaseRequest): """Request handler for getting a verkey for a DID from the ledger. @@ -593,6 +598,7 @@ async def get_did_endpoint(request: web.BaseRequest): @docs(tags=["ledger"], summary="Fetch the current transaction author agreement, if any") @response_schema(TAAResultSchema, 200, description="") +@tenant_authentication async def ledger_get_taa(request: web.BaseRequest): """Request handler for fetching the transaction author agreement. @@ -633,6 +639,7 @@ async def ledger_get_taa(request: web.BaseRequest): @docs(tags=["ledger"], summary="Accept the transaction author agreement") @request_schema(TAAAcceptSchema) @response_schema(LedgerModulesResultSchema(), 200, description="") +@tenant_authentication async def ledger_accept_taa(request: web.BaseRequest): """Request handler for accepting the current transaction author agreement. @@ -693,6 +700,7 @@ async def ledger_accept_taa(request: web.BaseRequest): @docs(tags=["ledger"], summary="Fetch list of available write ledgers") @response_schema(ConfigurableWriteLedgersSchema, 200, description="") +@tenant_authentication async def get_write_ledgers(request: web.BaseRequest): """Request handler for fetching the list of available write ledgers. @@ -714,6 +722,7 @@ async def get_write_ledgers(request: web.BaseRequest): @docs(tags=["ledger"], summary="Fetch the current write ledger") @response_schema(WriteLedgerSchema, 200, description="") +@tenant_authentication async def get_write_ledger(request: web.BaseRequest): """Request handler for fetching the currently set write ledger. @@ -739,6 +748,7 @@ async def get_write_ledger(request: web.BaseRequest): @docs(tags=["ledger"], summary="Set write ledger") @match_info_schema(WriteLedgerRequestSchema()) @response_schema(WriteLedgerSchema, 200, description="") +@tenant_authentication async def set_write_ledger(request: web.BaseRequest): """Request handler for setting write ledger. @@ -769,6 +779,7 @@ async def set_write_ledger(request: web.BaseRequest): tags=["ledger"], summary="Fetch the multiple ledger configuration currently in use" ) @response_schema(LedgerConfigListSchema, 200, description="") +@tenant_authentication async def get_ledger_config(request: web.BaseRequest): """Request handler for fetching the ledger configuration list in use. diff --git a/aries_cloudagent/ledger/tests/test_indy.py b/aries_cloudagent/ledger/tests/test_indy.py deleted file mode 100644 index 9b702d98bd..0000000000 --- a/aries_cloudagent/ledger/tests/test_indy.py +++ /dev/null @@ -1,3498 +0,0 @@ -import asyncio -import json -import tempfile -import pytest - -from os import path - -from aries_cloudagent.tests import mock -from unittest import IsolatedAsyncioTestCase - -from ...config.injection_context import InjectionContext -from ...cache.in_memory import InMemoryCache -from ...indy.issuer import IndyIssuer, IndyIssuerError -from ...indy.sdk.profile import IndySdkProfile -from ...storage.record import StorageRecord -from ...wallet.base import BaseWallet -from ...wallet.did_info import DIDInfo -from ...wallet.did_posture import DIDPosture -from ...wallet.error import WalletNotFoundError -from ...wallet.indy import IndySdkWallet -from ...wallet.key_type import ED25519 -from ...wallet.did_method import SOV - -from ..endpoint_type import EndpointType -from ..indy import ( - BadLedgerRequestError, - ClosedPoolError, - ErrorCode, - IndyErrorHandler, - IndyError, - IndySdkLedger, - IndySdkLedgerPool, - IndySdkLedgerPoolProvider, - GENESIS_TRANSACTION_FILE, - LedgerConfigError, - LedgerError, - LedgerTransactionError, - Role, - TAA_ACCEPTED_RECORD_TYPE, -) - - -GENESIS_TRANSACTION_PATH = path.join( - tempfile.gettempdir(), f"name_{GENESIS_TRANSACTION_FILE}" -) - - -@pytest.mark.indy -class TestIndySdkLedgerPoolProvider(IsolatedAsyncioTestCase): - async def test_provide(self): - provider = IndySdkLedgerPoolProvider() - mock_injector = mock.MagicMock(inject=mock.MagicMock(return_value=None)) - provider.provide( - settings={ - "ledger.read_only": True, - "ledger.genesis_transactions": "genesis-txns", - }, - injector=mock_injector, - ) - - -@pytest.mark.indy -class TestIndySdkLedger(IsolatedAsyncioTestCase): - async def asyncSetUp(self): - self.test_did = "55GkHamhTU1ZbTbV2ab9DE" - self.test_did_info = DIDInfo( - did=self.test_did, - verkey="3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx", - metadata={"test": "test"}, - method=SOV, - key_type=ED25519, - ) - self.test_verkey = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" - context = InjectionContext() - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - with mock.patch.object(IndySdkProfile, "_make_finalizer"): - self.profile = IndySdkProfile( - mock.CoroutineMock(), - context, - ) - self.session = await self.profile.session() - - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.list_pools") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("builtins.open") - async def test_init( - self, mock_open, mock_open_ledger, mock_list_pools, mock_create_config - ): - mock_open.return_value = mock.MagicMock() - mock_list_pools.return_value = [] - - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger( - IndySdkLedgerPool("name", genesis_transactions="genesis_transactions"), - self.profile, - ) - - assert ledger.pool_name == "name" - assert not ledger.read_only - assert ledger.backend - assert ledger.profile is self.profile - - await ledger.__aenter__() - - mock_open.assert_called_once_with(GENESIS_TRANSACTION_PATH, "w") - mock_open.return_value.__enter__.return_value.write.assert_called_once_with( - "genesis_transactions" - ) - mock_create_config.assert_called_once_with( - "name", json.dumps({"genesis_txn": GENESIS_TRANSACTION_PATH}) - ) - assert ledger.did_to_nym(ledger.nym_to_did(self.test_did)) == self.test_did - - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.list_pools") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("builtins.open") - async def test_init_not_checked( - self, mock_open, mock_open_ledger, mock_list_pools, mock_create_config - ): - mock_open.return_value = mock.MagicMock() - mock_list_pools.return_value = [] - - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name"), self.profile) - - assert ledger.pool_name == "name" - assert ledger.backend - assert ledger.profile is self.profile - - with self.assertRaises(LedgerError): - await ledger.__aenter__() - - mock_list_pools.return_value = [{"pool": ledger.pool_name}] - await ledger.__aenter__() - - @mock.patch("indy.pool.list_pools") - @mock.patch("builtins.open") - async def test_init_do_not_recreate(self, mock_open, mock_list_pools): - mock_open.return_value = mock.MagicMock() - mock_list_pools.return_value = [{"pool": "name"}, {"pool": "another"}] - - pool = IndySdkLedgerPool("name") - assert pool.name == "name" - - with self.assertRaises(LedgerConfigError): - await pool.create_pool_config("genesis_transactions", recreate=False) - - mock_open.assert_called_once_with(GENESIS_TRANSACTION_PATH, "w") - - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.delete_pool_ledger_config") - @mock.patch("indy.pool.list_pools") - @mock.patch("builtins.open") - async def test_init_recreate( - self, mock_open, mock_list_pools, mock_delete_config, mock_create_config - ): - mock_open.return_value = mock.MagicMock() - mock_list_pools.return_value = [{"pool": "name"}, {"pool": "another"}] - mock_delete_config.return_value = None - - pool = IndySdkLedgerPool("name") - assert pool.name == "name" - - await pool.create_pool_config("genesis_transactions", recreate=True) - - mock_open.assert_called_once_with(GENESIS_TRANSACTION_PATH, "w") - mock_delete_config.assert_called_once_with("name") - mock_create_config.assert_called_once_with( - "name", json.dumps({"genesis_txn": GENESIS_TRANSACTION_PATH}) - ) - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - async def test_aenter_aexit( - self, mock_close_pool, mock_open_ledger, mock_set_proto - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger as led: - mock_set_proto.assert_called_once_with(2) - mock_open_ledger.assert_called_once_with("name", "{}") - assert led == ledger - mock_close_pool.assert_not_called() - assert led.pool_handle == mock_open_ledger.return_value - - mock_close_pool.assert_called_once() - assert ledger.pool_handle is None - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - async def test_aenter_aexit_nested_keepalive( - self, mock_close_pool, mock_open_ledger, mock_set_proto - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, keepalive=1), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger as led0: - mock_set_proto.assert_called_once_with(2) - mock_open_ledger.assert_called_once_with("name", "{}") - assert led0 == ledger - mock_close_pool.assert_not_called() - assert led0.pool_handle == mock_open_ledger.return_value - - async with ledger as led1: - assert ledger.pool.ref_count == 1 - - mock_close_pool.assert_not_called() # it's a future - assert ledger.pool_handle - - await asyncio.sleep(1.01) - mock_close_pool.assert_called_once() - assert ledger.pool_handle is None - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - async def test_aenter_aexit_close_x( - self, mock_close_pool, mock_open_ledger, mock_set_proto - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_close_pool.side_effect = IndyError(ErrorCode.PoolLedgerTimeout) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - with self.assertRaises(LedgerError): - async with ledger as led: - assert led.pool_handle == mock_open_ledger.return_value - - assert ledger.pool_handle == mock_open_ledger.return_value - assert ledger.pool.ref_count == 1 - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - async def test_submit_pool_closed( - self, mock_close_pool, mock_open_ledger, mock_create_config, mock_set_proto - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - with self.assertRaises(ClosedPoolError) as context: - await ledger._submit("{}") - assert "sign and submit request to closed pool" in str(context.exception) - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("indy.ledger.sign_and_submit_request") - @mock.patch("indy.ledger.multi_sign_request") - async def test_submit_signed( - self, - mock_indy_multi_sign, - mock_sign_submit, - mock_close_pool, - mock_open_ledger, - mock_create_config, - mock_set_proto, - ): - mock_indy_multi_sign.return_value = json.dumps({"endorsed": "content"}) - mock_sign_submit.return_value = '{"op": "REPLY"}' - - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - async with ledger: - mock_wallet_get_public_did.return_value = None - - with self.assertRaises(BadLedgerRequestError): - await ledger._submit("{}", True) - - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - mock_did = mock_wallet_get_public_did.return_value - mock_did.did = self.test_did - - await ledger._submit( - request_json="{}", - sign=True, - taa_accept=False, - ) - - result_json = await ledger._submit( # multi-sign for later endorsement - request_json="{}", - sign=True, - taa_accept=False, - write_ledger=False, - ) - assert json.loads(result_json) == {"endorsed": "content"} - - await ledger.txn_submit( # cover txn_submit() - request_json="{}", - sign=True, - taa_accept=False, - ) - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("indy.ledger.sign_and_submit_request") - @mock.patch("indy.ledger.append_txn_author_agreement_acceptance_to_request") - async def test_submit_signed_taa_accept( - self, - mock_append_taa, - mock_sign_submit, - mock_close_pool, - mock_open_ledger, - mock_create_config, - mock_set_proto, - ): - mock_append_taa.return_value = "{}" - mock_sign_submit.return_value = '{"op": "REPLY"}' - - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True), self.profile - ) - ledger.get_latest_txn_author_acceptance = mock.CoroutineMock( - return_value={ - "text": "sample", - "version": "0.0", - "digest": "digest", - "mechanism": "dummy", - "time": "now", - } - ) - - async with ledger: - mock_did = mock_wallet_get_public_did.return_value - mock_did.did = self.test_did - - await ledger._submit( - request_json="{}", - sign=None, - taa_accept=True, - sign_did=self.test_did_info, - ) - mock_append_taa.assert_called_once_with( - "{}", "sample", "0.0", "digest", "dummy", "now" - ) - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("indy.ledger.submit_request") - async def test_submit_unsigned( - self, - mock_submit, - mock_close_pool, - mock_open_ledger, - mock_create_config, - mock_set_proto, - ): - mock_did = mock.MagicMock() - - future = asyncio.Future() - future.set_result(mock_did) - - mock_submit.return_value = '{"op": "REPLY"}' - - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = future - async with ledger: - await ledger._submit("{}", False) - mock_submit.assert_called_once_with(ledger.pool_handle, "{}") - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("indy.ledger.submit_request") - async def test_submit_unsigned_ledger_transaction_error( - self, - mock_submit, - mock_close_pool, - mock_open_ledger, - mock_create_config, - mock_set_proto, - ): - mock_did = mock.MagicMock() - - future = asyncio.Future() - future.set_result(mock_did) - - mock_submit.return_value = '{"op": "NO-SUCH-OP"}' - - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = future - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True), self.profile - ) - async with ledger: - with self.assertRaises(LedgerTransactionError): - await ledger._submit("{}", False) - mock_submit.assert_called_once_with(ledger.pool_handle, "{}") - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("indy.ledger.submit_request") - async def test_submit_rejected( - self, - mock_submit, - mock_close_pool, - mock_open_ledger, - mock_create_config, - mock_set_proto, - ): - mock_did = mock.MagicMock() - - future = asyncio.Future() - future.set_result(mock_did) - - mock_submit.return_value = '{"op": "REQNACK", "reason": "a reason"}' - - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = future - async with ledger: - with self.assertRaises(LedgerTransactionError) as context: - await ledger._submit("{}", False) - assert "Ledger rejected transaction request" in str(context.exception) - - mock_submit.return_value = '{"op": "REJECT", "reason": "another reason"}' - - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = future - async with ledger: - with self.assertRaises(LedgerTransactionError) as context: - await ledger._submit("{}", False) - assert "Ledger rejected transaction request" in str(context.exception) - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("indy.ledger.multi_sign_request") - async def test_txn_endorse( - self, - mock_indy_multi_sign, - mock_indy_close, - mock_indy_open, - mock_create_config, - mock_set_proto, - ): - mock_indy_multi_sign.return_value = json.dumps({"endorsed": "content"}) - mock_indy_open.return_value = 1 - - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = None - with self.assertRaises(ClosedPoolError): - await ledger.txn_endorse(request_json=json.dumps({"...": "..."})) - - async with ledger: - with self.assertRaises(BadLedgerRequestError): - await ledger.txn_endorse(request_json=json.dumps({"...": "..."})) - - mock_wallet_get_public_did.return_value = self.test_did_info - - endorsed_json = await ledger.txn_endorse( - request_json=json.dumps({"...": "..."}) - ) - assert json.loads(endorsed_json) == {"endorsed": "content"} - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.fetch_schema_by_id") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.fetch_schema_by_seq_no") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_schema_request") - @mock.patch("indy.ledger.append_request_endorser") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_send_schema( - self, - mock_is_ledger_read_only, - mock_append_request_endorser, - mock_build_schema_req, - mock_add_record, - mock_fetch_schema_by_seq_no, - mock_fetch_schema_by_id, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_is_ledger_read_only.return_value = False - - issuer = mock.MagicMock(IndyIssuer) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - issuer.create_schema.return_value = ("schema_issuer_did:name:1.0", "{}") - mock_fetch_schema_by_id.return_value = None - mock_fetch_schema_by_seq_no.return_value = None - - mock_submit.return_value = ( - r'{"op":"REPLY","result":{"txnMetadata":{"seqNo": 1}}}' - ) - future = asyncio.Future() - future.set_result(mock.MagicMock(add_record=mock.CoroutineMock())) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - ledger, "get_indy_storage", mock.MagicMock() - ) as mock_get_storage: - mock_get_storage.return_value = future - async with ledger: - mock_wallet_get_public_did.return_value = None - - with self.assertRaises(BadLedgerRequestError): - schema_id, schema_def = await ledger.create_and_send_schema( - issuer, "schema_name", "schema_version", [1, 2, 3] - ) - - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - mock_did = mock_wallet_get_public_did.return_value - mock_did.did = self.test_did - - schema_id, schema_def = await ledger.create_and_send_schema( - issuer, "schema_name", "schema_version", [1, 2, 3] - ) - issuer.create_schema.assert_called_once_with( - mock_did.did, "schema_name", "schema_version", [1, 2, 3] - ) - - mock_build_schema_req.assert_called_once_with( - mock_did.did, issuer.create_schema.return_value[1] - ) - - mock_submit.assert_called_once_with( - mock_build_schema_req.return_value, - sign=True, - sign_did=mock_wallet_get_public_did.return_value, - taa_accept=None, - write_ledger=True, - ) - - assert schema_id == issuer.create_schema.return_value[0] - - schema_id, signed_txn = await ledger.create_and_send_schema( - issuer=issuer, - schema_name="schema_name", - schema_version="schema_version", - attribute_names=[1, 2, 3], - write_ledger=False, - endorser_did=self.test_did, - ) - assert schema_id == issuer.create_schema.return_value[0] - assert "signed_txn" in signed_txn - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.check_existing_schema") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_schema_request") - async def test_send_schema_already_exists( - self, - mock_build_schema_req, - mock_add_record, - mock_check_existing, - mock_close_pool, - mock_open_ledger, - mock_create_config, - mock_set_proto, - ): - # mock_did = mock.CoroutineMock() - - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - - issuer = mock.MagicMock(IndyIssuer) - issuer.create_schema.return_value = ("1", "{}") - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - mock_add_record = mock.CoroutineMock() - future = asyncio.Future() - future.set_result( - mock.MagicMock(return_value=mock.MagicMock(add_record=mock_add_record)) - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - ledger, "get_indy_storage", mock.MagicMock() - ) as mock_get_storage: - mock_get_storage.return_value = future - mock_wallet_get_public_did.return_value = self.test_did_info - fetch_schema_id = ( - f"{mock_wallet_get_public_did.return_value.did}:2:" - "schema_name:schema_version" - ) - mock_check_existing.return_value = (fetch_schema_id, {}) - - async with ledger: - schema_id, schema_def = await ledger.create_and_send_schema( - issuer, "schema_name", "schema_version", [1, 2, 3] - ) - assert schema_id == fetch_schema_id - assert schema_def == {} - - mock_add_record.assert_not_called() - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.check_existing_schema") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_schema_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_send_schema_ledger_transaction_error_already_exists( - self, - mock_is_ledger_read_only, - mock_build_schema_req, - mock_add_record, - mock_check_existing, - mock_close_pool, - mock_open_ledger, - mock_create_config, - mock_set_proto, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_is_ledger_read_only.return_value = False - - issuer = mock.MagicMock(IndyIssuer) - issuer.create_schema.return_value = ("1", "{}") - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - ledger._submit = mock.CoroutineMock( - side_effect=LedgerTransactionError("UnauthorizedClientRequest") - ) - future = asyncio.Future() - future.set_result(mock.MagicMock(add_record=mock.CoroutineMock())) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - ledger, "get_indy_storage", mock.MagicMock() - ) as mock_get_storage: - mock_get_storage.return_value = future - mock_wallet_get_public_did.return_value = self.test_did_info - fetch_schema_id = ( - f"{mock_wallet_get_public_did.return_value.did}:2:" - "schema_name:schema_version" - ) - mock_check_existing.side_effect = [None, (fetch_schema_id, "{}")] - async with ledger: - schema_id, schema_def = await ledger.create_and_send_schema( - issuer, "schema_name", "schema_version", [1, 2, 3] - ) - assert schema_id == fetch_schema_id - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.check_existing_schema") - async def test_send_schema_ledger_read_only( - self, - mock_check_existing, - mock_close_pool, - mock_open_ledger, - mock_create_config, - mock_set_proto, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - - issuer = mock.MagicMock(IndyIssuer) - issuer.create_schema.return_value = ("1", "{}") - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - fetch_schema_id = ( - f"{mock_wallet_get_public_did.return_value.did}:2:" - "schema_name:schema_version" - ) - mock_check_existing.side_effect = [None, fetch_schema_id] - async with ledger: - with self.assertRaises(LedgerError) as context: - await ledger.create_and_send_schema( - issuer, "schema_name", "schema_version", [1, 2, 3] - ) - assert "read only" in str(context.exception) - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.check_existing_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_send_schema_issuer_error( - self, - mock_is_ledger_read_only, - mock_check_existing, - mock_close_pool, - mock_open_ledger, - mock_create_config, - mock_set_proto, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_is_ledger_read_only.return_value = False - - issuer = mock.MagicMock(IndyIssuer) - issuer.create_schema = mock.CoroutineMock( - side_effect=IndyIssuerError("dummy error") - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - fetch_schema_id = ( - f"{mock_wallet_get_public_did.return_value.did}:2:" - "schema_name:schema_version" - ) - mock_check_existing.side_effect = [None, fetch_schema_id] - async with ledger: - with self.assertRaises(LedgerError) as context: - await ledger.create_and_send_schema( - issuer, "schema_name", "schema_version", [1, 2, 3] - ) - assert "dummy error" in str(context.exception) - - @mock.patch("indy.pool.set_protocol_version") - @mock.patch("indy.pool.create_pool_ledger_config") - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.check_existing_schema") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_schema_request") - async def test_send_schema_ledger_transaction_error( - self, - mock_build_schema_req, - mock_add_record, - mock_check_existing, - mock_close_pool, - mock_open_ledger, - mock_create_config, - mock_set_proto, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - - issuer = mock.MagicMock(IndyIssuer) - issuer.create_schema.return_value = ("1", "{}") - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - ledger._submit = mock.CoroutineMock( - side_effect=LedgerTransactionError("Some other error message") - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - fetch_schema_id = ( - f"{mock_wallet_get_public_did.return_value.did}:2:" - "schema_name:schema_version" - ) - mock_check_existing.side_effect = [None, fetch_schema_id] - async with ledger: - with self.assertRaises(LedgerTransactionError): - await ledger.create_and_send_schema( - issuer, "schema_name", "schema_version", [1, 2, 3] - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.fetch_schema_by_id") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.fetch_schema_by_seq_no") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_schema_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_send_schema_no_seq_no( - self, - mock_is_ledger_read_only, - mock_build_schema_req, - mock_add_record, - mock_fetch_schema_by_seq_no, - mock_fetch_schema_by_id, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - issuer = mock.MagicMock(IndyIssuer) - mock_is_ledger_read_only.return_value = False - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - issuer.create_schema.return_value = ("schema_issuer_did:name:1.0", "{}") - mock_fetch_schema_by_id.return_value = None - mock_fetch_schema_by_seq_no.return_value = None - - mock_submit.return_value = ( - r'{"op":"REPLY","result":{"txnMetadata":{"no": "seqNo"}}}' - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - async with ledger: - mock_wallet.get_public_did = mock.CoroutineMock() - mock_did = mock_wallet_get_public_did.return_value - mock_did.did = self.test_did - - with self.assertRaises(LedgerError) as context: - await ledger.create_and_send_schema( - issuer, "schema_name", "schema_version", [1, 2, 3] - ) - assert "schema sequence number" in str(context.exception) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.fetch_schema_by_id") - async def test_check_existing_schema( - self, - mock_fetch_schema_by_id, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - - mock_fetch_schema_by_id.return_value = {"attrNames": ["a", "b", "c"]} - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - mock_did = mock_wallet_get_public_did.return_value - mock_did.did = self.test_did - async with ledger: - schema_id, schema_def = await ledger.check_existing_schema( - public_did=self.test_did, - schema_name="test", - schema_version="1.0", - attribute_names=["c", "b", "a"], - ) - assert schema_id == f"{self.test_did}:2:test:1.0" - - with self.assertRaises(LedgerTransactionError): - await ledger.check_existing_schema( - public_did=self.test_did, - schema_name="test", - schema_version="1.0", - attribute_names=["a", "b", "c", "d"], - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_schema_request") - @mock.patch("indy.ledger.parse_get_schema_response") - async def test_get_schema( - self, - mock_parse_get_schema_resp, - mock_build_get_schema_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - - mock_parse_get_schema_resp.return_value = (None, '{"attrNames": ["a", "b"]}') - - mock_submit.return_value = '{"result":{"seqNo":1}}' - - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - mock_did = mock_wallet_get_public_did.return_value - mock_did.did = self.test_did - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, cache=InMemoryCache()), - self.profile, - ) - async with ledger: - response = await ledger.get_schema("schema_id") - mock_wallet_get_public_did.assert_called_once_with() - mock_build_get_schema_req.assert_called_once_with( - mock_did.did, "schema_id" - ) - mock_submit.assert_called_once_with( - mock_build_get_schema_req.return_value, sign_did=mock_did - ) - mock_parse_get_schema_resp.assert_called_once_with( - mock_submit.return_value - ) - - assert response == json.loads( - mock_parse_get_schema_resp.return_value[1] - ) - - response == await ledger.get_schema("schema_id") # cover get-from-cache - assert response == json.loads( - mock_parse_get_schema_resp.return_value[1] - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_schema_request") - async def test_get_schema_not_found( - self, - mock_build_get_schema_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - - mock_submit.return_value = json.dumps({"result": {"seqNo": None}}) - - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - mock_did = mock_wallet_get_public_did.return_value - mock_did.did = self.test_did - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, cache=InMemoryCache()), - self.profile, - ) - - async with ledger: - response = await ledger.get_schema("schema_id") - mock_wallet_get_public_did.assert_called_once_with() - mock_build_get_schema_req.assert_called_once_with( - mock_did.did, "schema_id" - ) - mock_submit.assert_called_once_with( - mock_build_get_schema_req.return_value, sign_did=mock_did - ) - - assert response is None - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_txn_request") - @mock.patch("indy.ledger.build_get_schema_request") - @mock.patch("indy.ledger.parse_get_schema_response") - async def test_get_schema_by_seq_no( - self, - mock_parse_get_schema_resp, - mock_build_get_schema_req, - mock_build_get_txn_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - - mock_parse_get_schema_resp.return_value = (None, '{"attrNames": ["a", "b"]}') - - submissions = [ - json.dumps( - { - "result": { - "data": { - "txn": { - "type": "101", - "metadata": {"from": self.test_did}, - "data": { - "data": {"name": "preferences", "version": "1.0"} - }, - } - } - } - } - ), - json.dumps({"result": {"seqNo": 999}}), - ] # need to subscript these in assertions later - mock_submit.side_effect = list( - submissions - ) # becomes list iterator, unsubscriptable, in mock object - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - mock_did = mock_wallet_get_public_did.return_value - mock_did.did = self.test_did - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True), self.profile - ) - async with ledger: - response = await ledger.get_schema("999") - mock_wallet_get_public_did.assert_called_once_with() - mock_build_get_txn_req.assert_called_once_with(None, None, seq_no=999) - mock_build_get_schema_req.assert_called_once_with( - mock_did.did, f"{self.test_did}:2:preferences:1.0" - ) - mock_submit.assert_has_calls( - [ - mock.call(mock_build_get_txn_req.return_value), - mock.call( - mock_build_get_schema_req.return_value, sign_did=mock_did - ), - ] - ) - mock_parse_get_schema_resp.assert_called_once_with(submissions[1]) - - assert response == json.loads( - mock_parse_get_schema_resp.return_value[1] - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_txn_request") - @mock.patch("indy.ledger.build_get_schema_request") - @mock.patch("indy.ledger.parse_get_schema_response") - async def test_get_schema_by_wrong_seq_no( - self, - mock_parse_get_schema_resp, - mock_build_get_schema_req, - mock_build_get_txn_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - - mock_parse_get_schema_resp.return_value = (None, '{"attrNames": ["a", "b"]}') - - submissions = [ - json.dumps( - { - "result": { - "data": { - "txn": { - "type": "102", - } - } - } - } - ), # not a schema - json.dumps({"result": {"seqNo": 999}}), - ] # need to subscript these in assertions later - mock_submit.side_effect = list( - submissions - ) # becomes list iterator, unsubscriptable, in mock object - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - mock_did = mock_wallet_get_public_did.return_value - mock_did.did = self.test_did - async with ledger: - with self.assertRaises(LedgerTransactionError): - await ledger.get_schema("999") - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.find_all_records") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_cred_def_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_send_credential_definition( - self, - mock_is_ledger_read_only, - mock_build_cred_def, - mock_add_record, - mock_find_all_records, - mock_submit, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_find_all_records.return_value = [] - mock_is_ledger_read_only.return_value = False - - mock_get_schema.return_value = {"seqNo": 999} - cred_def_id = f"{self.test_did}:3:CL:999:default" - cred_def_value = { - "primary": {"n": "...", "s": "...", "r": "...", "revocation": None} - } - cred_def = { - "ver": "1.0", - "id": cred_def_id, - "schemaId": "999", - "type": "CL", - "tag": "default", - "value": cred_def_value, - } - cred_def_json = json.dumps(cred_def) - - mock_fetch_cred_def.side_effect = [None, cred_def] - - issuer = mock.MagicMock(IndyIssuer) - issuer.make_credential_definition_id.return_value = cred_def_id - issuer.create_and_store_credential_definition.return_value = ( - cred_def_id, - cred_def_json, - ) - issuer.credential_definition_in_wallet.return_value = False - - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - future = asyncio.Future() - future.set_result(mock.MagicMock(add_record=mock.CoroutineMock())) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - ledger, "get_indy_storage", mock.MagicMock() - ) as mock_get_storage: - mock_get_storage.return_value = future - async with ledger: - mock_wallet_get_public_did.return_value = None - with self.assertRaises(BadLedgerRequestError): - await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - mock_wallet_get_public_did.return_value = DIDInfo( - did=self.test_did, - verkey=self.test_verkey, - metadata=None, - method=SOV, - key_type=ED25519, - ) - mock_did = mock_wallet_get_public_did.return_value - ( - result_id, - result_def, - novel, - ) = await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - assert result_id == cred_def_id - assert novel - mock_get_schema.assert_called_once_with(schema_id) - mock_build_cred_def.assert_called_once_with(mock_did.did, cred_def_json) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.find_all_records") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_cred_def_request") - @mock.patch("indy.ledger.append_request_endorser") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_send_credential_definition_endorse_only( - self, - mock_is_ledger_read_only, - mock_append_request_endorser, - mock_build_cred_def, - mock_add_record, - mock_find_all_records, - mock_submit, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_find_all_records.return_value = [] - mock_is_ledger_read_only.return_value = False - - mock_get_schema.return_value = {"seqNo": 999} - cred_def_id = f"{self.test_did}:3:CL:999:default" - cred_def_value = { - "primary": {"n": "...", "s": "...", "r": "...", "revocation": None} - } - cred_def = { - "ver": "1.0", - "id": cred_def_id, - "schemaId": "999", - "type": "CL", - "tag": "default", - "value": cred_def_value, - } - cred_def_json = json.dumps(cred_def) - - mock_fetch_cred_def.side_effect = [None, cred_def] - - issuer = mock.MagicMock(IndyIssuer) - issuer.make_credential_definition_id.return_value = cred_def_id - issuer.create_and_store_credential_definition.return_value = ( - cred_def_id, - cred_def_json, - ) - issuer.credential_definition_in_wallet.return_value = False - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = DIDInfo( - self.test_did, - self.test_verkey, - None, - SOV, - ED25519, - ) - async with ledger: - ( - result_id, - signed_txn, - novel, - ) = await ledger.create_and_send_credential_definition( - issuer=issuer, - schema_id=schema_id, - signature_type=None, - tag=tag, - support_revocation=False, - write_ledger=False, - endorser_did=self.test_did, - ) - assert "signed_txn" in signed_txn - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.find_all_records") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_cred_def_request") - async def test_send_credential_definition_exists_in_ledger_and_wallet( - self, - mock_build_cred_def, - mock_add_record, - mock_find_all_records, - mock_submit, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_find_all_records.return_value = [] - - mock_get_schema.return_value = {"seqNo": 999} - cred_def_id = f"{self.test_did}:3:CL:999:default" - cred_def_value = { - "primary": {"n": "...", "s": "...", "r": "...", "revocation": None} - } - cred_def = { - "ver": "1.0", - "id": cred_def_id, - "schemaId": "999", - "type": "CL", - "tag": "default", - "value": cred_def_value, - } - cred_def_json = json.dumps(cred_def) - - mock_fetch_cred_def.return_value = {"mock": "cred-def"} - - issuer = mock.MagicMock(IndyIssuer) - issuer.make_credential_definition_id.return_value = cred_def_id - issuer.create_and_store_credential_definition.return_value = ( - cred_def_id, - cred_def_json, - ) - issuer.credential_definition_in_wallet.return_value = True - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - future = asyncio.Future() - future.set_result(mock.MagicMock(add_record=mock.CoroutineMock())) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - ledger, "get_indy_storage", mock.MagicMock() - ) as mock_get_storage: - mock_get_storage.return_value = future - mock_wallet_get_public_did.return_value = DIDInfo( - did=self.test_did, - verkey=self.test_verkey, - metadata=None, - method=SOV, - key_type=ED25519, - ) - - async with ledger: - mock_did = mock_wallet_get_public_did.return_value - - ( - result_id, - result_def, - novel, - ) = await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - assert result_id == cred_def_id - assert not novel - - mock_wallet_get_public_did.assert_called_once_with() - mock_get_schema.assert_called_once_with(schema_id) - - mock_build_cred_def.assert_not_called() - mock_get_storage.assert_not_called() - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - async def test_send_credential_definition_no_such_schema( - self, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_get_schema.return_value = {} - - issuer = mock.MagicMock(IndyIssuer) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - async with ledger: - with self.assertRaises(LedgerError): - await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.find_all_records") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_cred_def_request") - async def test_send_credential_definition_offer_exception( - self, - mock_build_cred_def, - mock_add_record, - mock_find_all_records, - mock_submit, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_find_all_records.return_value = [] - - mock_get_schema.return_value = {"seqNo": 999} - - issuer = mock.MagicMock(IndyIssuer) - issuer.credential_definition_in_wallet.side_effect = IndyIssuerError( - "common IO error" - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - async with ledger: - with self.assertRaises(LedgerError): - await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - async def test_send_credential_definition_cred_def_in_wallet_not_ledger( - self, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_get_schema.return_value = {"seqNo": 999} - cred_def_id = f"{self.test_did}:3:CL:999:default" - cred_def_value = { - "primary": {"n": "...", "s": "...", "r": "...", "revocation": None} - } - cred_def = { - "ver": "1.0", - "id": cred_def_id, - "schemaId": "999", - "type": "CL", - "tag": "default", - "value": cred_def_value, - } - cred_def_json = json.dumps(cred_def) - - mock_fetch_cred_def.return_value = {} - - issuer = mock.MagicMock(IndyIssuer) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - async with ledger: - with self.assertRaises(LedgerError): - await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - async def test_send_credential_definition_cred_def_not_on_ledger_wallet_check_x( - self, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_get_schema.return_value = {"seqNo": 999} - cred_def_id = f"{self.test_did}:3:CL:999:default" - cred_def_value = { - "primary": {"n": "...", "s": "...", "r": "...", "revocation": None} - } - cred_def = { - "ver": "1.0", - "id": cred_def_id, - "schemaId": "999", - "type": "CL", - "tag": "default", - "value": cred_def_value, - } - cred_def_json = json.dumps(cred_def) - - mock_fetch_cred_def.return_value = {} - - issuer = mock.MagicMock(IndyIssuer) - issuer.credential_definition_in_wallet = mock.CoroutineMock( - side_effect=IndyIssuerError("dummy error") - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - async with ledger: - with self.assertRaises(LedgerError) as context: - await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - assert "dummy error" in str(context.exception) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - async def test_send_credential_definition_cred_def_not_on_ledger_nor_wallet_send_x( - self, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_get_schema.return_value = {"seqNo": 999} - cred_def_id = f"{self.test_did}:3:CL:999:default" - cred_def_value = { - "primary": {"n": "...", "s": "...", "r": "...", "revocation": None} - } - cred_def = { - "ver": "1.0", - "id": cred_def_id, - "schemaId": "999", - "type": "CL", - "tag": "default", - "value": cred_def_value, - } - cred_def_json = json.dumps(cred_def) - - mock_fetch_cred_def.return_value = {} - - issuer = mock.MagicMock(IndyIssuer) - issuer.credential_definition_in_wallet = mock.CoroutineMock(return_value=False) - issuer.create_and_store_credential_definition = mock.CoroutineMock( - side_effect=IndyIssuerError("dummy error") - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - async with ledger: - with self.assertRaises(LedgerError) as context: - await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - assert "dummy error" in str(context.exception) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - async def test_send_credential_definition_read_only( - self, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_get_schema.return_value = {"seqNo": 999} - cred_def_id = f"{self.test_did}:3:CL:999:default" - cred_def_value = { - "primary": {"n": "...", "s": "...", "r": "...", "revocation": None} - } - cred_def = { - "ver": "1.0", - "id": cred_def_id, - "schemaId": "999", - "type": "CL", - "tag": "default", - "value": cred_def_value, - } - cred_def_json = json.dumps(cred_def) - - mock_fetch_cred_def.return_value = {} - - issuer = mock.MagicMock(IndyIssuer) - issuer.credential_definition_in_wallet = mock.CoroutineMock(return_value=False) - issuer.create_and_store_credential_definition = mock.CoroutineMock( - return_value=("cred-def-id", "cred-def-json") - ) - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - async with ledger: - with self.assertRaises(LedgerError) as context: - await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - assert "read only" in str(context.exception) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - async def test_send_credential_definition_cred_def_on_ledger_not_in_wallet( - self, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_get_schema.return_value = {"seqNo": 999} - cred_def_id = f"{self.test_did}:3:CL:999:default" - cred_def_value = { - "primary": {"n": "...", "s": "...", "r": "...", "revocation": None} - } - cred_def = { - "ver": "1.0", - "id": cred_def_id, - "schemaId": "999", - "type": "CL", - "tag": "default", - "value": cred_def_value, - } - cred_def_json = json.dumps(cred_def) - - mock_fetch_cred_def.return_value = cred_def - - issuer = mock.MagicMock(IndyIssuer) - issuer.credential_definition_in_wallet = mock.CoroutineMock(return_value=False) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - async with ledger: - with self.assertRaises(LedgerError): - await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.find_all_records") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_cred_def_request") - async def test_send_credential_definition_on_ledger_in_wallet( - self, - mock_build_cred_def, - mock_add_record, - mock_find_all_records, - mock_submit, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_find_all_records.return_value = [] - - mock_get_schema.return_value = {"seqNo": 999} - cred_def_id = f"{self.test_did}:3:CL:999:default" - cred_def_value = { - "primary": {"n": "...", "s": "...", "r": "...", "revocation": None} - } - cred_def = { - "ver": "1.0", - "id": cred_def_id, - "schemaId": "999", - "type": "CL", - "tag": "default", - "value": cred_def_value, - } - cred_def_json = json.dumps(cred_def) - - mock_fetch_cred_def.return_value = cred_def - - issuer = mock.MagicMock(IndyIssuer) - issuer.make_credential_definition_id.return_value = cred_def_id - issuer.create_and_store_credential_definition.return_value = ( - cred_def_id, - cred_def_json, - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - async with ledger: - mock_wallet_get_public_did.return_value = None - with self.assertRaises(BadLedgerRequestError): - await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - - mock_wallet_get_public_did.return_value = DIDInfo( - did=self.test_did, - verkey=self.test_verkey, - metadata=None, - method=SOV, - key_type=ED25519, - ) - mock_did = mock_wallet_get_public_did.return_value - - ( - result_id, - result_def, - novel, - ) = await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - assert result_id == cred_def_id - - mock_get_schema.assert_called_once_with(schema_id) - - mock_build_cred_def.assert_not_called() - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch( - "aries_cloudagent.ledger.indy.IndySdkLedger.fetch_credential_definition" - ) - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.find_all_records") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("indy.ledger.build_cred_def_request") - async def test_send_credential_definition_create_cred_def_exception( - self, - mock_build_cred_def, - mock_add_record, - mock_find_all_records, - mock_submit, - mock_fetch_cred_def, - mock_close, - mock_open, - mock_get_schema, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_find_all_records.return_value = [] - - mock_get_schema.return_value = {"seqNo": 999} - cred_def_id = f"{self.test_did}:3:CL:999:default" - cred_def_value = { - "primary": {"n": "...", "s": "...", "r": "...", "revocation": None} - } - cred_def = { - "ver": "1.0", - "id": cred_def_id, - "schemaId": "999", - "type": "CL", - "tag": "default", - "value": cred_def_value, - } - cred_def_json = json.dumps(cred_def) - - mock_fetch_cred_def.return_value = None - - issuer = mock.MagicMock(IndyIssuer) - issuer.create_and_store_credential_definition.side_effect = IndyIssuerError( - "invalid structure" - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - schema_id = "schema_issuer_did:name:1.0" - tag = "default" - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = DIDInfo( - did=self.test_did, - verkey=self.test_verkey, - metadata=None, - method=SOV, - key_type=ED25519, - ) - async with ledger: - with self.assertRaises(LedgerError): - await ledger.create_and_send_credential_definition( - issuer, schema_id, None, tag - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_cred_def_request") - @mock.patch("indy.ledger.parse_get_cred_def_response") - async def test_get_credential_definition( - self, - mock_parse_get_cred_def_resp, - mock_build_get_cred_def_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_parse_get_cred_def_resp.return_value = ( - None, - json.dumps({"result": {"seqNo": 1}}), - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = mock.CoroutineMock() - mock_did = mock_wallet_get_public_did.return_value - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, cache=InMemoryCache()), - self.profile, - ) - - async with ledger: - response = await ledger.get_credential_definition("cred_def_id") - mock_wallet_get_public_did.assert_called_once_with() - mock_build_get_cred_def_req.assert_called_once_with( - mock_did.did, "cred_def_id" - ) - mock_submit.assert_called_once_with( - mock_build_get_cred_def_req.return_value, sign_did=mock_did - ) - mock_parse_get_cred_def_resp.assert_called_once_with( - mock_submit.return_value - ) - assert response == json.loads( - mock_parse_get_cred_def_resp.return_value[1] - ) - response == await ledger.get_credential_definition( # cover get-from-cache - "cred_def_id" - ) - assert response == json.loads( - mock_parse_get_cred_def_resp.return_value[1] - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_cred_def_request") - @mock.patch("indy.ledger.parse_get_cred_def_response") - async def test_get_credential_definition_ledger_not_found( - self, - mock_parse_get_cred_def_resp, - mock_build_get_cred_def_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - - mock_parse_get_cred_def_resp.side_effect = IndyError( - error_code=ErrorCode.LedgerNotFound, error_details={"message": "not today"} - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True), self.profile - ) - async with ledger: - response = await ledger.get_credential_definition("cred_def_id") - mock_did = mock_wallet_get_public_did.return_value - mock_wallet_get_public_did.assert_called_once_with() - mock_build_get_cred_def_req.assert_called_once_with( - mock_did.did, "cred_def_id" - ) - mock_submit.assert_called_once_with( - mock_build_get_cred_def_req.return_value, sign_did=mock_did - ) - mock_parse_get_cred_def_resp.assert_called_once_with( - mock_submit.return_value - ) - - assert response is None - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_cred_def_request") - @mock.patch("indy.ledger.parse_get_cred_def_response") - async def test_fetch_credential_definition_ledger_x( - self, - mock_parse_get_cred_def_resp, - mock_build_get_cred_def_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - - mock_parse_get_cred_def_resp.side_effect = IndyError( - error_code=ErrorCode.CommonInvalidParam1, - error_details={"message": "not today"}, - ) - - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - with self.assertRaises(LedgerError) as context: - await ledger.fetch_credential_definition("cred_def_id") - assert "not today" in str(context.exception) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_key_for_did( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_submit.return_value = json.dumps( - {"result": {"data": json.dumps({"verkey": self.test_verkey})}} - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.get_key_for_did(self.test_did) - - mock_build_get_nym_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - ) - mock_submit.assert_called_once_with( - mock_build_get_nym_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ) - assert response == self.test_verkey - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_endpoint_for_did( - self, mock_submit, mock_build_get_attrib_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - endpoint = "http://aries.ca" - mock_submit.return_value = json.dumps( - {"result": {"data": json.dumps({"endpoint": {"endpoint": endpoint}})}} - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.get_endpoint_for_did(self.test_did) - - mock_build_get_attrib_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - "endpoint", - None, - None, - ) - mock_submit.assert_called_once_with( - mock_build_get_attrib_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ) - assert response == endpoint - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_endpoint_of_type_profile_for_did( - self, mock_submit, mock_build_get_attrib_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - endpoint = "http://company.com/masterdata" - endpoint_type = EndpointType.PROFILE - mock_submit.return_value = json.dumps( - { - "result": { - "data": json.dumps( - {"endpoint": {EndpointType.PROFILE.indy: endpoint}} - ) - } - } - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.get_endpoint_for_did( - self.test_did, - endpoint_type, - ) - - mock_build_get_attrib_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - "endpoint", - None, - None, - ) - mock_submit.assert_called_once_with( - mock_build_get_attrib_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ) - assert response == endpoint - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_all_endpoints_for_did( - self, mock_submit, mock_build_get_attrib_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - profile_endpoint = "http://company.com/masterdata" - default_endpoint = "http://agent.company.com" - data_json = json.dumps( - {"endpoint": {"endpoint": default_endpoint, "profile": profile_endpoint}} - ) - mock_submit.return_value = json.dumps({"result": {"data": data_json}}) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.get_all_endpoints_for_did(self.test_did) - - mock_build_get_attrib_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - "endpoint", - None, - None, - ) - mock_submit.assert_called_once_with( - mock_build_get_attrib_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ) - assert response == json.loads(data_json).get("endpoint") - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_all_endpoints_for_did_none( - self, mock_submit, mock_build_get_attrib_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - profile_endpoint = "http://company.com/masterdata" - default_endpoint = "http://agent.company.com" - mock_submit.return_value = json.dumps({"result": {"data": None}}) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.get_all_endpoints_for_did(self.test_did) - - mock_build_get_attrib_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - "endpoint", - None, - None, - ) - mock_submit.assert_called_once_with( - mock_build_get_attrib_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ) - assert response is None - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_endpoint_for_did_address_none( - self, mock_submit, mock_build_get_attrib_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_submit.return_value = json.dumps( - {"result": {"data": json.dumps({"endpoint": None})}} - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.get_endpoint_for_did(self.test_did) - - mock_build_get_attrib_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - "endpoint", - None, - None, - ) - mock_submit.assert_called_once_with( - mock_build_get_attrib_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ) - assert response is None - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_endpoint_for_did_no_endpoint( - self, mock_submit, mock_build_get_attrib_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_submit.return_value = json.dumps({"result": {"data": None}}) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.get_endpoint_for_did(self.test_did) - - mock_build_get_attrib_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - "endpoint", - None, - None, - ) - mock_submit.assert_called_once_with( - mock_build_get_attrib_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ) - assert response is None - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("indy.ledger.build_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_update_endpoint_for_did( - self, - mock_is_ledger_read_only, - mock_submit, - mock_build_attrib_req, - mock_build_get_attrib_req, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - endpoint = ["http://old.aries.ca", "http://new.aries.ca"] - mock_is_ledger_read_only.return_value = False - mock_submit.side_effect = [ - json.dumps( - { - "result": { - "data": json.dumps({"endpoint": {"endpoint": endpoint[i]}}) - } - } - ) - for i in range(len(endpoint)) - ] - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.update_endpoint_for_did( - self.test_did, endpoint[1] - ) - - mock_build_get_attrib_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - "endpoint", - None, - None, - ) - mock_submit.assert_has_calls( - [ - mock.call( - mock_build_get_attrib_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ), - mock.call(mock_build_attrib_req.return_value, True, True), - ] - ) - assert response - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @pytest.mark.asyncio - async def test_construct_attr_json_with_routing_keys(self, mock_close, mock_open): - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - async with ledger: - attr_json = await ledger._construct_attr_json( - "https://url", - EndpointType.ENDPOINT, - routing_keys=["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], - ) - assert attr_json == json.dumps( - { - "endpoint": { - "endpoint": "https://url", - "routingKeys": ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], - } - } - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @pytest.mark.asyncio - async def test_construct_attr_json_with_routing_keys_all_exist_endpoints( - self, mock_close, mock_open - ): - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - async with ledger: - attr_json = await ledger._construct_attr_json( - "https://url", - EndpointType.ENDPOINT, - all_exist_endpoints={"profile": "https://endpoint/profile"}, - routing_keys=["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], - ) - assert attr_json == json.dumps( - { - "endpoint": { - "profile": "https://endpoint/profile", - "endpoint": "https://url", - "routingKeys": ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"], - } - } - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("indy.ledger.build_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - @pytest.mark.asyncio - async def test_update_endpoint_for_did_calls_attr_json( - self, - mock_is_ledger_read_only, - mock_submit, - mock_build_attrib_req, - mock_build_get_attrib_req, - mock_close, - mock_open, - ): - routing_keys = ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"] - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - mock_is_ledger_read_only.return_value = False - async with ledger: - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - ledger, - "_construct_attr_json", - mock.CoroutineMock( - return_value=json.dumps( - { - "endpoint": { - "endpoint": { - "endpoint": "https://url", - "routingKeys": [], - } - } - } - ) - ), - ) as mock_construct_attr_json, mock.patch.object( - ledger, - "get_all_endpoints_for_did", - mock.CoroutineMock(return_value={}), - ), mock.patch.object( - ledger, "did_to_nym" - ): - mock_wallet_get_public_did.return_value = self.test_did_info - await ledger.update_endpoint_for_did( - mock_wallet_get_public_did, - "https://url", - EndpointType.ENDPOINT, - routing_keys=routing_keys, - ) - mock_construct_attr_json.assert_called_once_with( - "https://url", EndpointType.ENDPOINT, {}, routing_keys - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("indy.ledger.build_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_update_endpoint_for_did_no_prior_endpoints( - self, - mock_is_ledger_read_only, - mock_submit, - mock_build_attrib_req, - mock_build_get_attrib_req, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - endpoint = "http://new.aries.ca" - mock_is_ledger_read_only.return_value = False - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - with mock.patch.object( - ledger, "get_all_endpoints_for_did", mock.CoroutineMock() - ) as mock_get_all: - mock_get_all.return_value = None - response = await ledger.update_endpoint_for_did( - self.test_did, endpoint - ) - - mock_build_get_attrib_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - "endpoint", - None, - None, - ) - mock_submit.assert_has_calls( - [ - mock.call(mock_build_attrib_req.return_value, True, True), - ] - ) - assert response - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("indy.ledger.build_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_update_endpoint_of_type_profile_for_did( - self, - mock_is_ledger_read_only, - mock_submit, - mock_build_attrib_req, - mock_build_get_attrib_req, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - endpoint = ["http://company.com/oldProfile", "http://company.com/newProfile"] - endpoint_type = EndpointType.PROFILE - mock_is_ledger_read_only.return_value = False - mock_submit.side_effect = [ - json.dumps( - { - "result": { - "data": json.dumps( - {"endpoint": {endpoint_type.indy: endpoint[i]}} - ) - } - } - ) - for i in range(len(endpoint)) - ] - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - # ledger = mock.patch.object( - # ledger, - # "is_ledger_read_only", - # mock.CoroutineMock(return_value=False), - # ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.update_endpoint_for_did( - self.test_did, endpoint[1], endpoint_type - ) - - mock_build_get_attrib_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - "endpoint", - None, - None, - ) - mock_submit.assert_has_calls( - [ - mock.call( - mock_build_get_attrib_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ), - mock.call(mock_build_attrib_req.return_value, True, True), - ] - ) - assert response - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_update_endpoint_for_did_duplicate( - self, mock_submit, mock_build_get_attrib_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - endpoint = "http://aries.ca" - mock_submit.return_value = json.dumps( - {"result": {"data": json.dumps({"endpoint": {"endpoint": endpoint}})}} - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.update_endpoint_for_did(self.test_did, endpoint) - - mock_build_get_attrib_req.assert_called_once_with( - self.test_did, - ledger.did_to_nym(self.test_did), - "endpoint", - None, - None, - ) - mock_submit.assert_called_once_with( - mock_build_get_attrib_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ) - assert not response - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_attrib_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_update_endpoint_for_did_read_only( - self, mock_submit, mock_build_get_attrib_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - endpoint = "http://aries.ca" - mock_submit.return_value = json.dumps( - {"result": {"data": json.dumps({"endpoint": {"endpoint": endpoint}})}} - ) - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - with self.assertRaises(LedgerError) as context: - await ledger.update_endpoint_for_did( - self.test_did, "distinct endpoint" - ) - assert "read only" in str(context.exception) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_register_nym( - self, - mock_is_ledger_read_only, - mock_submit, - mock_build_nym_req, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_is_ledger_read_only.return_value = False - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - IndySdkWallet, "get_local_did" - ) as mock_wallet_get_local_did, mock.patch.object( - IndySdkWallet, "replace_local_did_metadata" - ) as mock_wallet_replace_local_did_metadata: - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True), self.profile - ) - mock_wallet_get_public_did.return_value = self.test_did_info - mock_wallet_get_local_did.return_value = self.test_did_info - mock_wallet_replace_local_did_metadata.return_value = mock.CoroutineMock() - async with ledger: - await ledger.register_nym( - self.test_did, - self.test_verkey, - "alias", - None, - ) - mock_build_nym_req.assert_called_once_with( - self.test_did, - self.test_did, - self.test_verkey, - "alias", - None, - ) - mock_submit.assert_called_once_with( - mock_build_nym_req.return_value, - True, - True, - sign_did=mock_wallet_get_public_did.return_value, - ) - mock_wallet_replace_local_did_metadata.assert_called_once_with( - self.test_did_info.did, - { - "test": "test", - **DIDPosture.POSTED.metadata, - }, - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - async def test_register_nym_read_only(self, mock_close, mock_open): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - with self.assertRaises(LedgerError) as context: - await ledger.register_nym( - self.test_did, - self.test_verkey, - "alias", - None, - ) - assert "read only" in str(context.exception) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_register_nym_no_public_did( - self, - mock_is_ledger_read_only, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock( - type="indy", - get_local_did=mock.CoroutineMock(), - replace_local_did_metadata=mock.CoroutineMock(), - ) - mock_is_ledger_read_only.return_value = False - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = None - async with ledger: - with self.assertRaises(WalletNotFoundError): - await ledger.register_nym( - self.test_did, - self.test_verkey, - "alias", - None, - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_register_nym_ledger_x( - self, - mock_is_ledger_read_only, - mock_submit, - mock_build_nym_req, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - mock_build_nym_req.side_effect = IndyError( - error_code=ErrorCode.CommonInvalidParam1, - error_details={"message": "not today"}, - ) - mock_is_ledger_read_only.return_value = False - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - with self.assertRaises(LedgerError): - await ledger.register_nym( - self.test_did, - self.test_verkey, - "alias", - None, - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.is_ledger_read_only") - async def test_register_nym_steward_register_others_did( - self, - mock_is_ledger_read_only, - mock_submit, - mock_build_nym_req, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_is_ledger_read_only.return_value = False - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - IndySdkWallet, "get_local_did" - ) as mock_wallet_get_local_did, mock.patch.object( - IndySdkWallet, "replace_local_did_metadata" - ) as mock_wallet_replace_local_did_metadata: - mock_wallet_get_public_did.return_value = self.test_did_info - mock_wallet_get_local_did.side_effect = WalletNotFoundError() - mock_wallet_replace_local_did_metadata.return_value = mock.CoroutineMock() - async with ledger: - await ledger.register_nym( - self.test_did, - self.test_verkey, - "alias", - None, - ) - mock_build_nym_req.assert_called_once_with( - self.test_did, - self.test_did, - self.test_verkey, - "alias", - None, - ) - mock_submit.assert_called_once_with( - mock_build_nym_req.return_value, - True, - True, - sign_did=mock_wallet_get_public_did.return_value, - ) - mock_wallet_replace_local_did_metadata.assert_not_called() - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_nym_role( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_submit.return_value = json.dumps( - { - "result": { - "dest": "GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa", - "txnTime": 1597858571, - "reqId": 1597858571783588400, - "state_proof": { - "root_hash": "7K26MUQt8E2X1vsRJUmc2298VtY8YC5BSDfT5CRJeUDi", - "proof_nodes": "+QHo...", - "multi_signature": { - "participants": ["Node4", "Node3", "Node2"], - "value": { - "state_root_hash": "7K2...", - "pool_state_root_hash": "GT8...", - "ledger_id": 1, - "txn_root_hash": "Hnr...", - "timestamp": 1597858571, - }, - "signature": "QuX...", - }, - }, - "data": json.dumps( - { - "dest": "GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa", - "identifier": "V4SGRU86Z58d6TV7PBUe6f", - "role": 101, - "seqNo": 11, - "txnTime": 1597858571, - "verkey": "GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa", - } - ), - "seqNo": 11, - "identifier": "GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa", - "type": "105", - }, - "op": "REPLY", - } - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - assert await ledger.get_nym_role(self.test_did) == Role.ENDORSER - mock_build_get_nym_req.assert_called_once_with( - self.test_did, - self.test_did, - ) - mock_submit.assert_called_once_with(mock_build_get_nym_req.return_value) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - async def test_get_nym_role_indy_x( - self, mock_build_get_nym_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_build_get_nym_req.side_effect = IndyError( - error_code=ErrorCode.CommonInvalidParam1, - error_details={"message": "not today"}, - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - with self.assertRaises(LedgerError) as context: - await ledger.get_nym_role(self.test_did) - assert "not today" in context.exception.message - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_nym_role_did_not_public_x( - self, mock_submit, mock_build_get_nym_req, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_submit.return_value = json.dumps( - { - "result": { - "dest": "GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa", - "txnTime": 1597858571, - "reqId": 1597858571783588400, - "state_proof": { - "root_hash": "7K26MUQt8E2X1vsRJUmc2298VtY8YC5BSDfT5CRJeUDi", - "proof_nodes": "+QHo...", - "multi_signature": { - "participants": ["Node4", "Node3", "Node2"], - "value": { - "state_root_hash": "7K2...", - "pool_state_root_hash": "GT8...", - "ledger_id": 1, - "txn_root_hash": "Hnr...", - "timestamp": 1597858571, - }, - "signature": "QuX...", - }, - }, - "data": json.dumps(None), - "seqNo": 11, - "identifier": "GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa", - "type": "105", - }, - "op": "REPLY", - } - ) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - with self.assertRaises(BadLedgerRequestError): - await ledger.get_nym_role(self.test_did) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("indy.ledger.build_get_txn_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.register_nym") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_rotate_public_did_keypair( - self, - mock_submit, - mock_register_nym, - mock_build_get_txn_request, - mock_build_get_nym_request, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_submit.side_effect = [ - json.dumps({"result": {"data": json.dumps({"seqNo": 1234})}}), - json.dumps( - { - "result": { - "data": {"txn": {"data": {"role": "101", "alias": "Billy"}}} - } - } - ), - ] - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - IndySdkWallet, "rotate_did_keypair_start", autospec=True - ) as mock_wallet_rotate_did_keypair_start, mock.patch.object( - IndySdkWallet, "rotate_did_keypair_apply", autospec=True - ) as mock_wallet_rotate_did_keypair_apply: - mock_wallet_get_public_did.return_value = self.test_did_info - mock_wallet_rotate_did_keypair_start.return_value = self.test_verkey - mock_wallet_rotate_did_keypair_apply.return_value = None - async with ledger: - await ledger.rotate_public_did_keypair() - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_rotate_public_did_keypair_no_nym( - self, mock_submit, mock_build_get_nym_request, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_submit.return_value = json.dumps({"result": {"data": json.dumps(None)}}) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - IndySdkWallet, "rotate_did_keypair_start", autospec=True - ) as mock_wallet_rotate_did_keypair_start, mock.patch.object( - IndySdkWallet, "rotate_did_keypair_apply", autospec=True - ) as mock_wallet_rotate_did_keypair_apply: - mock_wallet_get_public_did.return_value = self.test_did_info - mock_wallet_rotate_did_keypair_start.return_value = self.test_verkey - mock_wallet_rotate_did_keypair_apply.return_value = None - async with ledger: - with self.assertRaises(BadLedgerRequestError): - await ledger.rotate_public_did_keypair() - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_nym_request") - @mock.patch("indy.ledger.build_get_txn_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.register_nym") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_rotate_public_did_keypair_corrupt_nym_txn( - self, - mock_submit, - mock_register_nym, - mock_build_get_txn_request, - mock_build_get_nym_request, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_submit.side_effect = [ - json.dumps({"result": {"data": json.dumps({"seqNo": 1234})}}), - json.dumps({"result": {"data": None}}), - ] - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - IndySdkWallet, "rotate_did_keypair_start", autospec=True - ) as mock_wallet_rotate_did_keypair_start, mock.patch.object( - IndySdkWallet, "rotate_did_keypair_apply", autospec=True - ) as mock_wallet_rotate_did_keypair_apply: - mock_wallet_get_public_did.return_value = self.test_did_info - mock_wallet_rotate_did_keypair_start.return_value = self.test_verkey - mock_wallet_rotate_did_keypair_apply.return_value = None - async with ledger: - with self.assertRaises(BadLedgerRequestError): - await ledger.rotate_public_did_keypair() - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_revoc_reg_def_request") - @mock.patch("indy.ledger.parse_get_revoc_reg_def_response") - async def test_get_revoc_reg_def( - self, - mock_indy_parse_get_rrdef_resp, - mock_indy_build_get_rrdef_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_indy_parse_get_rrdef_resp.return_value = ( - "rr-id", - json.dumps({"...": "..."}), - ) - mock_submit.return_value = json.dumps({"result": {"txnTime": 1234567890}}) - - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - result = await ledger.get_revoc_reg_def("rr-id") - assert result == {"...": "...", "txnTime": 1234567890} - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_revoc_reg_def_request") - async def test_get_revoc_reg_def_indy_x( - self, mock_indy_build_get_rrdef_req, mock_submit, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - mock_indy_build_get_rrdef_req.side_effect = IndyError( - error_code=ErrorCode.CommonInvalidParam1, - error_details={"message": "not today"}, - ) - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - with self.assertRaises(IndyError) as context: - await ledger.get_revoc_reg_def("rr-id") - assert "not today" in context.exception.message - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_revoc_reg_request") - @mock.patch("indy.ledger.parse_get_revoc_reg_response") - async def test_get_revoc_reg_entry( - self, - mock_indy_parse_get_rr_resp, - mock_indy_build_get_rr_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_indy_parse_get_rr_resp.return_value = ( - "rr-id", - '{"hello": "world"}', - 1234567890, - ) - - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - (result, _) = await ledger.get_revoc_reg_entry("rr-id", 1234567890) - assert result == {"hello": "world"} - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_revoc_reg_request") - @mock.patch("indy.ledger.parse_get_revoc_reg_response") - async def test_get_revoc_reg_entry_x( - self, - mock_indy_parse_get_rr_resp, - mock_indy_build_get_rr_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_indy_parse_get_rr_resp.side_effect = IndyError( - error_code=ErrorCode.PoolLedgerTimeout, - error_details={"message": "bye"}, - ) - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - with self.assertRaises(LedgerError): - async with ledger: - await ledger.get_revoc_reg_entry("rr-id", 1234567890) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_get_revoc_reg_delta_request") - @mock.patch("indy.ledger.parse_get_revoc_reg_delta_response") - async def test_get_revoc_reg_delta( - self, - mock_indy_parse_get_rrd_resp, - mock_indy_build_get_rrd_req, - mock_submit, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_indy_parse_get_rrd_resp.return_value = ( - "rr-id", - '{"hello": "world"}', - 1234567890, - ) - - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - (result, _) = await ledger.get_revoc_reg_delta("rr-id") - assert result == {"hello": "world"} - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_revoc_reg_def_request") - async def test_send_revoc_reg_def_public_did( - self, mock_indy_build_rrdef_req, mock_submit, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_indy_build_rrdef_req.return_value = '{"hello": "world"}' - - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - IndySdkWallet, "get_local_did" - ) as mock_wallet_get_local_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - await ledger.send_revoc_reg_def({"rr": "def"}, issuer_did=None) - mock_wallet_get_public_did.assert_called_once() - assert not mock_wallet_get_local_did.called - mock_submit.assert_called_once_with( - mock_indy_build_rrdef_req.return_value, - True, - sign_did=self.test_did_info, - write_ledger=True, - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_revoc_reg_def_request") - async def test_send_revoc_reg_def_local_did( - self, mock_indy_build_rrdef_req, mock_submit, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_indy_build_rrdef_req.return_value = '{"hello": "world"}' - - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_local_did" - ) as mock_wallet_get_local_did: - mock_wallet_get_local_did.return_value = self.test_did_info - async with ledger: - await ledger.send_revoc_reg_def( - {"rr": "def"}, - issuer_did=self.test_did, - ) - mock_wallet_get_local_did.assert_called_once_with(self.test_did) - mock_submit.assert_called_once_with( - mock_indy_build_rrdef_req.return_value, - True, - sign_did=self.test_did_info, - write_ledger=True, - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_revoc_reg_def_request") - async def test_send_revoc_reg_def_x_no_did( - self, mock_indy_build_rrdef_req, mock_submit, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_indy_build_rrdef_req.return_value = '{"hello": "world"}' - - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_local_did" - ) as mock_wallet_get_local_did: - mock_wallet_get_local_did.return_value = None - async with ledger: - with self.assertRaises(LedgerTransactionError) as context: - await ledger.send_revoc_reg_def( - {"rr": "def"}, - issuer_did=self.test_did, - ) - assert "No issuer DID found for revocation registry definition" in str( - context.exception - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_revoc_reg_entry_request") - async def test_send_revoc_reg_entry_public_did( - self, mock_indy_build_rre_req, mock_submit, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_indy_build_rre_req.return_value = '{"hello": "world"}' - - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did, mock.patch.object( - IndySdkWallet, "get_local_did" - ) as mock_wallet_get_local_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - await ledger.send_revoc_reg_entry( - "rr-id", "CL_ACCUM", {"rev-reg": "entry"}, issuer_did=None - ) - mock_wallet_get_public_did.assert_called_once() - assert not mock_wallet_get_local_did.called - mock_submit.assert_called_once_with( - mock_indy_build_rre_req.return_value, - True, - sign_did=self.test_did_info, - write_ledger=True, - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_revoc_reg_entry_request") - async def test_send_revoc_reg_entry_local_did( - self, mock_indy_build_rre_req, mock_submit, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_indy_build_rre_req.return_value = '{"hello": "world"}' - - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_local_did" - ) as mock_wallet_get_local_did: - mock_wallet_get_local_did.return_value = self.test_did_info - async with ledger: - result = await ledger.send_revoc_reg_entry( - "rr-id", - "CL_ACCUM", - {"rev-reg": "entry"}, - issuer_did=self.test_did, - ) - mock_wallet_get_local_did.assert_called_once_with(self.test_did) - mock_submit.assert_called_once_with( - mock_indy_build_rre_req.return_value, - True, - sign_did=self.test_did_info, - write_ledger=True, - ) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - @mock.patch("indy.ledger.build_revoc_reg_entry_request") - async def test_send_revoc_reg_entry_x_no_did( - self, mock_indy_build_rre_req, mock_submit, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - mock_indy_build_rre_req.return_value = '{"hello": "world"}' - - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, read_only=True), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_local_did" - ) as mock_wallet_get_local_did: - mock_wallet_get_local_did.return_value = None - async with ledger: - with self.assertRaises(LedgerTransactionError) as context: - await ledger.send_revoc_reg_entry( - "rr-id", - "CL_ACCUM", - {"rev-reg": "entry"}, - issuer_did=self.test_did, - ) - assert "No issuer DID found for revocation registry entry" in str( - context.exception - ) - - @mock.patch("indy.pool.open_pool_ledger") - @mock.patch("indy.pool.close_pool_ledger") - async def test_taa_digest_bad_value( - self, - mock_close_pool, - mock_open_ledger, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - with self.assertRaises(ValueError): - await ledger.taa_digest(None, None) - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("indy.ledger.build_get_acceptance_mechanisms_request") - @mock.patch("indy.ledger.build_get_txn_author_agreement_request") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger._submit") - async def test_get_txn_author_agreement( - self, - mock_submit, - mock_build_get_taa_req, - mock_build_get_acc_mech_req, - mock_close, - mock_open, - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - txn_result_data = {"text": "text", "version": "1.0"} - mock_submit.side_effect = [ - json.dumps({"result": {"data": txn_result_data}}) for i in range(2) - ] - ledger = IndySdkLedger(IndySdkLedgerPool("name", checked=True), self.profile) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - response = await ledger.get_txn_author_agreement(reload=True) - - mock_build_get_acc_mech_req.assert_called_once_with( - self.test_did, None, None - ) - mock_build_get_taa_req.assert_called_once_with( - self.test_did, - None, - ) - mock_submit.assert_has_calls( - [ - mock.call( - mock_build_get_acc_mech_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ), - mock.call( - mock_build_get_taa_req.return_value, - sign_did=mock_wallet_get_public_did.return_value, - ), - ] - ) - assert response == { - "aml_record": txn_result_data, - "taa_record": { - **txn_result_data, - "digest": ledger.taa_digest( - txn_result_data["version"], txn_result_data["text"] - ), - }, - "taa_required": True, - } - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.add_record") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.find_all_records") - async def test_accept_and_get_latest_txn_author_agreement( - self, mock_find_all_records, mock_add_record, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, cache=InMemoryCache()), self.profile - ) - - accept_time = ledger.taa_rough_timestamp() - taa_record = { - "text": "text", - "version": "1.0", - "digest": "abcd1234", - } - acceptance = { - "text": taa_record["text"], - "version": taa_record["version"], - "digest": taa_record["digest"], - "mechanism": "dummy", - "time": accept_time, - } - - mock_find_all_records.return_value = [ - StorageRecord( - TAA_ACCEPTED_RECORD_TYPE, - json.dumps(acceptance), - {"pool_name": ledger.pool_name}, - ) - ] - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - await ledger.accept_txn_author_agreement( - taa_record=taa_record, mechanism="dummy", accept_time=None - ) - - await ledger.pool.cache.clear( - f"{TAA_ACCEPTED_RECORD_TYPE}::{ledger.pool_name}" - ) - for i in range(2): # populate, then get from, cache - response = await ledger.get_latest_txn_author_acceptance() - assert response == acceptance - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.storage.indy.IndySdkStorage.find_all_records") - async def test_get_latest_txn_author_agreement_none( - self, mock_find_all_records, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, cache=InMemoryCache()), self.profile - ) - - mock_find_all_records.return_value = [] - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - await ledger.pool.cache.clear( - f"{TAA_ACCEPTED_RECORD_TYPE}::{ledger.pool_name}" - ) - response = await ledger.get_latest_txn_author_acceptance() - assert response == {} - - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_open") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedgerPool.context_close") - @mock.patch("aries_cloudagent.ledger.indy.IndySdkLedger.get_schema") - async def test_credential_definition_id2schema_id( - self, mock_get_schema, mock_close, mock_open - ): - mock_wallet = mock.MagicMock() - self.session.context.injector.bind_provider(BaseWallet, mock_wallet) - S_ID = f"{self.test_did}:2:favourite_drink:1.0" - SEQ_NO = "9999" - mock_get_schema.return_value = {"id": S_ID} - - ledger = IndySdkLedger( - IndySdkLedgerPool("name", checked=True, cache=InMemoryCache()), self.profile - ) - with mock.patch.object( - IndySdkWallet, "get_public_did" - ) as mock_wallet_get_public_did: - mock_wallet_get_public_did.return_value = self.test_did_info - async with ledger: - s_id_short = await ledger.credential_definition_id2schema_id( - f"{self.test_did}:3:CL:{SEQ_NO}:tag" - ) - - mock_get_schema.assert_called_once_with(SEQ_NO) - - assert s_id_short == S_ID - s_id_long = await ledger.credential_definition_id2schema_id( - f"{self.test_did}:3:CL:{s_id_short}:tag" - ) - assert s_id_long == s_id_short - - def test_error_handler(self): - try: # with self.assertRaises() makes a copy of exception, loses traceback! - with IndyErrorHandler("message", LedgerTransactionError): - try: - 1 / 0 - except ZeroDivisionError as zx: - ix = IndyError(error_code=1, error_details={"message": "bye"}) - ix.__traceback__ = zx.__traceback__ - raise ix - except LedgerTransactionError as err: - assert type(err) == LedgerTransactionError - assert type(err.__cause__) == IndyError - assert err.__traceback__ - assert "bye" in err.message diff --git a/aries_cloudagent/ledger/tests/test_routes.py b/aries_cloudagent/ledger/tests/test_routes.py index 4347823376..69992e7d2a 100644 --- a/aries_cloudagent/ledger/tests/test_routes.py +++ b/aries_cloudagent/ledger/tests/test_routes.py @@ -1,30 +1,33 @@ from typing import Tuple - from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock +from ...connections.models.conn_record import ConnRecord from ...core.in_memory import InMemoryProfile from ...ledger.base import BaseLedger from ...ledger.endpoint_type import EndpointType -from ...ledger.multiple_ledger.ledger_requests_executor import ( - IndyLedgerRequestsExecutor, -) from ...ledger.multiple_ledger.base_manager import ( BaseMultipleLedgerManager, ) +from ...ledger.multiple_ledger.ledger_requests_executor import ( + IndyLedgerRequestsExecutor, +) from ...multitenant.base import BaseMultitenantManager from ...multitenant.manager import MultitenantManager - from .. import routes as test_module -from ..indy import Role -from ...connections.models.conn_record import ConnRecord +from ..indy_vdr import Role class TestLedgerRoutes(IsolatedAsyncioTestCase): def setUp(self): self.ledger = mock.create_autospec(BaseLedger) self.ledger.pool_name = "pool.0" - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.context = self.profile.context setattr(self.context, "profile", self.profile) self.profile.context.injector.bind_instance(BaseLedger, self.ledger) @@ -37,6 +40,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_did = "did" diff --git a/aries_cloudagent/messaging/credential_definitions/routes.py b/aries_cloudagent/messaging/credential_definitions/routes.py index f70bac14e5..6a84a89ca5 100644 --- a/aries_cloudagent/messaging/credential_definitions/routes.py +++ b/aries_cloudagent/messaging/credential_definitions/routes.py @@ -16,6 +16,7 @@ ) from marshmallow import fields +from ...admin.decorators.auth import tenant_authentication from ...admin.request_context import AdminRequestContext from ...connections.models.conn_record import ConnRecord from ...core.event_bus import Event, EventBus @@ -183,6 +184,7 @@ class CredDefConnIdMatchInfoSchema(OpenAPISchema): @querystring_schema(CreateCredDefTxnForEndorserOptionSchema()) @querystring_schema(CredDefConnIdMatchInfoSchema()) @response_schema(TxnOrCredentialDefinitionSendResultSchema(), 200, description="") +@tenant_authentication async def credential_definitions_send_credential_definition(request: web.BaseRequest): """Request handler for sending a credential definition to the ledger. @@ -378,6 +380,7 @@ async def credential_definitions_send_credential_definition(request: web.BaseReq ) @querystring_schema(CredDefQueryStringSchema()) @response_schema(CredentialDefinitionsCreatedResultSchema(), 200, description="") +@tenant_authentication async def credential_definitions_created(request: web.BaseRequest): """Request handler for retrieving credential definitions that current agent created. @@ -412,6 +415,7 @@ async def credential_definitions_created(request: web.BaseRequest): ) @match_info_schema(CredDefIdMatchInfoSchema()) @response_schema(CredentialDefinitionGetResultSchema(), 200, description="") +@tenant_authentication async def credential_definitions_get_credential_definition(request: web.BaseRequest): """Request handler for getting a credential definition from the ledger. @@ -462,6 +466,7 @@ async def credential_definitions_get_credential_definition(request: web.BaseRequ ) @match_info_schema(CredDefIdMatchInfoSchema()) @response_schema(CredentialDefinitionGetResultSchema(), 200, description="") +@tenant_authentication async def credential_definitions_fix_cred_def_wallet_record(request: web.BaseRequest): """Request handler for fixing a credential definition wallet non-secret record. diff --git a/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py b/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py index b88e7bf2fa..90043bdfef 100644 --- a/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py +++ b/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py @@ -23,7 +23,11 @@ class TestCredentialDefinitionRoutes(IsolatedAsyncioTestCase): def setUp(self): self.session_inject = {} - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.profile_injector = self.profile.context.injector self.ledger = mock.create_autospec(BaseLedger) @@ -61,6 +65,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_send_credential_definition(self): @@ -391,7 +396,7 @@ async def test_get_credential_definition_no_ledger(self): async def test_credential_definition_endpoints_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarAnoncredsProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -404,6 +409,7 @@ async def test_credential_definition_endpoints_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( return_value={ diff --git a/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py b/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py index c0f1f26169..9d6ef93765 100644 --- a/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py +++ b/aries_cloudagent/messaging/decorators/tests/test_attach_decorator.py @@ -6,10 +6,11 @@ import pytest -from ....indy.sdk.wallet_setup import IndyWalletConfig +from aries_cloudagent.wallet.base import BaseWallet + +from ....core.in_memory import InMemoryProfile from ....messaging.models.base import BaseModelError -from ....wallet.did_method import SOV -from ....wallet.indy import IndySdkWallet +from ....wallet.did_method import SOV, DIDMethods from ....wallet.key_type import ED25519 from ....wallet.util import b64_to_bytes, bytes_to_b64 from ..attach_decorator import ( @@ -78,16 +79,10 @@ def seed(): @pytest.fixture() async def wallet(): - wallet = await IndyWalletConfig( - { - "auto_remove": True, - "key": await IndySdkWallet.generate_wallet_key(), - "key_derivation_method": "RAW", - "name": "test-wallet-sign-verify-attach-deco", - } - ).create_wallet() - yield IndySdkWallet(wallet) - await wallet.close() + profile = InMemoryProfile.test_profile(bind={DIDMethods: DIDMethods()}) + async with profile.session() as session: + wallet = session.inject(BaseWallet) + yield wallet class TestAttachDecorator(TestCase): @@ -422,7 +417,6 @@ def test_data_json_external_mutation(self): assert "key_one" not in data -@pytest.mark.indy class TestAttachDecoratorSignature: @pytest.mark.asyncio async def test_did_raw_key(self, wallet, seed): @@ -459,7 +453,6 @@ async def test_indy_sign(self, wallet, seed): assert not deco_indy.data.jws.signatures assert deco_indy.data.header_map(0) is not None assert deco_indy.data.header_map() is not None - assert "kid" in deco_indy.data.header_map() assert "jwk" in deco_indy.data.header_map() assert "kid" in deco_indy.data.header_map()["jwk"] assert deco_indy.data.header_map()["kid"] == did_key(did_info[0].verkey) @@ -491,7 +484,6 @@ async def test_indy_sign(self, wallet, seed): assert not deco_indy.data.jws.signatures assert deco_indy.data.header_map(0) is not None assert deco_indy.data.header_map() is not None - assert "kid" in deco_indy.data.header_map() assert "jwk" in deco_indy.data.header_map() assert "kid" in deco_indy.data.header_map()["jwk"] assert deco_indy.data.header_map()["kid"] == did_key(did_info[0].verkey) @@ -515,11 +507,8 @@ async def test_indy_sign(self, wallet, seed): assert deco_indy.data.jws.signatures for i in range(len(did_info)): assert deco_indy.data.header_map(i) is not None - assert "kid" in deco_indy.data.header_map(i, jose=False) - assert "kid" in deco_indy.data.header_map(i, jose=True) assert "jwk" in deco_indy.data.header_map(i) assert "kid" in deco_indy.data.header_map(i)["jwk"] - assert deco_indy.data.header_map(i)["kid"] == did_key(did_info[i].verkey) assert deco_indy.data.header_map(i)["jwk"]["kid"] == did_key( did_info[i].verkey ) diff --git a/aries_cloudagent/messaging/jsonld/routes.py b/aries_cloudagent/messaging/jsonld/routes.py index 12c8105571..72cfd62fcb 100644 --- a/aries_cloudagent/messaging/jsonld/routes.py +++ b/aries_cloudagent/messaging/jsonld/routes.py @@ -2,10 +2,10 @@ from aiohttp import web from aiohttp_apispec import docs, request_schema, response_schema -from pydid.verification_method import Ed25519VerificationKey2018 - from marshmallow import INCLUDE, Schema, fields +from pydid.verification_method import Ed25519VerificationKey2018 +from ...admin.decorators.auth import tenant_authentication from ...admin.request_context import AdminRequestContext from ...config.base import InjectionError from ...resolver.base import ResolverError @@ -66,6 +66,7 @@ class SignResponseSchema(OpenAPISchema): ) @request_schema(SignRequestSchema()) @response_schema(SignResponseSchema(), 200, description="") +@tenant_authentication async def sign(request: web.BaseRequest): """Request handler for signing a jsonld doc. @@ -130,6 +131,7 @@ class VerifyResponseSchema(OpenAPISchema): ) @request_schema(VerifyRequestSchema()) @response_schema(VerifyResponseSchema(), 200, description="") +@tenant_authentication async def verify(request: web.BaseRequest): """Request handler for signing a jsonld doc. diff --git a/aries_cloudagent/messaging/jsonld/tests/test_routes.py b/aries_cloudagent/messaging/jsonld/tests/test_routes.py index b36a21b162..f48afb3dcb 100644 --- a/aries_cloudagent/messaging/jsonld/tests/test_routes.py +++ b/aries_cloudagent/messaging/jsonld/tests/test_routes.py @@ -1,15 +1,16 @@ -from copy import deepcopy import json +from copy import deepcopy +from unittest import IsolatedAsyncioTestCase +import pytest from aiohttp import web -from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock from pyld import jsonld -import pytest -from .. import routes as test_module +from aries_cloudagent.tests import mock + from ....admin.request_context import AdminRequestContext from ....config.base import InjectionError +from ....core.in_memory import InMemoryProfile from ....resolver.base import DIDMethodNotSupported, DIDNotFound, ResolverError from ....resolver.did_resolver import DIDResolver from ....vc.ld_proofs.document_loader import DocumentLoader @@ -17,6 +18,7 @@ from ....wallet.did_method import SOV, DIDMethods from ....wallet.error import WalletError from ....wallet.key_type import ED25519 +from .. import routes as test_module from ..error import ( BadJWSHeaderError, DroppedAttributeError, @@ -84,7 +86,12 @@ def mock_verify_credential(): @pytest.fixture def mock_sign_request(mock_sign_credential): - context = AdminRequestContext.test_context() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + context = AdminRequestContext.test_context({}, profile) outbound_message_router = mock.CoroutineMock() request_dict = { "context": context, @@ -110,6 +117,7 @@ def mock_sign_request(mock_sign_credential): }, ), __getitem__=lambda _, k: request_dict[k], + headers={"x-api-key": "secret-key"}, ) yield request @@ -137,7 +145,14 @@ def request_body(): @pytest.fixture def mock_verify_request(mock_verify_credential, mock_resolver, request_body): def _mock_verify_request(request_body=request_body): - context = AdminRequestContext.test_context({DIDResolver: mock_resolver}) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + context = AdminRequestContext.test_context( + {DIDResolver: mock_resolver}, profile + ) outbound_message_router = mock.CoroutineMock() request_dict = { "context": context, @@ -148,6 +163,7 @@ def _mock_verify_request(request_body=request_body): query={}, json=mock.CoroutineMock(return_value=request_body), __getitem__=lambda _, k: request_dict[k], + headers={"x-api-key": "secret-key"}, ) return request @@ -270,7 +286,12 @@ def test_post_process_routes(): class TestJSONLDRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): - self.context = AdminRequestContext.test_context() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context({}, self.profile) self.context.profile.context.injector.bind_instance( DocumentLoader, custom_document_loader ) @@ -287,6 +308,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_verify_credential(self): diff --git a/aries_cloudagent/messaging/schemas/routes.py b/aries_cloudagent/messaging/schemas/routes.py index 5b8fa38147..6d4e50f9c2 100644 --- a/aries_cloudagent/messaging/schemas/routes.py +++ b/aries_cloudagent/messaging/schemas/routes.py @@ -15,6 +15,7 @@ from marshmallow import fields from marshmallow.validate import Regexp +from ...admin.decorators.auth import tenant_authentication from ...admin.request_context import AdminRequestContext from ...connections.models.conn_record import ConnRecord from ...core.event_bus import Event, EventBus @@ -166,6 +167,7 @@ class SchemaConnIdMatchInfoSchema(OpenAPISchema): @querystring_schema(CreateSchemaTxnForEndorserOptionSchema()) @querystring_schema(SchemaConnIdMatchInfoSchema()) @response_schema(TxnOrSchemaSendResultSchema(), 200, description="") +@tenant_authentication async def schemas_send_schema(request: web.BaseRequest): """Request handler for creating a schema. @@ -340,6 +342,7 @@ async def schemas_send_schema(request: web.BaseRequest): ) @querystring_schema(SchemaQueryStringSchema()) @response_schema(SchemasCreatedResultSchema(), 200, description="") +@tenant_authentication async def schemas_created(request: web.BaseRequest): """Request handler for retrieving schemas that current agent created. @@ -369,6 +372,7 @@ async def schemas_created(request: web.BaseRequest): @docs(tags=["schema"], summary="Gets a schema from the ledger") @match_info_schema(SchemaIdMatchInfoSchema()) @response_schema(SchemaGetResultSchema(), 200, description="") +@tenant_authentication async def schemas_get_schema(request: web.BaseRequest): """Request handler for sending a credential offer. @@ -419,6 +423,7 @@ async def schemas_get_schema(request: web.BaseRequest): @docs(tags=["schema"], summary="Writes a schema non-secret record to the wallet") @match_info_schema(SchemaIdMatchInfoSchema()) @response_schema(SchemaGetResultSchema(), 200, description="") +@tenant_authentication async def schemas_fix_schema_wallet_record(request: web.BaseRequest): """Request handler for fixing a schema's wallet non-secrets records. diff --git a/aries_cloudagent/messaging/schemas/tests/test_routes.py b/aries_cloudagent/messaging/schemas/tests/test_routes.py index 6e42b4c190..411a951d9e 100644 --- a/aries_cloudagent/messaging/schemas/tests/test_routes.py +++ b/aries_cloudagent/messaging/schemas/tests/test_routes.py @@ -22,7 +22,11 @@ class TestSchemaRoutes(IsolatedAsyncioTestCase): def setUp(self): self.session_inject = {} - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.profile_injector = self.profile.context.injector self.ledger = mock.create_autospec(BaseLedger) self.ledger.__aenter__ = mock.CoroutineMock(return_value=self.ledger) @@ -54,6 +58,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_send_schema(self): @@ -402,7 +407,7 @@ async def test_get_schema_x_ledger(self): async def test_schema_endpoints_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet-type": "askar"}, + settings={"wallet-type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarAnoncredsProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -415,6 +420,7 @@ async def test_schema_endpoints_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( diff --git a/aries_cloudagent/multitenant/admin/routes.py b/aries_cloudagent/multitenant/admin/routes.py index 4948a7e518..1e84020d98 100644 --- a/aries_cloudagent/multitenant/admin/routes.py +++ b/aries_cloudagent/multitenant/admin/routes.py @@ -10,6 +10,7 @@ ) from marshmallow import ValidationError, fields, validate, validates_schema +from ...admin.decorators.auth import admin_authentication from ...admin.request_context import AdminRequestContext from ...core.error import BaseError from ...core.profile import ProfileManagerProvider @@ -363,6 +364,7 @@ class WalletListQueryStringSchema(OpenAPISchema): @docs(tags=["multitenancy"], summary="Query subwallets") @querystring_schema(WalletListQueryStringSchema()) @response_schema(WalletListSchema(), 200, description="") +@admin_authentication async def wallets_list(request: web.BaseRequest): """Request handler for listing all internal subwallets. @@ -392,6 +394,7 @@ async def wallets_list(request: web.BaseRequest): @docs(tags=["multitenancy"], summary="Get a single subwallet") @match_info_schema(WalletIdMatchInfoSchema()) @response_schema(WalletRecordSchema(), 200, description="") +@admin_authentication async def wallet_get(request: web.BaseRequest): """Request handler for getting a single subwallet. @@ -422,6 +425,7 @@ async def wallet_get(request: web.BaseRequest): @docs(tags=["multitenancy"], summary="Create a subwallet") @request_schema(CreateWalletRequestSchema) @response_schema(CreateWalletResponseSchema(), 200, description="") +@admin_authentication async def wallet_create(request: web.BaseRequest): """Request handler for adding a new subwallet for handling by the agent. @@ -495,6 +499,7 @@ async def wallet_create(request: web.BaseRequest): @match_info_schema(WalletIdMatchInfoSchema()) @request_schema(UpdateWalletRequestSchema) @response_schema(WalletRecordSchema(), 200, description="") +@admin_authentication async def wallet_update(request: web.BaseRequest): """Request handler for updating a existing subwallet for handling by the agent. @@ -559,6 +564,7 @@ async def wallet_update(request: web.BaseRequest): @docs(tags=["multitenancy"], summary="Get auth token for a subwallet") @request_schema(CreateWalletTokenRequestSchema) @response_schema(CreateWalletTokenResponseSchema(), 200, description="") +@admin_authentication async def wallet_create_token(request: web.BaseRequest): """Request handler for creating an authorization token for a specific subwallet. @@ -603,6 +609,7 @@ async def wallet_create_token(request: web.BaseRequest): @match_info_schema(WalletIdMatchInfoSchema()) @request_schema(RemoveWalletRequestSchema) @response_schema(MultitenantModuleResponseSchema(), 200, description="") +@admin_authentication async def wallet_remove(request: web.BaseRequest): """Request handler to remove a subwallet from agent and storage. diff --git a/aries_cloudagent/multitenant/admin/tests/test_routes.py b/aries_cloudagent/multitenant/admin/tests/test_routes.py index 9f4a6d32ba..7576591968 100644 --- a/aries_cloudagent/multitenant/admin/tests/test_routes.py +++ b/aries_cloudagent/multitenant/admin/tests/test_routes.py @@ -24,7 +24,7 @@ async def asyncSetUp(self): return_value=self.mock_multitenant_mgr ) self.profile = InMemoryProfile.test_profile( - settings={"wallet.type": "askar"}, + settings={"wallet.type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -45,13 +45,18 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_format_wallet_record_removes_wallet_key(self): wallet_record = WalletRecord( wallet_id="test", key_management_mode=WalletRecord.MODE_MANAGED, - settings={"wallet.name": "wallet_name", "wallet.key": "wallet_key"}, + settings={ + "wallet.name": "wallet_name", + "wallet.key": "wallet_key", + "admin.admin_api_key": "secret-key", + }, ) formatted = test_module.format_wallet_record(wallet_record) diff --git a/aries_cloudagent/multitenant/manager.py b/aries_cloudagent/multitenant/manager.py index 550389f0db..4f1cf89134 100644 --- a/aries_cloudagent/multitenant/manager.py +++ b/aries_cloudagent/multitenant/manager.py @@ -3,6 +3,7 @@ import logging from typing import Iterable, Optional +from ..askar.profile_anon import AskarAnoncredsProfile from ..config.injection_context import InjectionContext from ..config.wallet import wallet_config from ..core.profile import Profile @@ -84,6 +85,13 @@ async def get_wallet_profile( profile, _ = await wallet_config(context, provision=provision) self._profiles.put(wallet_id, profile) + # return anoncreds profile if explicitly set as wallet type + if profile.context.settings.get("wallet.type") == "askar-anoncreds": + return AskarAnoncredsProfile( + profile.opened, + profile.context, + ) + return profile async def update_wallet(self, wallet_id: str, new_settings: dict) -> WalletRecord: diff --git a/aries_cloudagent/protocols/actionmenu/v1_0/routes.py b/aries_cloudagent/protocols/actionmenu/v1_0/routes.py index c9c94af9d6..802fa75ebf 100644 --- a/aries_cloudagent/protocols/actionmenu/v1_0/routes.py +++ b/aries_cloudagent/protocols/actionmenu/v1_0/routes.py @@ -4,9 +4,9 @@ from aiohttp import web from aiohttp_apispec import docs, match_info_schema, request_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....messaging.models.base import BaseModelError @@ -95,6 +95,7 @@ class ActionMenuFetchResultSchema(OpenAPISchema): ) @match_info_schema(MenuConnIdMatchInfoSchema()) @response_schema(ActionMenuModulesResultSchema(), 200, description="") +@tenant_authentication async def actionmenu_close(request: web.BaseRequest): """Request handler for closing the menu associated with a connection. @@ -122,6 +123,7 @@ async def actionmenu_close(request: web.BaseRequest): @docs(tags=["action-menu"], summary="Fetch the active menu") @match_info_schema(MenuConnIdMatchInfoSchema()) @response_schema(ActionMenuFetchResultSchema(), 200, description="") +@tenant_authentication async def actionmenu_fetch(request: web.BaseRequest): """Request handler for fetching the previously-received menu for a connection. @@ -141,6 +143,7 @@ async def actionmenu_fetch(request: web.BaseRequest): @match_info_schema(MenuConnIdMatchInfoSchema()) @request_schema(PerformRequestSchema()) @response_schema(ActionMenuModulesResultSchema(), 200, description="") +@tenant_authentication async def actionmenu_perform(request: web.BaseRequest): """Request handler for performing a menu action. @@ -170,6 +173,7 @@ async def actionmenu_perform(request: web.BaseRequest): @docs(tags=["action-menu"], summary="Request the active menu") @match_info_schema(MenuConnIdMatchInfoSchema()) @response_schema(ActionMenuModulesResultSchema(), 200, description="") +@tenant_authentication async def actionmenu_request(request: web.BaseRequest): """Request handler for requesting a menu from the connection target. @@ -200,6 +204,7 @@ async def actionmenu_request(request: web.BaseRequest): @match_info_schema(MenuConnIdMatchInfoSchema()) @request_schema(SendMenuSchema()) @response_schema(ActionMenuModulesResultSchema(), 200, description="") +@tenant_authentication async def actionmenu_send(request: web.BaseRequest): """Request handler for requesting a menu from the connection target. diff --git a/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py index 0d157842e8..31a1e00d85 100644 --- a/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/actionmenu/v1_0/tests/test_routes.py @@ -1,16 +1,22 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageNotFoundError - from .. import routes as test_module class TestActionMenuRoutes(IsolatedAsyncioTestCase): def setUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -20,6 +26,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_actionmenu_close(self): diff --git a/aries_cloudagent/protocols/basicmessage/v1_0/routes.py b/aries_cloudagent/protocols/basicmessage/v1_0/routes.py index 7fffa930e1..015318eb5f 100644 --- a/aries_cloudagent/protocols/basicmessage/v1_0/routes.py +++ b/aries_cloudagent/protocols/basicmessage/v1_0/routes.py @@ -2,9 +2,9 @@ from aiohttp import web from aiohttp_apispec import docs, match_info_schema, request_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....messaging.models.openapi import OpenAPISchema @@ -39,6 +39,7 @@ class BasicConnIdMatchInfoSchema(OpenAPISchema): @match_info_schema(BasicConnIdMatchInfoSchema()) @request_schema(SendMessageSchema()) @response_schema(BasicMessageModuleResponseSchema(), 200, description="") +@tenant_authentication async def connections_send_message(request: web.BaseRequest): """Request handler for sending a basic message to a connection. diff --git a/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py index de3373f053..7d6c5b069c 100644 --- a/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/basicmessage/v1_0/tests/test_routes.py @@ -1,16 +1,22 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageNotFoundError - from .. import routes as test_module class TestBasicMessageRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -20,6 +26,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_conn_id = "connection-id" diff --git a/aries_cloudagent/protocols/connections/v1_0/routes.py b/aries_cloudagent/protocols/connections/v1_0/routes.py index 5d3a0c6e66..e067b547af 100644 --- a/aries_cloudagent/protocols/connections/v1_0/routes.py +++ b/aries_cloudagent/protocols/connections/v1_0/routes.py @@ -11,9 +11,9 @@ request_schema, response_schema, ) - from marshmallow import fields, validate, validates_schema +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....cache.base import BaseCache from ....connections.models.conn_record import ConnRecord, ConnRecordSchema @@ -22,13 +22,13 @@ from ....messaging.valid import ( ENDPOINT_EXAMPLE, ENDPOINT_VALIDATE, + GENERIC_DID_VALIDATE, INDY_DID_EXAMPLE, INDY_DID_VALIDATE, INDY_RAW_PUBLIC_KEY_EXAMPLE, INDY_RAW_PUBLIC_KEY_VALIDATE, UUID4_EXAMPLE, UUID4_VALIDATE, - GENERIC_DID_VALIDATE, ) from ....storage.error import StorageError, StorageNotFoundError from ....wallet.error import WalletError @@ -430,6 +430,7 @@ def connection_sort_key(conn): ) @querystring_schema(ConnectionsListQueryStringSchema()) @response_schema(ConnectionListSchema(), 200, description="") +@tenant_authentication async def connections_list(request: web.BaseRequest): """Request handler for searching connection records. @@ -484,6 +485,7 @@ async def connections_list(request: web.BaseRequest): @docs(tags=["connection"], summary="Fetch a single connection record") @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def connections_retrieve(request: web.BaseRequest): """Request handler for fetching a single connection record. @@ -513,6 +515,7 @@ async def connections_retrieve(request: web.BaseRequest): @docs(tags=["connection"], summary="Fetch connection remote endpoint") @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @response_schema(EndpointsResultSchema(), 200, description="") +@tenant_authentication async def connections_endpoints(request: web.BaseRequest): """Request handler for fetching connection endpoints. @@ -542,6 +545,7 @@ async def connections_endpoints(request: web.BaseRequest): @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @querystring_schema(ConnectionMetadataQuerySchema()) @response_schema(ConnectionMetadataSchema(), 200, description="") +@tenant_authentication async def connections_metadata(request: web.BaseRequest): """Handle fetching metadata associated with a single connection record.""" context: AdminRequestContext = request["context"] @@ -568,6 +572,7 @@ async def connections_metadata(request: web.BaseRequest): @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @request_schema(ConnectionMetadataSetRequestSchema()) @response_schema(ConnectionMetadataSchema(), 200, description="") +@tenant_authentication async def connections_metadata_set(request: web.BaseRequest): """Handle fetching metadata associated with a single connection record.""" context: AdminRequestContext = request["context"] @@ -597,6 +602,7 @@ async def connections_metadata_set(request: web.BaseRequest): @querystring_schema(CreateInvitationQueryStringSchema()) @request_schema(CreateInvitationRequestSchema()) @response_schema(InvitationResultSchema(), 200, description="") +@tenant_authentication async def connections_create_invitation(request: web.BaseRequest): """Request handler for creating a new connection invitation. @@ -671,6 +677,7 @@ async def connections_create_invitation(request: web.BaseRequest): @querystring_schema(ReceiveInvitationQueryStringSchema()) @request_schema(ReceiveInvitationRequestSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def connections_receive_invitation(request: web.BaseRequest): """Request handler for receiving a new connection invitation. @@ -713,6 +720,7 @@ async def connections_receive_invitation(request: web.BaseRequest): @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @querystring_schema(AcceptInvitationQueryStringSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def connections_accept_invitation(request: web.BaseRequest): """Request handler for accepting a stored connection invitation. @@ -764,6 +772,7 @@ async def connections_accept_invitation(request: web.BaseRequest): @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @querystring_schema(AcceptRequestQueryStringSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def connections_accept_request(request: web.BaseRequest): """Request handler for accepting a stored connection request. @@ -798,6 +807,7 @@ async def connections_accept_request(request: web.BaseRequest): @docs(tags=["connection"], summary="Remove an existing connection record") @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @response_schema(ConnectionModuleResponseSchema, 200, description="") +@tenant_authentication async def connections_remove(request: web.BaseRequest): """Request handler for removing a connection record. @@ -826,6 +836,7 @@ async def connections_remove(request: web.BaseRequest): @docs(tags=["connection"], summary="Create a new static connection") @request_schema(ConnectionStaticRequestSchema()) @response_schema(ConnectionStaticResultSchema(), 200, description="") +@tenant_authentication async def connections_create_static(request: web.BaseRequest): """Request handler for creating a new static connection. diff --git a/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py index d880f17e59..d561e8f0a0 100644 --- a/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/connections/v1_0/tests/test_routes.py @@ -1,22 +1,27 @@ import json - -from unittest.mock import ANY from unittest import IsolatedAsyncioTestCase +from unittest.mock import ANY + from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext from .....cache.base import BaseCache from .....cache.in_memory import InMemoryCache from .....connections.models.conn_record import ConnRecord +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageNotFoundError - from .. import routes as test_module class TestConnectionRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -26,6 +31,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_connections_list(self): diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py index dd4e100081..8d8c97231e 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/routes.py @@ -8,9 +8,9 @@ request_schema, response_schema, ) - from marshmallow import fields, validate +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....messaging.models.base import BaseModelError @@ -169,6 +169,7 @@ def mediation_sort_key(mediation: dict): ) @querystring_schema(MediationListQueryStringSchema()) @response_schema(MediationListSchema(), 200) +@tenant_authentication async def list_mediation_requests(request: web.BaseRequest): """List mediation requests for either client or server role.""" context: AdminRequestContext = request["context"] @@ -194,6 +195,7 @@ async def list_mediation_requests(request: web.BaseRequest): @docs(tags=["mediation"], summary="Retrieve mediation request record") @match_info_schema(MediationIdMatchInfoSchema()) @response_schema(MediationRecordSchema(), 200) +@tenant_authentication async def retrieve_mediation_request(request: web.BaseRequest): """Retrieve a single mediation request.""" context: AdminRequestContext = request["context"] @@ -216,6 +218,7 @@ async def retrieve_mediation_request(request: web.BaseRequest): @docs(tags=["mediation"], summary="Delete mediation request by ID") @match_info_schema(MediationIdMatchInfoSchema()) @response_schema(MediationRecordSchema, 200) +@tenant_authentication async def delete_mediation_request(request: web.BaseRequest): """Delete a mediation request by ID.""" context: AdminRequestContext = request["context"] @@ -241,6 +244,7 @@ async def delete_mediation_request(request: web.BaseRequest): @match_info_schema(ConnectionsConnIdMatchInfoSchema()) @request_schema(MediationCreateRequestSchema()) @response_schema(MediationRecordSchema(), 201) +@tenant_authentication async def request_mediation(request: web.BaseRequest): """Request mediation from connection.""" context: AdminRequestContext = request["context"] @@ -280,6 +284,7 @@ async def request_mediation(request: web.BaseRequest): @docs(tags=["mediation"], summary="Grant received mediation") @match_info_schema(MediationIdMatchInfoSchema()) @response_schema(MediationGrantSchema(), 201) +@tenant_authentication async def mediation_request_grant(request: web.BaseRequest): """Grant a stored mediation request.""" context: AdminRequestContext = request["context"] @@ -303,6 +308,7 @@ async def mediation_request_grant(request: web.BaseRequest): @match_info_schema(MediationIdMatchInfoSchema()) @request_schema(AdminMediationDenySchema()) @response_schema(MediationDenySchema(), 201) +@tenant_authentication async def mediation_request_deny(request: web.BaseRequest): """Deny a stored mediation request.""" context: AdminRequestContext = request["context"] @@ -329,6 +335,7 @@ async def mediation_request_deny(request: web.BaseRequest): ) @querystring_schema(GetKeylistQuerySchema()) @response_schema(KeylistSchema(), 200) +@tenant_authentication async def get_keylist(request: web.BaseRequest): """Retrieve keylists by connection or role.""" context: AdminRequestContext = request["context"] @@ -358,6 +365,7 @@ async def get_keylist(request: web.BaseRequest): @querystring_schema(KeylistQueryPaginateQuerySchema()) @request_schema(KeylistQueryFilterRequestSchema()) @response_schema(KeylistQuerySchema(), 201) +@tenant_authentication async def send_keylist_query(request: web.BaseRequest): """Send keylist query to mediator.""" context: AdminRequestContext = request["context"] @@ -394,6 +402,7 @@ async def send_keylist_query(request: web.BaseRequest): @match_info_schema(MediationIdMatchInfoSchema()) @request_schema(KeylistUpdateRequestSchema()) @response_schema(KeylistUpdateSchema(), 201) +@tenant_authentication async def send_keylist_update(request: web.BaseRequest): """Send keylist update to mediator.""" context: AdminRequestContext = request["context"] @@ -439,6 +448,7 @@ async def send_keylist_update(request: web.BaseRequest): @docs(tags=["mediation"], summary="Get default mediator") @response_schema(MediationRecordSchema(), 200) +@tenant_authentication async def get_default_mediator(request: web.BaseRequest): """Get default mediator.""" context: AdminRequestContext = request["context"] @@ -455,6 +465,7 @@ async def get_default_mediator(request: web.BaseRequest): @docs(tags=["mediation"], summary="Set default mediator") @match_info_schema(MediationIdMatchInfoSchema()) @response_schema(MediationRecordSchema(), 201) +@tenant_authentication async def set_default_mediator(request: web.BaseRequest): """Set default mediator.""" context: AdminRequestContext = request["context"] @@ -471,6 +482,7 @@ async def set_default_mediator(request: web.BaseRequest): @docs(tags=["mediation"], summary="Clear default mediator") @response_schema(MediationRecordSchema(), 201) +@tenant_authentication async def clear_default_mediator(request: web.BaseRequest): """Clear set default mediator.""" context: AdminRequestContext = request["context"] @@ -489,6 +501,7 @@ async def clear_default_mediator(request: web.BaseRequest): @request_schema(MediationIdMatchInfoSchema()) # TODO Fix this response so that it adequately represents Optionals @response_schema(KeylistUpdateSchema(), 200) +@tenant_authentication async def update_keylist_for_connection(request: web.BaseRequest): """Update keylist for a connection.""" context: AdminRequestContext = request["context"] diff --git a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py index bdf9911c32..2b3ecd3c04 100644 --- a/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/coordinate_mediation/v1_0/tests/test_routes.py @@ -1,18 +1,23 @@ -from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase -from .. import routes as test_module +from aries_cloudagent.tests import mock + from .....admin.request_context import AdminRequestContext from .....core.in_memory import InMemoryProfile from .....storage.error import StorageError, StorageNotFoundError +from .....wallet.did_method import DIDMethods +from .. import routes as test_module from ..models.mediation_record import MediationRecord from ..route_manager import RouteManager -from .....wallet.did_method import DIDMethods class TestCoordinateMediationRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.profile.context.injector.bind_instance(DIDMethods, DIDMethods()) self.context = AdminRequestContext.test_context(profile=self.profile) self.outbound_message_router = mock.CoroutineMock() @@ -28,6 +33,7 @@ def setUp(self): query={}, json=mock.CoroutineMock(return_value={}), __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) serialized = { "mediation_id": "fake_id", diff --git a/aries_cloudagent/protocols/did_rotate/v1_0/messages/tests/test_rotate.py b/aries_cloudagent/protocols/did_rotate/v1_0/messages/tests/test_rotate.py index 66e1558fce..b26c65785c 100644 --- a/aries_cloudagent/protocols/did_rotate/v1_0/messages/tests/test_rotate.py +++ b/aries_cloudagent/protocols/did_rotate/v1_0/messages/tests/test_rotate.py @@ -8,7 +8,6 @@ class TestRotate(TestCase): - def test_init_type(self): """Test initializer.""" diff --git a/aries_cloudagent/protocols/did_rotate/v1_0/routes.py b/aries_cloudagent/protocols/did_rotate/v1_0/routes.py index be72612b7f..f441ded27b 100644 --- a/aries_cloudagent/protocols/did_rotate/v1_0/routes.py +++ b/aries_cloudagent/protocols/did_rotate/v1_0/routes.py @@ -6,6 +6,7 @@ from aiohttp_apispec import docs, json_schema, match_info_schema, response_schema from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....messaging.models.openapi import OpenAPISchema @@ -46,6 +47,7 @@ class DIDRotateRequestJSONSchema(OpenAPISchema): @response_schema( RotateMessageSchema(), 200, description="Rotate agent message for observer" ) +@tenant_authentication async def rotate(request: web.BaseRequest): """Request to rotate a DID.""" @@ -77,6 +79,7 @@ async def rotate(request: web.BaseRequest): @response_schema( HangupMessageSchema(), 200, description="Hangup agent message for observer" ) +@tenant_authentication async def hangup(request: web.BaseRequest): """Hangup a DID rotation.""" diff --git a/aries_cloudagent/protocols/did_rotate/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/did_rotate/v1_0/tests/test_routes.py index a4b68f08c4..f596005cf3 100644 --- a/aries_cloudagent/protocols/did_rotate/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/did_rotate/v1_0/tests/test_routes.py @@ -2,12 +2,13 @@ from unittest import IsolatedAsyncioTestCase from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....protocols.didcomm_prefix import DIDCommPrefix from .....storage.error import StorageNotFoundError from .....tests import mock -from ..messages import Hangup, Rotate from .. import message_types as test_message_types from .. import routes as test_module +from ..messages import Hangup, Rotate from ..tests import MockConnRecord, test_conn_id test_valid_rotate_request = { @@ -28,8 +29,12 @@ def generate_mock_rotate_message(): class TestDIDRotateRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -39,6 +44,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) @mock.patch.object( @@ -107,7 +113,6 @@ async def test_rotate_conn_not_found(self): "retrieve_by_id", mock.CoroutineMock(side_effect=StorageNotFoundError()), ) as mock_retrieve_by_id: - with self.assertRaises(test_module.web.HTTPNotFound): await test_module.rotate(self.request) diff --git a/aries_cloudagent/protocols/didexchange/v1_0/routes.py b/aries_cloudagent/protocols/didexchange/v1_0/routes.py index 7826aacbd4..f9c21e8266 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/routes.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/routes.py @@ -12,6 +12,7 @@ ) from marshmallow import fields, validate +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord, ConnRecordSchema from ....messaging.models.base import BaseModelError @@ -238,7 +239,8 @@ class DIDXRejectRequestSchema(OpenAPISchema): @match_info_schema(DIDXConnIdMatchInfoSchema()) @querystring_schema(DIDXAcceptInvitationQueryStringSchema()) @response_schema(ConnRecordSchema(), 200, description="") -async def didx_accept_invitation(request: web.Request): +@tenant_authentication +async def didx_accept_invitation(request: web.BaseRequest): """Request handler for accepting a stored connection invitation. Args: @@ -300,6 +302,7 @@ async def didx_accept_invitation(request: web.Request): ) @querystring_schema(DIDXCreateRequestImplicitQueryStringSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def didx_create_request_implicit(request: web.BaseRequest): """Request handler for creating and sending a request to an implicit invitation. @@ -358,6 +361,7 @@ async def didx_create_request_implicit(request: web.BaseRequest): @querystring_schema(DIDXReceiveRequestImplicitQueryStringSchema()) @request_schema(DIDXRequestSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def didx_receive_request_implicit(request: web.BaseRequest): """Request handler for receiving a request against public DID's implicit invitation. @@ -400,6 +404,7 @@ async def didx_receive_request_implicit(request: web.BaseRequest): @match_info_schema(DIDXConnIdMatchInfoSchema()) @querystring_schema(DIDXAcceptRequestQueryStringSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def didx_accept_request(request: web.BaseRequest): """Request handler for accepting a stored connection request. @@ -445,6 +450,7 @@ async def didx_accept_request(request: web.BaseRequest): @match_info_schema(DIDXConnIdMatchInfoSchema()) @request_schema(DIDXRejectRequestSchema()) @response_schema(ConnRecordSchema(), 200, description="") +@tenant_authentication async def didx_reject(request: web.BaseRequest): """Abandon or reject a DID Exchange.""" context: AdminRequestContext = request["context"] diff --git a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py index 2888c91166..a06edc2bcb 100644 --- a/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/didexchange/v1_0/tests/test_routes.py @@ -1,16 +1,23 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock -from .. import routes as test_module from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageNotFoundError from ....coordinate_mediation.v1_0.route_manager import RouteManager +from .. import routes as test_module class TestDIDExchangeConnRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.profile = self.context.profile self.request_dict = { "context": self.context, @@ -21,6 +28,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.profile.context.injector.bind_instance(RouteManager, mock.MagicMock()) diff --git a/aries_cloudagent/protocols/discovery/v1_0/routes.py b/aries_cloudagent/protocols/discovery/v1_0/routes.py index f9d282b4cc..04416b8045 100644 --- a/aries_cloudagent/protocols/discovery/v1_0/routes.py +++ b/aries_cloudagent/protocols/discovery/v1_0/routes.py @@ -2,9 +2,9 @@ from aiohttp import web from aiohttp_apispec import docs, querystring_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....messaging.models.base import BaseModelError from ....messaging.models.openapi import OpenAPISchema @@ -66,6 +66,7 @@ class QueryDiscoveryExchRecordsSchema(OpenAPISchema): ) @querystring_schema(QueryFeaturesQueryStringSchema()) @response_schema(V10DiscoveryRecordSchema(), 200, description="") +@tenant_authentication async def query_features(request: web.BaseRequest): """Request handler for creating and sending feature query. @@ -96,6 +97,7 @@ async def query_features(request: web.BaseRequest): ) @querystring_schema(QueryDiscoveryExchRecordsSchema()) @response_schema(V10DiscoveryExchangeListResultSchema(), 200, description="") +@tenant_authentication async def query_records(request: web.BaseRequest): """Request handler for looking up V10DiscoveryExchangeRecord. diff --git a/aries_cloudagent/protocols/discovery/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/discovery/v1_0/tests/test_routes.py index ce4a4a34e6..4af4af5a8c 100644 --- a/aries_cloudagent/protocols/discovery/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/discovery/v1_0/tests/test_routes.py @@ -1,10 +1,10 @@ from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock +from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageError - from .. import routes as test_module from ..manager import V10DiscoveryMgr from ..messages.query import Query @@ -14,7 +14,12 @@ class TestDiscoveryRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.profile = self.context.profile self.request_dict = { "context": self.context, @@ -25,6 +30,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_query_features(self): diff --git a/aries_cloudagent/protocols/discovery/v2_0/routes.py b/aries_cloudagent/protocols/discovery/v2_0/routes.py index aeac69a424..bf2adb78b0 100644 --- a/aries_cloudagent/protocols/discovery/v2_0/routes.py +++ b/aries_cloudagent/protocols/discovery/v2_0/routes.py @@ -2,9 +2,9 @@ from aiohttp import web from aiohttp_apispec import docs, querystring_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....messaging.models.base import BaseModelError from ....messaging.models.openapi import OpenAPISchema @@ -76,6 +76,7 @@ class QueryDiscoveryExchRecordsSchema(OpenAPISchema): ) @querystring_schema(QueryFeaturesQueryStringSchema()) @response_schema(V20DiscoveryExchangeResultSchema(), 200, description="") +@tenant_authentication async def query_features(request: web.BaseRequest): """Request handler for creating and sending feature queries. @@ -106,6 +107,7 @@ async def query_features(request: web.BaseRequest): ) @querystring_schema(QueryDiscoveryExchRecordsSchema()) @response_schema(V20DiscoveryExchangeListResultSchema(), 200, description="") +@tenant_authentication async def query_records(request: web.BaseRequest): """Request handler for looking up V20DiscoveryExchangeRecord. diff --git a/aries_cloudagent/protocols/discovery/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/discovery/v2_0/tests/test_routes.py index d6c5ecd2f6..bcd542227e 100644 --- a/aries_cloudagent/protocols/discovery/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/discovery/v2_0/tests/test_routes.py @@ -1,10 +1,10 @@ from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock +from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....storage.error import StorageError - from .. import routes as test_module from ..manager import V20DiscoveryMgr from ..messages.queries import Queries, QueryItem @@ -14,7 +14,12 @@ class TestDiscoveryRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.profile = self.context.profile self.request_dict = { "context": self.context, @@ -25,6 +30,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_query_features(self): diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py index a96a2e8fe9..293a8448ef 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py @@ -415,6 +415,9 @@ async def complete_transaction( if (not endorser) and ( txn_goal_code != TransactionRecord.WRITE_DID_TRANSACTION ): + ledger = self.profile.inject(BaseLedger) + if not ledger: + raise TransactionManagerError("No ledger available") if ( self._profile.context.settings.get_value("wallet.type") == "askar-anoncreds" @@ -425,13 +428,9 @@ async def complete_transaction( legacy_indy_registry = LegacyIndyRegistry() ledger_response_json = await legacy_indy_registry.txn_submit( - self._profile, ledger_transaction, sign=False, taa_accept=False + ledger, ledger_transaction, sign=False, taa_accept=False ) else: - ledger = self.profile.inject(BaseLedger) - if not ledger: - raise TransactionManagerError("No ledger available") - async with ledger: try: ledger_response_json = await shield( diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py index 5631c161e2..f4ab0f2ebc 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py @@ -12,6 +12,7 @@ ) from marshmallow import fields, validate +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....core.event_bus import Event, EventBus @@ -124,6 +125,7 @@ class EndorserInfoSchema(OpenAPISchema): ) @querystring_schema(TransactionsListQueryStringSchema()) @response_schema(TransactionListSchema(), 200) +@tenant_authentication async def transactions_list(request: web.BaseRequest): """Request handler for searching transaction records. @@ -153,6 +155,7 @@ async def transactions_list(request: web.BaseRequest): @docs(tags=["endorse-transaction"], summary="Fetch a single transaction record") @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def transactions_retrieve(request: web.BaseRequest): """Request handler for fetching a single transaction record. @@ -186,6 +189,7 @@ async def transactions_retrieve(request: web.BaseRequest): @querystring_schema(TranIdMatchInfoSchema()) @request_schema(DateSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def transaction_create_request(request: web.BaseRequest): """Request handler for creating a new transaction record and request. @@ -276,6 +280,7 @@ async def transaction_create_request(request: web.BaseRequest): @querystring_schema(EndorserDIDInfoSchema()) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def endorse_transaction_response(request: web.BaseRequest): """Request handler for creating an endorsed transaction response. @@ -347,6 +352,7 @@ async def endorse_transaction_response(request: web.BaseRequest): ) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def refuse_transaction_response(request: web.BaseRequest): """Request handler for creating a refused transaction response. @@ -413,6 +419,7 @@ async def refuse_transaction_response(request: web.BaseRequest): ) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def cancel_transaction(request: web.BaseRequest): """Request handler for cancelling a Transaction request. @@ -477,6 +484,7 @@ async def cancel_transaction(request: web.BaseRequest): ) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def transaction_resend(request: web.BaseRequest): """Request handler for resending a transaction request. @@ -541,6 +549,7 @@ async def transaction_resend(request: web.BaseRequest): @querystring_schema(AssignTransactionJobsSchema()) @match_info_schema(TransactionConnIdMatchInfoSchema()) @response_schema(TransactionJobsSchema(), 200) +@tenant_authentication async def set_endorser_role(request: web.BaseRequest): """Request handler for assigning transaction jobs. @@ -581,6 +590,7 @@ async def set_endorser_role(request: web.BaseRequest): @querystring_schema(EndorserInfoSchema()) @match_info_schema(TransactionConnIdMatchInfoSchema()) @response_schema(EndorserInfoSchema(), 200) +@tenant_authentication async def set_endorser_info(request: web.BaseRequest): """Request handler for assigning endorser information. @@ -644,6 +654,7 @@ async def set_endorser_info(request: web.BaseRequest): ) @match_info_schema(TranIdMatchInfoSchema()) @response_schema(TransactionRecordSchema(), 200) +@tenant_authentication async def transaction_write(request: web.BaseRequest): """Request handler for writing an endorsed transaction to the ledger. diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py index ad79131aa4..d924b93216 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/tests/test_routes.py @@ -1,7 +1,7 @@ import asyncio import json - from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from .....connections.models.conn_record import ConnRecord @@ -23,7 +23,11 @@ class TestEndorseTransactionRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.context = self.profile.context setattr(self.context, "profile", self.profile) self.session = await self.profile.session() @@ -67,6 +71,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_did = "sample-did" diff --git a/aries_cloudagent/protocols/introduction/v0_1/routes.py b/aries_cloudagent/protocols/introduction/v0_1/routes.py index ed1e9ea226..591b14811c 100644 --- a/aries_cloudagent/protocols/introduction/v0_1/routes.py +++ b/aries_cloudagent/protocols/introduction/v0_1/routes.py @@ -5,9 +5,9 @@ from aiohttp import web from aiohttp_apispec import docs, match_info_schema, querystring_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....messaging.models.openapi import OpenAPISchema from ....messaging.valid import UUID4_EXAMPLE @@ -53,6 +53,7 @@ class IntroConnIdMatchInfoSchema(OpenAPISchema): @match_info_schema(IntroConnIdMatchInfoSchema()) @querystring_schema(IntroStartQueryStringSchema()) @response_schema(IntroModuleResponseSchema, description="") +@tenant_authentication async def introduction_start(request: web.BaseRequest): """Request handler for starting an introduction. diff --git a/aries_cloudagent/protocols/introduction/v0_1/tests/test_routes.py b/aries_cloudagent/protocols/introduction/v0_1/tests/test_routes.py index 9ace9b497a..aa5b64437d 100644 --- a/aries_cloudagent/protocols/introduction/v0_1/tests/test_routes.py +++ b/aries_cloudagent/protocols/introduction/v0_1/tests/test_routes.py @@ -1,15 +1,21 @@ -from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase -from .....admin.request_context import AdminRequestContext +from aries_cloudagent.tests import mock +from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .. import routes as test_module class TestIntroductionRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -19,6 +25,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_introduction_start_no_service(self): diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py index 3d3b68c3d9..e05e039bea 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/routes.py @@ -12,6 +12,7 @@ ) from marshmallow import fields, validate +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....core.profile import Profile @@ -381,6 +382,7 @@ class V10CredentialExchangeAutoRemoveRequestSchema(OpenAPISchema): ) @querystring_schema(V10CredentialExchangeListQueryStringSchema) @response_schema(V10CredentialExchangeListResultSchema(), 200, description="") +@tenant_authentication async def credential_exchange_list(request: web.BaseRequest): """Request handler for searching credential exchange records. @@ -422,6 +424,7 @@ async def credential_exchange_list(request: web.BaseRequest): ) @match_info_schema(CredExIdMatchInfoSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_retrieve(request: web.BaseRequest): """Request handler for fetching single credential exchange record. @@ -469,6 +472,7 @@ async def credential_exchange_retrieve(request: web.BaseRequest): ) @request_schema(V10CredentialCreateSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_create(request: web.BaseRequest): """Request handler for creating a credential from attr values. @@ -548,6 +552,7 @@ async def credential_exchange_create(request: web.BaseRequest): ) @request_schema(V10CredentialProposalRequestMandSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send(request: web.BaseRequest): """Request handler for sending credential from issuer to holder from attr values. @@ -650,6 +655,7 @@ async def credential_exchange_send(request: web.BaseRequest): ) @request_schema(V10CredentialProposalRequestOptSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_proposal(request: web.BaseRequest): """Request handler for sending credential proposal. @@ -773,6 +779,7 @@ async def _create_free_offer( ) @request_schema(V10CredentialConnFreeOfferRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_create_free_offer(request: web.BaseRequest): """Request handler for creating free credential offer. @@ -847,6 +854,7 @@ async def credential_exchange_create_free_offer(request: web.BaseRequest): ) @request_schema(V10CredentialFreeOfferRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_free_offer(request: web.BaseRequest): """Request handler for sending free credential offer. @@ -937,6 +945,7 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): @match_info_schema(CredExIdMatchInfoSchema()) @request_schema(V10CredentialBoundOfferRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_bound_offer(request: web.BaseRequest): """Request handler for sending bound credential offer. @@ -1037,6 +1046,7 @@ async def credential_exchange_send_bound_offer(request: web.BaseRequest): @match_info_schema(CredExIdMatchInfoSchema()) @request_schema(V10CredentialExchangeAutoRemoveRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_request(request: web.BaseRequest): """Request handler for sending credential request. @@ -1153,6 +1163,7 @@ async def credential_exchange_send_request(request: web.BaseRequest): @match_info_schema(CredExIdMatchInfoSchema()) @request_schema(V10CredentialIssueRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_issue(request: web.BaseRequest): """Request handler for sending credential. @@ -1249,6 +1260,7 @@ async def credential_exchange_issue(request: web.BaseRequest): @match_info_schema(CredExIdMatchInfoSchema()) @request_schema(V10CredentialStoreRequestSchema()) @response_schema(V10CredentialExchangeSchema(), 200, description="") +@tenant_authentication async def credential_exchange_store(request: web.BaseRequest): """Request handler for storing credential. @@ -1354,6 +1366,7 @@ async def credential_exchange_store(request: web.BaseRequest): @match_info_schema(CredExIdMatchInfoSchema()) @request_schema(V10CredentialProblemReportRequestSchema()) @response_schema(IssueCredentialModuleResponseSchema(), 200, description="") +@tenant_authentication async def credential_exchange_problem_report(request: web.BaseRequest): """Request handler for sending problem report. @@ -1400,6 +1413,7 @@ async def credential_exchange_problem_report(request: web.BaseRequest): ) @match_info_schema(CredExIdMatchInfoSchema()) @response_schema(IssueCredentialModuleResponseSchema(), 200, description="") +@tenant_authentication async def credential_exchange_remove(request: web.BaseRequest): """Request handler for removing a credential exchange record. diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py index e100a50a58..01c06e76a5 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_routes.py @@ -1,18 +1,23 @@ -from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase +from aries_cloudagent.tests import mock + from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....wallet.base import BaseWallet - from .. import routes as test_module - from . import CRED_DEF_ID class TestCredentialRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -22,6 +27,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_credential_exchange_list(self): diff --git a/aries_cloudagent/indy/sdk/__init__.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/__init__.py similarity index 100% rename from aries_cloudagent/indy/sdk/__init__.py rename to aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/__init__.py diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py new file mode 100644 index 0000000000..e2a688db96 --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/handler.py @@ -0,0 +1,601 @@ +"""V2.0 issue-credential vc_di credential format handler. + +indy compatible, attachment is a valid verifiable credential +""" + +import datetime +import json +import logging +from typing import Mapping, Tuple + +from ...models.cred_ex_record import V20CredExRecord +from ...models.detail.indy import ( + V20CredExRecordIndy, +) +from .models.cred import ( + VCDIIndyCredentialSchema, +) +from .models.cred_request import ( + AnoncredsLinkSecretRequest, + BindingProof, + DidcommSignedAttachmentRequest, + VCDICredRequest, + VCDICredRequestSchema, +) + +from ......vc.vc_ld import VerifiableCredential + +from .models.cred_offer import ( + AnoncredsLinkSecret, + BindingMethod, + DidcommSignedAttachment, + VCDICredAbstract, + VCDICredAbstractSchema, +) +from marshmallow import RAISE + +from ......anoncreds.revocation import AnonCredsRevocation + +from ......anoncreds.registry import AnonCredsRegistry +from ......anoncreds.holder import AnonCredsHolder, AnonCredsHolderError +from ......anoncreds.issuer import ( + AnonCredsIssuer, +) +from ......cache.base import BaseCache +from ......ledger.base import BaseLedger +from ......ledger.multiple_ledger.ledger_requests_executor import ( + GET_CRED_DEF, + IndyLedgerRequestsExecutor, +) +from ......messaging.credential_definitions.util import ( + CRED_DEF_SENT_RECORD_TYPE, + CredDefQueryStringSchema, +) +from ......messaging.decorators.attach_decorator import AttachDecorator +from ......multitenant.base import BaseMultitenantManager +from ......revocation_anoncreds.models.issuer_cred_rev_record import IssuerCredRevRecord +from ......storage.base import BaseStorage +from ......wallet.base import BaseWallet +from ...message_types import ( + ATTACHMENT_FORMAT, + CRED_20_ISSUE, + CRED_20_OFFER, + CRED_20_PROPOSAL, + CRED_20_REQUEST, +) +from ...messages.cred_format import V20CredFormat +from ...messages.cred_issue import V20CredIssue +from ...messages.cred_offer import V20CredOffer +from ...messages.cred_proposal import V20CredProposal +from ...messages.cred_request import V20CredRequest + +from ..handler import CredFormatAttachment, V20CredFormatError, V20CredFormatHandler + +LOGGER = logging.getLogger(__name__) + + +class VCDICredFormatHandler(V20CredFormatHandler): + """VCDI credential format handler.""" + + format = V20CredFormat.Format.VC_DI + + @classmethod + def validate_fields(cls, message_type: str, attachment_data: Mapping): + """Validate attachment data for a specific message type. + + Uses marshmallow schemas to validate if format specific attachment data + is valid for the specified message type. Only does structural and type + checks, does not validate if .e.g. the issuer value is valid. + + Args: + message_type (str): The message type to validate the attachment data for. + Should be one of the message types as defined in message_types.py + attachment_data (Mapping): [description] + The attachment data to valide + Raises: + Exception: When the data is not valid. + """ + mapping = { + CRED_20_PROPOSAL: CredDefQueryStringSchema, + CRED_20_OFFER: VCDICredAbstractSchema, + CRED_20_REQUEST: VCDICredRequestSchema, + CRED_20_ISSUE: VCDIIndyCredentialSchema, + } + + # Get schema class + Schema = mapping[message_type] + + # Validate, throw if not valid + Schema(unknown=RAISE).load(attachment_data) + + async def get_detail_record(self, cred_ex_id: str) -> V20CredExRecordIndy: + """Retrieve credential exchange detail record by cred_ex_id.""" + + async with self.profile.session() as session: + records = await VCDICredFormatHandler.format.detail.query_by_cred_ex_id( + session, cred_ex_id + ) + + if len(records) > 1: + LOGGER.warning( + "Cred ex id %s has %d %s detail records: should be 1", + cred_ex_id, + len(records), + VCDICredFormatHandler.format.api, + ) + return records[0] if records else None + + async def _check_uniqueness(self, cred_ex_id: str): + """Raise exception on evidence that cred ex already has cred issued to it.""" + async with self.profile.session() as session: + exist = await VCDICredFormatHandler.format.detail.query_by_cred_ex_id( + session, cred_ex_id + ) + if exist: + raise V20CredFormatError( + f"{VCDICredFormatHandler.format.api} detail record already " + f"exists for cred ex id {cred_ex_id}" + ) + + def get_format_identifier(self, message_type: str) -> str: + """Get attachment format identifier for format and message combination. + + Args: + message_type (str): Message type for which to return the format identifier + Returns: + str: Issue credential attachment format identifier + """ + return ATTACHMENT_FORMAT[message_type][VCDICredFormatHandler.format.api] + + def get_format_data(self, message_type: str, data: dict) -> CredFormatAttachment: + """Get credential format and attachment objects for use in cred ex messages. + + Returns a tuple of both credential format and attachment decorator for use + in credential exchange messages. It looks up the correct format identifier and + encodes the data as a base64 attachment. + + Args: + message_type (str): The message type for which to return the cred format. + Should be one of the message types defined in the message types file + data (dict): The data to include in the attach decorator + Returns: + CredFormatAttachment: Credential format and attachment data objects + """ + return ( + V20CredFormat( + attach_id=VCDICredFormatHandler.format.api, + format_=self.get_format_identifier(message_type), + ), + AttachDecorator.data_base64(data, ident=VCDICredFormatHandler.format.api), + ) + + async def _match_sent_cred_def_id(self, tag_query: Mapping[str, str]) -> str: + """Return most recent matching id of cred def that agent sent to ledger.""" + + async with self.profile.session() as session: + storage = session.inject(BaseStorage) + found = await storage.find_all_records( + type_filter=CRED_DEF_SENT_RECORD_TYPE, tag_query=tag_query + ) + if not found: + raise V20CredFormatError( + f"Issuer has no operable cred def for proposal spec {tag_query}" + ) + return max(found, key=lambda r: int(r.tags["epoch"])).tags["cred_def_id"] + + async def create_proposal( + self, cred_ex_record: V20CredExRecord, proposal_data: Mapping[str, str] + ) -> Tuple[V20CredFormat, AttachDecorator]: + """Create vc_di credential proposal.""" + if proposal_data is None: + proposal_data = {} + + return self.get_format_data(CRED_20_PROPOSAL, proposal_data) + + async def receive_proposal( + self, + cred_ex_record: V20CredExRecord, + cred_proposal_message: V20CredProposal, + ) -> None: + """Receive vcdi credential proposal. + + No custom handling is required for this step. + """ + + async def create_offer( + self, cred_proposal_message: V20CredProposal + ) -> CredFormatAttachment: + """Create vcdi credential offer.""" + + issuer = AnonCredsIssuer(self.profile) + # TODO use the ledger registry in the anoncreds module, + # or move the functionality into the ledger class. + ledger = self.profile.inject(BaseLedger) + cache = self.profile.inject_or(BaseCache) + + async with self.profile.session() as session: + wallet = session.inject(BaseWallet) + public_did_info = await wallet.get_public_did() + public_did = public_did_info.did + + cred_def_id = await issuer.match_created_credential_definitions( + **cred_proposal_message.attachment(VCDICredFormatHandler.format) + ) + + async def _create(): + offer_str = await issuer.create_credential_offer(cred_def_id) + return json.loads(offer_str) + + # TODO use the ledger registry in the anoncreds module, + # or move the functionality into the ledger class. + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) + ledger = ( + await ledger_exec_inst.get_ledger_for_identifier( + cred_def_id, + txn_record_type=GET_CRED_DEF, + ) + )[1] + # TODO use the ledger registry in the anoncreds module, + # or move the functionality into the ledger class. + async with ledger: + schema_id = await ledger.credential_definition_id2schema_id(cred_def_id) + schema = await ledger.get_schema(schema_id) + schema_attrs = set(schema["attrNames"]) + preview_attrs = set(cred_proposal_message.credential_preview.attr_dict()) + if preview_attrs != schema_attrs: + raise V20CredFormatError( + f"Preview attributes {preview_attrs} " + f"mismatch corresponding schema attributes {schema_attrs}" + ) + + cred_offer = None + cache_key = f"credential_offer::{cred_def_id}" + + if cache: + async with cache.acquire(cache_key) as entry: + if entry.result: + cred_offer = entry.result + else: + cred_offer = await _create() + await entry.set_result(cred_offer, 3600) + if not cred_offer: + cred_offer = await _create() + + credential = VerifiableCredential( + issuer=public_did, + credential_subject=cred_proposal_message.credential_preview.attr_dict(), + issuance_date=datetime.datetime.now(datetime.timezone.utc).isoformat(), + ) + + anoncreds_link_secret_instance = AnoncredsLinkSecret( + cred_def_id=cred_offer["cred_def_id"], + key_correctness_proof=cred_offer["key_correctness_proof"], + nonce=cred_offer["nonce"], + ) + + didcomm_signed_attachment_instance = DidcommSignedAttachment( + algs_supported=["EdDSA"], + did_methods_supported=["key"], + nonce=cred_offer["nonce"], + ) + + binding_method_instance = BindingMethod( + anoncreds_link_secret=anoncreds_link_secret_instance, + didcomm_signed_attachment=didcomm_signed_attachment_instance, + ) + + vcdi_cred_abstract = VCDICredAbstract( + data_model_versions_supported=["1.1"], + binding_required=True, + binding_method=binding_method_instance, + credential=credential, + ) + + return self.get_format_data( + CRED_20_OFFER, json.loads(vcdi_cred_abstract.to_json()) + ) + + async def receive_offer( + self, cred_ex_record: V20CredExRecord, cred_offer_message: V20CredOffer + ) -> None: + """Receive vcdi credential offer.""" + + async def create_request( + self, cred_ex_record: V20CredExRecord, request_data: Mapping = None + ) -> CredFormatAttachment: + """Create vcdi credential request.""" + if cred_ex_record.state != V20CredExRecord.STATE_OFFER_RECEIVED: + raise V20CredFormatError( + "vcdi issue credential format cannot start from credential request" + ) + + await self._check_uniqueness(cred_ex_record.cred_ex_id) + + holder_did = request_data.get("holder_did") if request_data else None + cred_offer = cred_ex_record.cred_offer.attachment(VCDICredFormatHandler.format) + + if ( + "anoncreds_link_secret" in cred_offer["binding_method"] + and "nonce" not in cred_offer["binding_method"]["anoncreds_link_secret"] + ): + raise V20CredFormatError( + "Missing nonce in credential offer with anoncreds link secret " + "binding method" + ) + + nonce = cred_offer["binding_method"]["anoncreds_link_secret"]["nonce"] + cred_def_id = cred_offer["binding_method"]["anoncreds_link_secret"][ + "cred_def_id" + ] + + ledger = self.profile.inject(BaseLedger) + # TODO use the ledger registry in the anoncreds module, + # or move the functionality into the ledger class. + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) + ledger = ( + await ledger_exec_inst.get_ledger_for_identifier( + cred_def_id, + txn_record_type=GET_CRED_DEF, + ) + )[1] + + async with ledger: + schema_id = await ledger.credential_definition_id2schema_id(cred_def_id) + + async def _create(): + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + + cred_def_result = await anoncreds_registry.get_credential_definition( + self.profile, cred_def_id + ) + + legacy_offer = await self._prepare_legacy_offer(cred_offer, schema_id) + + holder = AnonCredsHolder(self.profile) + request_json, metadata_json = await holder.create_credential_request( + legacy_offer, cred_def_result.credential_definition, holder_did + ) + + return { + "request": json.loads(request_json), + "metadata": json.loads(metadata_json), + } + + cache_key = f"credential_request::{cred_def_id}::{holder_did}::{nonce}" + cred_req_result = None + cache = self.profile.inject_or(BaseCache) + if cache: + async with cache.acquire(cache_key) as entry: + if entry.result: + cred_req_result = entry.result + else: + cred_req_result = await _create() + await entry.set_result(cred_req_result, 3600) + if not cred_req_result: + cred_req_result = await _create() + detail_record = V20CredExRecordIndy( + cred_ex_id=cred_ex_record.cred_ex_id, + cred_request_metadata=cred_req_result["metadata"], + ) + + anoncreds_link_secret_instance = AnoncredsLinkSecretRequest( + entropy=cred_req_result["request"]["prover_did"], + cred_def_id=cred_req_result["request"]["cred_def_id"], + blinded_ms=cred_req_result["request"]["blinded_ms"], + blinded_ms_correctness_proof=cred_req_result["request"][ + "blinded_ms_correctness_proof" + ], + nonce=cred_req_result["request"]["nonce"], + ) + + didcomm_signed_attachment_instance = DidcommSignedAttachmentRequest( + attachment_id="test" + ) + + binding_proof_instance = BindingProof( + anoncreds_link_secret=anoncreds_link_secret_instance, + didcomm_signed_attachment=didcomm_signed_attachment_instance, + ) + + vcdi_cred_request = VCDICredRequest( + data_model_version="2.0", binding_proof=binding_proof_instance + ) + + async with self.profile.session() as session: + await detail_record.save(session, reason="create v2.0 credential request") + + return self.get_format_data( + CRED_20_REQUEST, json.loads(vcdi_cred_request.to_json()) + ) + + async def receive_request( + self, cred_ex_record: V20CredExRecord, cred_request_message: V20CredRequest + ) -> None: + """Receive vcdi credential request.""" + if not cred_ex_record.cred_offer: + raise V20CredFormatError( + "vcdi issue credential format cannot start from credential request" + ) + + async def issue_credential( + self, cred_ex_record: V20CredExRecord, retries: int = 5 + ) -> CredFormatAttachment: + """Issue vcdi credential.""" + await self._check_uniqueness(cred_ex_record.cred_ex_id) + + cred_offer = cred_ex_record.cred_offer.attachment(VCDICredFormatHandler.format) + cred_request = cred_ex_record.cred_request.attachment( + VCDICredFormatHandler.format + ) + cred_values = cred_ex_record.cred_offer.credential_preview.attr_dict( + decode=False + ) + + cred_def_id = cred_offer["binding_method"]["anoncreds_link_secret"][ + "cred_def_id" + ] + + ledger = self.profile.inject(BaseLedger) + # TODO use the ledger registry in the anoncreds module, + # or move the functionality into the ledger class. + multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) + else: + ledger_exec_inst = self.profile.inject(IndyLedgerRequestsExecutor) + ledger = ( + await ledger_exec_inst.get_ledger_for_identifier( + cred_def_id, + txn_record_type=GET_CRED_DEF, + ) + )[1] + + async with ledger: + schema_id = await ledger.credential_definition_id2schema_id(cred_def_id) + + legacy_offer = await self._prepare_legacy_offer(cred_offer, schema_id) + legacy_request = await self._prepare_legacy_request(cred_request, cred_def_id) + + issuer = AnonCredsIssuer(self.profile) + + credential = await issuer.create_credential_w3c( + legacy_offer, legacy_request, cred_values + ) + + vcdi_credential = { + "credential": json.loads(credential), + } + + result = self.get_format_data(CRED_20_ISSUE, vcdi_credential) + + cred_rev_id = None + rev_reg_def_id = None + + async with self._profile.transaction() as txn: + detail_record = V20CredExRecordIndy( + cred_ex_id=cred_ex_record.cred_ex_id, + rev_reg_id=rev_reg_def_id, + cred_rev_id=cred_rev_id, + ) + await detail_record.save(txn, reason="v2.0 issue credential") + + if cred_rev_id: + issuer_cr_rec = IssuerCredRevRecord( + state=IssuerCredRevRecord.STATE_ISSUED, + cred_ex_id=cred_ex_record.cred_ex_id, + cred_ex_version=IssuerCredRevRecord.VERSION_2, + rev_reg_id=rev_reg_def_id, + cred_rev_id=cred_rev_id, + ) + await issuer_cr_rec.save( + txn, + reason=( + "Created issuer cred rev record for " + f"rev reg id {rev_reg_def_id}, index {cred_rev_id}" + ), + ) + await txn.commit() + + return result + + async def _prepare_legacy_offer(self, cred_offer: dict, schema_id: str) -> dict: + """Convert current offer to legacy offer format.""" + return { + "schema_id": schema_id, + "cred_def_id": cred_offer["binding_method"]["anoncreds_link_secret"][ + "cred_def_id" + ], + "key_correctness_proof": cred_offer["binding_method"][ + "anoncreds_link_secret" + ]["key_correctness_proof"], + "nonce": cred_offer["binding_method"]["anoncreds_link_secret"]["nonce"], + } + + async def _prepare_legacy_request(self, cred_request: dict, cred_def_id: str): + return { + "prover_did": cred_request["binding_proof"]["anoncreds_link_secret"][ + "entropy" + ], + "cred_def_id": cred_def_id, + "blinded_ms": cred_request["binding_proof"]["anoncreds_link_secret"][ + "blinded_ms" + ], + "blinded_ms_correctness_proof": cred_request["binding_proof"][ + "anoncreds_link_secret" + ]["blinded_ms_correctness_proof"], + "nonce": cred_request["binding_proof"]["anoncreds_link_secret"]["nonce"], + } + + async def receive_credential( + self, cred_ex_record: V20CredExRecord, cred_issue_message: V20CredIssue + ) -> None: + """Receive vcdi credential. + + Validation is done in the store credential step. + """ + + async def store_credential( + self, cred_ex_record: V20CredExRecord, cred_id: str = None + ) -> None: + """Store vcdi credential.""" + cred = cred_ex_record.cred_issue.attachment(VCDICredFormatHandler.format) + cred = cred["credential"] + + rev_reg_def = None + anoncreds_registry = self.profile.inject(AnonCredsRegistry) + cred_def_result = await anoncreds_registry.get_credential_definition( + self.profile, cred["proof"][0]["verificationMethod"] + ) + if cred["proof"][0].get("rev_reg_id"): + rev_reg_def_result = ( + await anoncreds_registry.get_revocation_registry_definition( + self.profile, cred["proof"][0]["rev_reg_id"] + ) + ) + rev_reg_def = rev_reg_def_result.revocation_registry + + holder = AnonCredsHolder(self.profile) + cred_offer_message = cred_ex_record.cred_offer + mime_types = None + if cred_offer_message and cred_offer_message.credential_preview: + mime_types = cred_offer_message.credential_preview.mime_types() or None + + if rev_reg_def: + revocation = AnonCredsRevocation(self.profile) + await revocation.get_or_fetch_local_tails_path(rev_reg_def) + try: + detail_record = await self.get_detail_record(cred_ex_record.cred_ex_id) + if detail_record is None: + raise V20CredFormatError( + f"No credential exchange {VCDICredFormatHandler.format.aries} " + f"detail record found for cred ex id {cred_ex_record.cred_ex_id}" + ) + cred_id_stored = await holder.store_credential_w3c( + cred_def_result.credential_definition.serialize(), + cred, + detail_record.cred_request_metadata, + mime_types, + credential_id=cred_id, + rev_reg_def=rev_reg_def.serialize() if rev_reg_def else None, + ) + + detail_record.cred_id_stored = cred_id_stored + detail_record.rev_reg_id = cred["proof"][0].get("rev_reg_id", None) + detail_record.cred_rev_id = cred["proof"][0].get("cred_rev_id", None) + + async with self.profile.session() as session: + # Store detail record, emit event + await detail_record.save( + session, reason="store credential v2.0", event=True + ) + except AnonCredsHolderError as e: + LOGGER.error(f"Error storing credential: {e.error_code} - {e.message}") + raise e diff --git a/aries_cloudagent/indy/sdk/tests/__init__.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/__init__.py similarity index 100% rename from aries_cloudagent/indy/sdk/tests/__init__.py rename to aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/__init__.py diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/cred.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/cred.py new file mode 100644 index 0000000000..0a7eafeb27 --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/cred.py @@ -0,0 +1,43 @@ +"""Cred request artifacts to attach to RFC 453 messages.""" + +from .......messaging.models.base import BaseModel, BaseModelSchema +from marshmallow import EXCLUDE, fields + + +class VCDIIndyCredential(BaseModel): + """VCDI Indy credential.""" + + class Meta: + """VCDI Indy credential metadata.""" + + schema_class = "VCDIIndyCredentialSchema" + + def __init__( + self, + credential: dict = None, + **kwargs, + ): + """Initialize vcdi cred abstract object. + + Args: + data_model_versions_supported: supported versions for data model + binding_required: boolean value + binding_methods: required if binding_required is true + credential: credential object + """ + super().__init__(**kwargs) + self.credential = credential + + +class VCDIIndyCredentialSchema(BaseModelSchema): + """VCDI Indy credential schema.""" + + class Meta: + """VCDI Indy credential schemametadata.""" + + model_class = VCDIIndyCredential + unknown = EXCLUDE + + credential = fields.Dict( + fields.Str(), required=True, metadata={"description": "", "example": ""} + ) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/cred_offer.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/cred_offer.py new file mode 100644 index 0000000000..c42ba32174 --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/cred_offer.py @@ -0,0 +1,214 @@ +"""Cred request artifacts to attach to RFC 453 messages.""" + +from typing import Sequence, Union +from .......indy.models.cred_abstract import IndyKeyCorrectnessProofSchema +from .......messaging.models.base import BaseModel, BaseModelSchema +from .......vc.vc_ld.models.credential import ( + CredentialSchema, + VerifiableCredential, +) +from .......messaging.valid import ( + INDY_CRED_DEF_ID_EXAMPLE, + INDY_CRED_DEF_ID_VALIDATE, + NUM_STR_WHOLE_EXAMPLE, + NUM_STR_WHOLE_VALIDATE, +) + +from marshmallow import EXCLUDE, fields + + +class AnoncredsLinkSecret(BaseModel): + """Anoncreds Link Secret Model.""" + + class Meta: + """AnoncredsLinkSecret metadata.""" + + schema_class = "AnoncredsLinkSecretSchema" + + def __init__( + self, + nonce: str = None, + cred_def_id: str = None, + key_correctness_proof: str = None, + **kwargs, + ): + """Initialize values for AnoncredsLinkSecret.""" + super().__init__(**kwargs) + self.nonce = nonce + self.cred_def_id = cred_def_id + self.key_correctness_proof = key_correctness_proof + + +class AnoncredsLinkSecretSchema(BaseModelSchema): + """Anoncreds Link Secret Schema.""" + + class Meta: + """AnoncredsLinkSecret schema metadata.""" + + model_class = AnoncredsLinkSecret + unknown = EXCLUDE + + nonce = fields.Str( + required=True, + validate=NUM_STR_WHOLE_VALIDATE, + metadata={ + "description": "Nonce in credential abstract", + "example": NUM_STR_WHOLE_EXAMPLE, + }, + ) + + cred_def_id = fields.Str( + required=True, + validate=INDY_CRED_DEF_ID_VALIDATE, + metadata={ + "description": "Credential definition identifier", + "example": INDY_CRED_DEF_ID_EXAMPLE, + }, + ) + + key_correctness_proof = fields.Nested( + IndyKeyCorrectnessProofSchema(), + required=True, + metadata={"description": "Key correctness proof"}, + ) + + +class DidcommSignedAttachment(BaseModel): + """Didcomm Signed Attachment Model.""" + + class Meta: + """DidcommSignedAttachment metadata.""" + + schema_class = "DidcommSignedAttachmentSchema" + + def __init__( + self, + algs_supported: Sequence[str] = None, + did_methods_supported: Sequence[str] = None, + nonce: str = None, + **kwargs, + ): + """Initialize values for DidcommSignedAttachment.""" + super().__init__(**kwargs) + self.algs_supported = algs_supported + self.did_methods_supported = did_methods_supported + self.nonce = nonce + + +class DidcommSignedAttachmentSchema(BaseModelSchema): + """Didcomm Signed Attachment Schema.""" + + class Meta: + """Didcomm signed attachment schema metadata.""" + + model_class = DidcommSignedAttachment + unknown = EXCLUDE + + algs_supported = fields.List(fields.Str(), required=True) + + did_methods_supported = fields.List(fields.Str(), required=True) + + nonce = fields.Str( + required=True, + validate=NUM_STR_WHOLE_VALIDATE, + metadata={ + "description": "Nonce in credential abstract", + "example": NUM_STR_WHOLE_EXAMPLE, + }, + ) + + +class BindingMethod(BaseModel): + """Binding Method Model.""" + + class Meta: + """Binding method metadata.""" + + schema_class = "BindingMethodSchema" + + def __init__( + self, + anoncreds_link_secret: Union[dict, AnoncredsLinkSecret] = None, + didcomm_signed_attachment: Union[dict, DidcommSignedAttachment] = None, + **kwargs, + ): + """Initialize values for DidcommSignedAttachment.""" + super().__init__(**kwargs) + self.anoncreds_link_secret = anoncreds_link_secret + self.didcomm_signed_attachment = didcomm_signed_attachment + + +class BindingMethodSchema(BaseModelSchema): + """VCDI Binding Method Schema.""" + + class Meta: + """VCDI binding method schema metadata.""" + + model_class = BindingMethod + unknown = EXCLUDE + + anoncreds_link_secret = fields.Nested(AnoncredsLinkSecretSchema, required=False) + didcomm_signed_attachment = fields.Nested( + DidcommSignedAttachmentSchema, required=True + ) + + +class VCDICredAbstract(BaseModel): + """VCDI Credential Abstract.""" + + class Meta: + """VCDI credential abstract metadata.""" + + schema_class = "VCDICredAbstractSchema" + + def __init__( + self, + data_model_versions_supported: Sequence[str] = None, + binding_required: str = None, + binding_method: str = None, + credential: Union[dict, VerifiableCredential] = None, + **kwargs, + ): + """Initialize vcdi cred abstract object. + + Args: + data_model_versions_supported: supported versions for data model + binding_required: boolean value + binding_methods: required if binding_required is true + credential: credential object + """ + super().__init__(**kwargs) + self.data_model_versions_supported = data_model_versions_supported + self.binding_required = binding_required + self.binding_method = binding_method + self.credential = credential + + +class VCDICredAbstractSchema(BaseModelSchema): + """VCDI Credential Abstract Schema.""" + + class Meta: + """VCDICredAbstractSchema metadata.""" + + model_class = VCDICredAbstract + unknown = EXCLUDE + + data_model_versions_supported = fields.List( + fields.Str(), required=True, metadata={"description": "", "example": ""} + ) + + binding_required = fields.Bool( + required=False, metadata={"description": "", "example": ""} + ) + + binding_method = fields.Nested( + BindingMethodSchema(), + required=binding_required, + metadata={"description": "", "example": ""}, + ) + + credential = fields.Nested( + CredentialSchema(), + required=True, + metadata={"description": "", "example": ""}, + ) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/cred_request.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/cred_request.py new file mode 100644 index 0000000000..7845b7676b --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/models/cred_request.py @@ -0,0 +1,188 @@ +"""Cred request artifacts to attach to RFC 453 messages.""" + +from typing import Mapping, Union +from .......messaging.models.base import BaseModel, BaseModelSchema +from .......messaging.valid import ( + INDY_CRED_DEF_ID_EXAMPLE, + INDY_CRED_DEF_ID_VALIDATE, + INDY_DID_EXAMPLE, + NUM_STR_WHOLE_EXAMPLE, + NUM_STR_WHOLE_VALIDATE, +) + +from marshmallow import EXCLUDE, fields + + +class AnoncredsLinkSecretRequest(BaseModel): + """Binding proof model.""" + + class Meta: + """VCDI credential request schema metadata.""" + + schema_class = "BindingProofSchema" + + def __init__( + self, + entropy: str = None, + cred_def_id: str = None, + blinded_ms: Mapping = None, + blinded_ms_correctness_proof: Mapping = None, + nonce: str = None, + **kwargs, + ): + """Initialize indy credential request.""" + super().__init__(**kwargs) + self.entropy = entropy + self.cred_def_id = cred_def_id + self.blinded_ms = blinded_ms + self.blinded_ms_correctness_proof = blinded_ms_correctness_proof + self.nonce = nonce + + +class AnoncredsLinkSecretSchema(BaseModelSchema): + """VCDI credential request schema.""" + + class Meta: + """VCDI credential request schema metadata.""" + + model_class = AnoncredsLinkSecretRequest + unknown = EXCLUDE + + entropy = fields.Str( + required=True, + validate=str, + metadata={"description": "Prover DID", "example": INDY_DID_EXAMPLE}, + ) + cred_def_id = fields.Str( + required=True, + validate=INDY_CRED_DEF_ID_VALIDATE, + metadata={ + "description": "Credential definition identifier", + "example": INDY_CRED_DEF_ID_EXAMPLE, + }, + ) + blinded_ms = fields.Dict( + required=True, metadata={"description": "Blinded master secret"} + ) + blinded_ms_correctness_proof = fields.Dict( + required=True, + metadata={"description": "Blinded master secret correctness proof"}, + ) + nonce = fields.Str( + required=True, + validate=NUM_STR_WHOLE_VALIDATE, + metadata={ + "description": "Nonce in credential request", + "example": NUM_STR_WHOLE_EXAMPLE, + }, + ) + + +class DidcommSignedAttachmentRequest(BaseModel): + """Didcomm Signed Attachment Model.""" + + class Meta: + """Didcomm signed attachment metadata.""" + + schema_class = "DidcommSignedAttachmentSchema" + + def __init__(self, attachment_id: str = None, **kwargs): + """Initialize DidcommSignedAttachment.""" + super().__init__(**kwargs) + self.attachment_id = attachment_id + + +class DidcommSignedAttachmentSchema(BaseModelSchema): + """Didcomm Signed Attachment Schema.""" + + class Meta: + """Didcomm Signed Attachment schema metadata.""" + + model_class = DidcommSignedAttachmentRequest + unknown = EXCLUDE + + attachment_id = fields.Str( + required=True, metadata={"description": "", "example": ""} + ) + + +class BindingProof(BaseModel): + """Binding Proof Model.""" + + class Meta: + """Binding proof metadata.""" + + schema_class = "BindingProofSchema" + + def __init__( + self, + anoncreds_link_secret: str = None, + didcomm_signed_attachment: str = None, + **kwargs, + ): + """Initialize binding proof.""" + super().__init__(**kwargs) + self.anoncreds_link_secret = anoncreds_link_secret + self.didcomm_signed_attachment = didcomm_signed_attachment + + +class BindingProofSchema(BaseModelSchema): + """Binding Proof Schema.""" + + class Meta: + """Binding proof schema metadata.""" + + model_class = BindingProof + unknown = EXCLUDE + + anoncreds_link_secret = fields.Nested( + AnoncredsLinkSecretSchema(), + required=True, + metadata={"description": "", "example": ""}, + ) + + didcomm_signed_attachment = fields.Nested( + DidcommSignedAttachmentSchema(), + required=True, + metadata={"description": "", "example": ""}, + ) + + +class VCDICredRequest(BaseModel): + """VCDI credential request model.""" + + class Meta: + """VCDI credential request metadata.""" + + schema_class = "VCDICredRequestSchema" + + def __init__( + self, + data_model_version: str = None, + binding_proof: Union[dict, BindingProof] = None, + **kwargs, + ): + """Initialize values for VCDICredRequest.""" + super().__init__(**kwargs) + self.data_model_version = data_model_version + self.binding_proof = binding_proof + + +class VCDICredRequestSchema(BaseModelSchema): + """VCDI credential request schema.""" + + class Meta: + """VCDI credential request schema metadata.""" + + model_class = VCDICredRequest + unknown = EXCLUDE + + data_model_version = fields.Str( + required=True, metadata={"description": "", "example": ""} + ) + + binding_proof = fields.Nested( + BindingProofSchema(), + required=True, + metadata={"description": "", "example": ""}, + ) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/__init__.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py new file mode 100644 index 0000000000..04d3c552ef --- /dev/null +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/vc_di/tests/test_handler.py @@ -0,0 +1,704 @@ +from copy import deepcopy +from time import time +import json + +import pytest +from .......anoncreds.holder import AnonCredsHolder +from .......messaging.credential_definitions.util import ( + CRED_DEF_SENT_RECORD_TYPE, +) +from .......messaging.decorators.attach_decorator import AttachDecorator +from .......multitenant.base import BaseMultitenantManager +from .......multitenant.manager import MultitenantManager +from .......protocols.issue_credential.v2_0.formats.handler import ( + V20CredFormatError, +) +from .......protocols.issue_credential.v2_0.messages.cred_format import ( + V20CredFormat, +) +from .......protocols.issue_credential.v2_0.messages.cred_offer import ( + V20CredOffer, +) +from .......protocols.issue_credential.v2_0.messages.cred_proposal import ( + V20CredProposal, +) +from .......protocols.issue_credential.v2_0.messages.cred_request import ( + V20CredRequest, +) +from .......protocols.issue_credential.v2_0.messages.inner.cred_preview import ( + V20CredAttrSpec, + V20CredPreview, +) +from .......protocols.issue_credential.v2_0.models.detail.indy import ( + V20CredExRecordIndy, +) + +from .......storage.record import StorageRecord + +from unittest import IsolatedAsyncioTestCase + +from marshmallow import ValidationError + +from aries_cloudagent.tests import mock + +from .......core.in_memory import InMemoryProfile +from .......ledger.base import BaseLedger +from .......ledger.multiple_ledger.ledger_requests_executor import ( + IndyLedgerRequestsExecutor, +) +from .......cache.in_memory import InMemoryCache +from .......cache.base import BaseCache +from .......anoncreds.issuer import AnonCredsIssuer + +from ....message_types import ( + ATTACHMENT_FORMAT, + CRED_20_ISSUE, + CRED_20_OFFER, + CRED_20_PROPOSAL, + CRED_20_REQUEST, +) +from ..handler import VCDICredFormatHandler +from ..handler import LOGGER as VCDI_LOGGER +from .. import handler as test_module + +# these are from faber +CRED_PREVIEW_TYPE = "https://didcomm.org/issue-credential/2.0/credential-preview" + + +TEST_DID = "LjgpST2rjsoxYegQDRm7EL" +SCHEMA_NAME = "bc-reg" +SCHEMA_TXN = 12 +SCHEMA_ID = f"{TEST_DID}:2:{SCHEMA_NAME}:1.0" +SCHEMA = { + "ver": "1.0", + "id": SCHEMA_ID, + "name": SCHEMA_NAME, + "version": "1.0", + "attrNames": ["legalName", "jurisdictionId", "incorporationDate"], + "seqNo": SCHEMA_TXN, +} +CRED_DEF_ID = f"{TEST_DID}:3:CL:12:tag1" +CRED_DEF = { + "ver": "1.0", + "id": CRED_DEF_ID, + "schemaId": SCHEMA_TXN, + "type": "CL", + "tag": "tag1", + "value": { + "primary": { + "n": "...", + "s": "...", + "r": { + "master_secret": "...", + "legalName": "...", + "jurisdictionId": "...", + "incorporationDate": "...", + }, + "rctxt": "...", + "z": "...", + }, + "revocation": { + "g": "1 ...", + "g_dash": "1 ...", + "h": "1 ...", + "h0": "1 ...", + "h1": "1 ...", + "h2": "1 ...", + "htilde": "1 ...", + "h_cap": "1 ...", + "u": "1 ...", + "pk": "1 ...", + "y": "1 ...", + }, + }, +} +REV_REG_DEF_TYPE = "CL_ACCUM" +REV_REG_ID = f"{TEST_DID}:4:{CRED_DEF_ID}:{REV_REG_DEF_TYPE}:tag1" +TAILS_DIR = "/tmp/indy/revocation/tails_files" +TAILS_HASH = "8UW1Sz5cqoUnK9hqQk7nvtKK65t7Chu3ui866J23sFyJ" +TAILS_LOCAL = f"{TAILS_DIR}/{TAILS_HASH}" +REV_REG_DEF = { + "ver": "1.0", + "id": REV_REG_ID, + "revocDefType": "CL_ACCUM", + "tag": "tag1", + "credDefId": CRED_DEF_ID, + "value": { + "issuanceType": "ISSUANCE_ON_DEMAND", + "maxCredNum": 5, + "publicKeys": {"accumKey": {"z": "1 ..."}}, + "tailsHash": TAILS_HASH, + "tailsLocation": TAILS_LOCAL, + }, +} +VCDI_OFFER = { + "data_model_versions_supported": ["1.1"], + "binding_required": True, + "binding_method": { + "anoncreds_link_secret": { + "cred_def_id": CRED_DEF_ID, + "key_correctness_proof": { + "c": "123467890", + "xz_cap": "12345678901234567890", + "xr_cap": [ + ["remainder", "1234567890"], + ["number", "12345678901234"], + ], + }, + "nonce": "803336938981521544311884", + }, + "didcomm_signed_attachment": { + "algs_supported": ["EdDSA"], + "did_methods_supported": ["key"], + "nonce": "803336938981521544311884", + }, + }, + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/data-integrity/v2", + {"@vocab": "https://www.w3.org/ns/credentials/issuer-dependent#"}, + ], + "type": ["VerifiableCredential"], + "issuer": "LSTv7AUoTyfqFxZbuAGqKR", + "credentialSubject": { + "name": "Alice Smith", + "date": "2018-05-28", + "degree": "Maths", + "birthdate_dateint": "20000331", + "timestamp": "1711845568", + }, + "issuanceDate": "2024-01-10T04:44:29.563418Z", + }, +} + +VCDI_CRED_REQ = { + "data_model_version": "2.0", + "binding_proof": { + "anoncreds_link_secret": { + "entropy": "M7PyEDW7WfLDA8UH4BPhVN", + "cred_def_id": CRED_DEF_ID, + "blinded_ms": { + "u": "10047077609650450290609991930929594521921208780899757965398360086992099381832995073955506958821655372681970112562804577530208651675996528617262693958751195285371230790988741041496869140904046414320278189103736789305088489636024127715978439300785989247215275867951013255925809735479471883338351299180591011255281885961242995409072561940244771612447316409017677474822482928698183528232263803799926211692640155689629903898365777273000566450465466723659861801656618726777274689021162957914736922404694190070274236964163273886807208820068271673047750886130307545831668836096290655823576388755329367886670574352063509727295", + "ur": "1 10047077609650450290609991930929594521921208780899757965398360086992099381832995073955506958821655372681970112562804577530208651675996528617262693958751195285371230790988741041496869140904046414320278189103736789305088489636024127715978439300785989247215275867951013255925809735479471883338351299180591011255281885961242995409072561940244771612447316409017677474822482928698183528232263803799926211692640155689629903898365777273000566450465466723659861801656618726777274689021162957914736922404694190070274236964163273886807208820068271673047750886130307545831668836096290655823576388755329367886670574352063509727295", + "hidden_attributes": ["master_secret"], + "committed_attributes": {}, + }, + "blinded_ms_correctness_proof": { + "c": "114820252909341277169123380270435575009714169580229908332117664809097619479483", + "v_dash_cap": "2800797042446023854769298889946111553775800551626595767742635719512900304820391485829151945623333206184503230504182991047567531709613146606620747119977967362375975470346540769137309709302645176745785595101997824808807951935607979085748767054924474772855886854081455495367299835633316236603850924206877781343663290011630380243973434735740532318737134036990657225621660855862337569102753069931768335142276913795486645880476005516655059658346071702100939785144477050087370752056081492070366540039114009106296993876935692142991636251707934248460120048734266848191670576929279282843107501392282445417047087792806945845190270343938754413343820710137866411061071233755924209847337885397612906914410338546708562034035772917684", + "m_caps": { + "master_secret": "27860715812851216521476619601374576486949815748604240358820717458669963808683330226247784299086475972693040811857737248781118170534547319442287676278121026619110648625203644115424" + }, + "r_caps": {}, + }, + "nonce": "866186615577411311009479", + }, + "didcomm_signed_attachment": {"attachment_id": "test"}, + }, +} + +VCDI_CRED = { + "credential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/data-integrity/v2", + {"@vocab": "https://www.w3.org/ns/credentials/issuer-dependent#"}, + ], + "type": ["VerifiableCredential"], + "issuer": "LSTv7AUoTyfqFxZbuAGqKR", + "credentialSubject": { + "name": "Alice Smith", + "timestamp": "1711845568", + "birthdate_dateint": "20000331", + "date": "2018-05-28", + "degree": "Maths", + }, + "proof": [ + { + "cryptosuite": "anoncreds-2023", + "type": "DataIntegrityProof", + "proofPurpose": "assertionMethod", + "verificationMethod": "LSTv7AUoTyfqFxZbuAGqKR:3:CL:536199:faber.agent.degree_schema", + "proofValue": "ukgGEqXNjaGVtYV9pZNkvTFNUdjdBVW9UeWZxRnhaYnVBR3FLUjoyOmRlZ3JlZSBzY2hlbWE6MzUuNzguNzSrY3JlZF9kZWZfaWTZPExTVHY3QVVvVHlmcUZ4WmJ1QUdxS1I6MzpDTDo1MzYxOTk6ZmFiZXIuYWdlbnQuZGVncmVlX3NjaGVtYalzaWduYXR1cmWCrHBfY3JlZGVudGlhbISjbV8y3AAgzP_Ml8zXzOpNYcyXIxROzNHMvlk9zNnMsAkSzK7Mq0jMwxN0zMAMZMz4QszbzPkioWHcAQA3UnhCzPTM8syhS0vMjcygzJUAV8yYzLlAUcz-KszVEC0ADS7MhFMszIPMlczRf8zmd8ykzPxHzOzMkn02WA93WADM5czczOhlTk7M3szBR2jM_3bMpCnMmlIRWMy6zIQnzOkFei80ZljM_FY4zNHMn21izIBLzNDMuMyKzIYIzOh-zKF_Hcz0zNMTzNbMsczazJl4OsyBWsyMzJkrzJnMwMypzJdjzOksS05gzN7M4TfM3hrMix5HzLoyzI7Mt8z4zJIiRTHMysz-zIUidsyKzIrMn8yKzOfMuW3MoEXMsUjMzno6zNVheMyFzPMqFgk6zK1HI3HM-0bMwxcQzJDMysz-b8zMBSDMqsyQzMzMwEslF8z4zPfM5cyWzITMiMyCGQg6IzBVYszAG8z5zJtfzPVPZn5lzIzM3hctzMHM5My5F1LMhsyJzILMy1PM98z0fsyEJ1LMsMzzzN4tzMB5BVLMxsz5zNHMh8yYzKMgEzLMiUotzLLM-EYEzIshoWXcAEsQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASzPfMkcz8fn_M4syUOszEP215zNwLoXbcAVUIzLZHzLnMyiLM0czSUjzM_EJ-zLsmzKnMncyRzO5UFTfMpcynzIE6Vsz3EVnMx1EhG8yMbszKzNxadsyKzLPM3XhcZMyWIEzM81vM1nwdE8zVzN7Mzyd4zMjM78yjG0TMuWXMiMzIF3fMyRJdK8yczLYgIi3MwczOUczRNcyuRDzMhWBGPhnMiXbMp0PMj8z8zMgiVzk_HcyDPxjMmTDM917M88z4GczIacyfzLVCR2rM21zMjloMzI44zNsPzMrMnczIfVQXzPdYzLBuzPY5zIp9cMzaBRfM0j7MoAjMucysawHM3MyrzIrMz8ymzIvMl8zzH8yCfczhzMvM5nVUTAhDUhXMkRjM717M7szWO8y8IcyYAV3M6RnM0cyrPmXMmMyKzMsEzNzM4cyTY8zTzITMqsz3HcyMLMzjfXZCK8yZXMzIMsy3zMNazJPMjMzBzMBXPHYyzI5bKsyuGczzDcy4zJvMknYTzJHMpcyYzMjMtmPM-czuzMrMycz_zLfM7MzzY8yezL3MqnnM00wXJsy9zMfM08z-zMLMugE9YszzzLsTzIQrCwPMucyqzIHMmGpozOjMrl5OzOfM7cy1KEvMqxIsP1LM-ETM_8yHzIJFUMy5zMg_zIXMwQEdGcy_zKrMh8zpzKvM0szlzNdQzPNFzLwWzMrMvQkTzK5czJU7QUYarHJfY3JlZGVudGlhbMC7c2lnbmF0dXJlX2NvcnJlY3RuZXNzX3Byb29mgqJzZdwBAFTMpszvdjHM9hBoWczmzIJiOBXM9szdzM1-zK0zcR7M9hsLzNfMuXDM4mxLzJwsYC3MuMzKacz3zK5yGRPM_QUmCMylzNfM48zvzPDMzk7MuVxtMsyZzPIXeszgzOrM_My5zL3M9czJzLR7zLrMxEAfzKsBdR_M7WPM2gvM-FYNzMtZCsytzJkra8y3aXcczODMg8z5zIIaUXrM9sySV1XM2MyCdHgazILMv3htzJ9VMjtDKxNGFMz4OsyXf8zyZHHMx8ynO09-TGp_JczPVGsSfcygG8zvGQtnbnLMzCDMycyCR23M0szca2VRW0hSzKPMrMyWzK3M8EnM_BpmQXMNGszHzIDM0MywK2sQJszjzNHM-XbMki3M68yxzON8zNR6fMy8zOgKzOZazMrMzw9nzIvMrkXM4UAGdjDMvczPIjsscFYaC8ySdRtNzOTMyDcVFsyDLMygaldrT8zwzPDMzSkHzPPM_w4wVszAoWPcACDM9Mz1zOMmzIdgfDJPQMyjzNVqF0PM3syRzLliSMyRZcy9AW3MoWTMmh9pDszs", + } + ], + "issuanceDate": "2024-03-31T00:39:32.220900632Z", + } +} + + +class TestV20VCDICredFormatHandler(IsolatedAsyncioTestCase): + async def asyncSetUp(self): + self.session = InMemoryProfile.test_session() + self.profile = self.session.profile + self.context = self.profile.context + setattr(self.profile, "session", mock.MagicMock(return_value=self.session)) + + # Ledger + Ledger = mock.MagicMock() + self.ledger = Ledger() + self.ledger.get_schema = mock.CoroutineMock(return_value=SCHEMA) + self.ledger.get_credential_definition = mock.CoroutineMock( + return_value=CRED_DEF + ) + self.ledger.get_revoc_reg_def = mock.CoroutineMock(return_value=REV_REG_DEF) + self.ledger.__aenter__ = mock.CoroutineMock(return_value=self.ledger) + self.ledger.credential_definition_id2schema_id = mock.CoroutineMock( + return_value=SCHEMA_ID + ) + self.context.injector.bind_instance(BaseLedger, self.ledger) + self.context.injector.bind_instance( + IndyLedgerRequestsExecutor, + mock.MagicMock( + get_ledger_for_identifier=mock.CoroutineMock( + return_value=(None, self.ledger) + ) + ), + ) + # Context + self.cache = InMemoryCache() + self.context.injector.bind_instance(BaseCache, self.cache) + + # Issuer + self.issuer = mock.MagicMock(AnonCredsIssuer, autospec=True) + self.context.injector.bind_instance(AnonCredsIssuer, self.issuer) + + # Holder + self.holder = mock.MagicMock(AnonCredsHolder, autospec=True) + self.context.injector.bind_instance(AnonCredsHolder, self.holder) + + self.handler = VCDICredFormatHandler(self.profile) + + assert self.handler.profile + + async def test_validate_fields(self): + # Test correct data + self.handler.validate_fields(CRED_20_PROPOSAL, {"cred_def_id": CRED_DEF_ID}) + self.handler.validate_fields(CRED_20_OFFER, VCDI_OFFER) + # getting + self.handler.validate_fields(CRED_20_REQUEST, VCDI_CRED_REQ) + self.handler.validate_fields(CRED_20_ISSUE, VCDI_CRED) + + # test incorrect proposal + with self.assertRaises(ValidationError): + self.handler.validate_fields( + CRED_20_PROPOSAL, {"some_random_key": "some_random_value"} + ) + + # test incorrect offer + with self.assertRaises(ValidationError): + offer = VCDI_OFFER.copy() + offer.pop("binding_method") + self.handler.validate_fields(CRED_20_OFFER, offer) + + # test incorrect request + with self.assertRaises(ValidationError): + req = VCDI_CRED_REQ.copy() + req.pop("data_model_version") + self.handler.validate_fields(CRED_20_REQUEST, req) + + # test incorrect cred + with self.assertRaises(ValidationError): + cred = VCDI_CRED.copy() + cred.pop("credential") + self.handler.validate_fields(CRED_20_ISSUE, cred) + + async def test_get_vcdi_detail_record(self): + cred_ex_id = "dummy" + details_vcdi = [ + V20CredExRecordIndy( + cred_ex_id=cred_ex_id, + rev_reg_id="rr-id", + cred_rev_id="0", + ), + V20CredExRecordIndy( + cred_ex_id=cred_ex_id, + rev_reg_id="rr-id", + cred_rev_id="1", + ), + ] + await details_vcdi[0].save(self.session) + await details_vcdi[1].save(self.session) # exercise logger warning on get() + + with mock.patch.object( + VCDI_LOGGER, "warning", mock.MagicMock() + ) as mock_warning: + assert await self.handler.get_detail_record(cred_ex_id) in details_vcdi + mock_warning.assert_called_once() + + async def test_check_uniqueness(self): + with mock.patch.object( + self.handler.format.detail, + "query_by_cred_ex_id", + mock.CoroutineMock(), + ) as mock_vcdi_query: + mock_vcdi_query.return_value = [] + await self.handler._check_uniqueness("dummy-cx-id") + + with mock.patch.object( + self.handler.format.detail, + "query_by_cred_ex_id", + mock.CoroutineMock(), + ) as mock_vcdi_query: + mock_vcdi_query.return_value = [mock.MagicMock()] + with self.assertRaises(V20CredFormatError) as context: + await self.handler._check_uniqueness("dummy-cx-id") + assert "detail record already exists" in str(context.exception) + + async def test_create_proposal(self): + cred_ex_record = mock.MagicMock() + proposal_data = {"schema_id": SCHEMA_ID} + + (cred_format, attachment) = await self.handler.create_proposal( + cred_ex_record, proposal_data + ) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == proposal_data + + # assert data is encoded as base64 + assert attachment.data.base64 + + async def test_create_proposal_none(self): + cred_ex_record = mock.MagicMock() + proposal_data = None + + (cred_format, attachment) = await self.handler.create_proposal( + cred_ex_record, proposal_data + ) + + # assert content of attachment is proposal data + assert attachment.content == {} + + async def test_receive_proposal(self): + cred_ex_record = mock.MagicMock() + cred_proposal_message = mock.MagicMock() + + # Not much to assert. Receive proposal doesn't do anything + await self.handler.receive_proposal(cred_ex_record, cred_proposal_message) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_offer(self): + schema_id_parts = SCHEMA_ID.split(":") + + cred_preview = V20CredPreview( + attributes=( + V20CredAttrSpec(name="legalName", value="value"), + V20CredAttrSpec(name="jurisdictionId", value="value"), + V20CredAttrSpec(name="incorporationDate", value="value"), + ) + ) + + cred_proposal = V20CredProposal( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_PROPOSAL][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + filters_attach=[ + AttachDecorator.data_base64({"cred_def_id": CRED_DEF_ID}, ident="0") + ], + ) + + cred_def_record = StorageRecord( + CRED_DEF_SENT_RECORD_TYPE, + CRED_DEF_ID, + { + "schema_id": SCHEMA_ID, + "schema_issuer_did": schema_id_parts[0], + "schema_name": schema_id_parts[-2], + "schema_version": schema_id_parts[-1], + "issuer_did": TEST_DID, + "cred_def_id": CRED_DEF_ID, + "epoch": str(int(time())), + }, + ) + await self.session.storage.add_record(cred_def_record) + + self.issuer.create_credential = mock.CoroutineMock( + return_value=json.dumps(VCDI_OFFER) + ) + + (cred_format, attachment) = await self.handler.create_offer(cred_proposal) + + self.issuer.create_credential.assert_called_once_with(CRED_DEF_ID) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == VCDI_OFFER + + # assert data is encoded as base64 + assert attachment.data.base64 + + self.issuer.create_credential_offer.reset_mock() + (cred_format, attachment) = await self.handler.create_offer(cred_proposal) + self.issuer.create_credential_offer.assert_not_called() + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_receive_offer(self): + cred_ex_record = mock.MagicMock() + cred_offer_message = mock.MagicMock() + + # Not much to assert. Receive offer doesn't do anything + await self.handler.receive_offer(cred_ex_record, cred_offer_message) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_create_request(self): + + holder_did = "did" + + cred_offer = V20CredOffer( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + # TODO here + offers_attach=[AttachDecorator.data_base64(VCDI_OFFER, ident="0")], + ) + cred_ex_record = V20CredExRecordIndy( + cred_ex_id="dummy-id", + state=V20CredExRecordIndy.STATE_OFFER_RECEIVED, + cred_offer=cred_offer.serialize(), + ) + + cred_def = {"cred": "def"} + self.ledger.get_credential_definition = mock.CoroutineMock( + return_value=cred_def + ) + + cred_req_meta = {} + self.holder.create_credential_request = mock.CoroutineMock( + return_value=(json.dumps(VCDI_CRED_REQ), json.dumps(cred_req_meta)) + ) + + (cred_format, attachment) = await self.handler.create_request( + cred_ex_record, {"holder_did": holder_did} + ) + + self.holder.create_credential_request.assert_called_once_with( + VCDI_OFFER, cred_def, holder_did + ) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == VCDI_CRED_REQ + + # assert data is encoded as base64 + assert attachment.data.base64 + + # cover case with cache (change ID to prevent already exists error) + cred_ex_record._id = "dummy-id2" + await self.handler.create_request(cred_ex_record, {"holder_did": holder_did}) + + # cover case with no cache in injection context + self.context.injector.clear_binding(BaseCache) + cred_ex_record._id = "dummy-id3" + self.context.injector.bind_instance( + BaseMultitenantManager, + mock.MagicMock(MultitenantManager, autospec=True), + ) + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=(None, self.ledger)), + ): + await self.handler.create_request( + cred_ex_record, {"holder_did": holder_did} + ) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_receive_request(self): + cred_ex_record = mock.MagicMock() + cred_request_message = mock.MagicMock() + + # Not much to assert. Receive request doesn't do anything + await self.handler.receive_request(cred_ex_record, cred_request_message) + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_issue_credential_revocable(self): + attr_values = { + "legalName": "value", + "jurisdictionId": "value", + "incorporationDate": "value", + } + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() + ] + ) + cred_offer = V20CredOffer( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + # TODO here + offers_attach=[AttachDecorator.data_base64(VCDI_OFFER, ident="0")], + ) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + # TODO here + requests_attach=[AttachDecorator.data_base64(VCDI_CRED_REQ, ident="0")], + ) + + cred_ex_record = V20CredExRecordIndy( + cred_ex_id="dummy-cxid", + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + initiator=V20CredExRecordIndy.INITIATOR_SELF, + role=V20CredExRecordIndy.ROLE_ISSUER, + state=V20CredExRecordIndy.STATE_REQUEST_RECEIVED, + ) + + cred_rev_id = "1000" + self.issuer.create_credential = mock.CoroutineMock( + return_value=(json.dumps(VCDI_CRED), cred_rev_id) + ) + + with mock.patch.object(test_module, "IndyRevocation", autospec=True) as revoc: + revoc.return_value.get_or_create_active_registry = mock.CoroutineMock( + return_value=( + mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + ), + mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(mock.CoroutineMock()), + max_creds=10, + ), + ) + ) + + (cred_format, attachment) = await self.handler.issue_credential( + cred_ex_record, retries=1 + ) + + self.issuer.create_credential.assert_called_once_with( + SCHEMA, + VCDI_OFFER, + VCDI_CRED_REQ, + attr_values, + REV_REG_ID, + "dummy-path", + ) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == VCDI_CRED + + # assert data is encoded as base64 + assert attachment.data.base64 + + @pytest.mark.skip(reason="Anoncreds-break") + async def test_issue_credential_non_revocable(self): + CRED_DEF_NR = deepcopy(CRED_DEF) + CRED_DEF_NR["value"]["revocation"] = None + attr_values = { + "legalName": "value", + "jurisdictionId": "value", + "incorporationDate": "value", + } + cred_preview = V20CredPreview( + attributes=[ + V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() + ] + ) + cred_offer = V20CredOffer( + credential_preview=cred_preview, + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + # TODO here + offers_attach=[AttachDecorator.data_base64(VCDI_OFFER, ident="0")], + ) + cred_request = V20CredRequest( + formats=[ + V20CredFormat( + attach_id="0", + format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ + V20CredFormat.Format.VC_DI.api + ], + ) + ], + requests_attach=[AttachDecorator.data_base64(VCDI_CRED_REQ, ident="0")], + ) + + cred_ex_record = V20CredExRecordIndy( + cred_ex_id="dummy-cxid", + cred_offer=cred_offer.serialize(), + cred_request=cred_request.serialize(), + initiator=V20CredExRecordIndy.INITIATOR_SELF, + role=V20CredExRecordIndy.ROLE_ISSUER, + state=V20CredExRecordIndy.STATE_REQUEST_RECEIVED, + ) + + self.issuer.create_credential = mock.CoroutineMock( + return_value=(json.dumps(VCDI_CRED), None) + ) + self.ledger.get_credential_definition = mock.CoroutineMock( + return_value=CRED_DEF_NR + ) + self.context.injector.bind_instance( + BaseMultitenantManager, + mock.MagicMock(MultitenantManager, autospec=True), + ) + with mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + mock.CoroutineMock(return_value=("test_ledger_id", self.ledger)), + ): + (cred_format, attachment) = await self.handler.issue_credential( + cred_ex_record, retries=0 + ) + + self.issuer.create_credential.assert_called_once_with( + SCHEMA, + VCDI_OFFER, + VCDI_CRED_REQ, + attr_values, + None, + None, + ) + + # assert identifier match + assert cred_format.attach_id == self.handler.format.api == attachment.ident + + # assert content of attachment is proposal data + assert attachment.content == VCDI_CRED + + # assert data is encoded as base64 + assert attachment.data.base64 diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py b/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py index 33c7241ae1..aab8fd6ee7 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/message_types.py @@ -43,18 +43,22 @@ CRED_20_PROPOSAL: { V20CredFormat.Format.INDY.api: "hlindy/cred-filter@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc-detail@v1.0", + V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc@v0.1", }, CRED_20_OFFER: { V20CredFormat.Format.INDY.api: "hlindy/cred-abstract@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc-detail@v1.0", + V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc-offer@v0.1", }, CRED_20_REQUEST: { V20CredFormat.Format.INDY.api: "hlindy/cred-req@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc-detail@v1.0", + V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc-request@v0.1", }, CRED_20_ISSUE: { V20CredFormat.Format.INDY.api: "hlindy/cred@v2.0", V20CredFormat.Format.LD_PROOF.api: "aries/ld-proof-vc@v1.0", + V20CredFormat.Format.VC_DI.api: "didcomm/w3c-di-vc@v0.1", }, } diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py b/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py index 374a188a3a..a9d6253ce7 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/messages/cred_format.py @@ -61,6 +61,15 @@ class Format(Enum): ), ) + VC_DI = FormatSpec( + "didcomm/", + V20CredExRecordIndy, + DeferLoad( + "aries_cloudagent.protocols.issue_credential.v2_0" + ".formats.vc_di.handler.VCDICredFormatHandler" + ), + ) + @classmethod def get(cls, label: Union[str, "V20CredFormat.Format"]): """Get format enum for label.""" diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/routes.py b/aries_cloudagent/protocols/issue_credential/v2_0/routes.py index 1341ae4dc2..bf15b91df2 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/routes.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/routes.py @@ -14,6 +14,7 @@ ) from marshmallow import ValidationError, fields, validate, validates_schema +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....anoncreds.holder import AnonCredsHolderError from ....anoncreds.issuer import AnonCredsIssuerError @@ -108,6 +109,7 @@ class V20CredExRecordDetailSchema(OpenAPISchema): indy = fields.Nested(V20CredExRecordIndySchema, required=False) ld_proof = fields.Nested(V20CredExRecordLDProofSchema, required=False) + vc_di = fields.Nested(V20CredExRecordSchema, required=False) class V20CredExRecordListResultSchema(OpenAPISchema): @@ -169,6 +171,46 @@ class V20CredFilterIndySchema(OpenAPISchema): ) +class V20CredFilterVCDISchema(OpenAPISchema): + """VCDI credential filtration criteria.""" + + cred_def_id = fields.Str( + required=False, + validate=INDY_CRED_DEF_ID_VALIDATE, + metadata={ + "description": "Credential definition identifier", + "example": INDY_CRED_DEF_ID_EXAMPLE, + }, + ) + schema_id = fields.Str( + required=False, + validate=INDY_SCHEMA_ID_VALIDATE, + metadata={ + "description": "Schema identifier", + "example": INDY_SCHEMA_ID_EXAMPLE, + }, + ) + schema_issuer_did = fields.Str( + required=False, + validate=INDY_DID_VALIDATE, + metadata={"description": "Schema issuer DID", "example": INDY_DID_EXAMPLE}, + ) + schema_name = fields.Str( + required=False, + metadata={"description": "Schema name", "example": "preferences"}, + ) + schema_version = fields.Str( + required=False, + validate=INDY_VERSION_VALIDATE, + metadata={"description": "Schema version", "example": INDY_VERSION_EXAMPLE}, + ) + issuer_did = fields.Str( + required=False, + validate=INDY_DID_VALIDATE, + metadata={"description": "Credential issuer DID", "example": INDY_DID_EXAMPLE}, + ) + + class V20CredFilterSchema(OpenAPISchema): """Credential filtration criteria.""" @@ -182,12 +224,17 @@ class V20CredFilterSchema(OpenAPISchema): required=False, metadata={"description": "Credential filter for linked data proof"}, ) + vc_di = fields.Nested( + V20CredFilterVCDISchema, + required=False, + metadata={"description": "Credential filter for vc_di"}, + ) @validates_schema def validate_fields(self, data, **kwargs): """Validate schema fields. - Data must have indy, ld_proof, or both. + Data must have indy, ld_proof, vc_di, or all. Args: data: The data to validate @@ -198,7 +245,7 @@ def validate_fields(self, data, **kwargs): """ if not any(f.api in data for f in V20CredFormat.Format): raise ValidationError( - "V20CredFilterSchema requires indy, ld_proof, or both" + "V20CredFilterSchema requires indy, ld_proof, vc_di or all" ) @@ -239,11 +286,13 @@ class V20IssueCredSchemaCore(AdminAPIMessageTracingSchema): @validates_schema def validate(self, data, **kwargs): - """Make sure preview is present when indy format is present.""" + """Make sure preview is present when indy/vc_di format is present.""" - if data.get("filter", {}).get("indy") and not data.get("credential_preview"): + if ( + data.get("filter", {}).get("indy") or data.get("filter", {}).get("vc_di") + ) and not data.get("credential_preview"): raise ValidationError( - "Credential preview is required if indy filter is present" + "Credential preview is required if indy or vc_di filter is present" ) @@ -495,6 +544,7 @@ def _format_result_with_details( ) @querystring_schema(V20CredExRecordListQueryStringSchema) @response_schema(V20CredExRecordListResultSchema(), 200, description="") +@tenant_authentication async def credential_exchange_list(request: web.BaseRequest): """Request handler for searching credential exchange records. @@ -542,6 +592,7 @@ async def credential_exchange_list(request: web.BaseRequest): ) @match_info_schema(V20CredExIdMatchInfoSchema()) @response_schema(V20CredExRecordDetailSchema(), 200, description="") +@tenant_authentication async def credential_exchange_retrieve(request: web.BaseRequest): """Request handler for fetching single credential exchange record. @@ -589,6 +640,7 @@ async def credential_exchange_retrieve(request: web.BaseRequest): ) @request_schema(V20IssueCredSchemaCore()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_create(request: web.BaseRequest): """Request handler for creating a credential from attr values. @@ -665,6 +717,7 @@ async def credential_exchange_create(request: web.BaseRequest): ) @request_schema(V20CredExFreeSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send(request: web.BaseRequest): """Request handler for sending credential from issuer to holder from attr values. @@ -781,6 +834,7 @@ async def credential_exchange_send(request: web.BaseRequest): ) @request_schema(V20CredExFreeSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_proposal(request: web.BaseRequest): """Request handler for sending credential proposal. @@ -907,6 +961,7 @@ async def _create_free_offer( ) @request_schema(V20CredOfferConnFreeRequestSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_create_free_offer(request: web.BaseRequest): """Request handler for creating free credential offer. @@ -979,6 +1034,7 @@ async def credential_exchange_create_free_offer(request: web.BaseRequest): ) @request_schema(V20CredOfferRequestSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_free_offer(request: web.BaseRequest): """Request handler for sending free credential offer. @@ -1071,6 +1127,7 @@ async def credential_exchange_send_free_offer(request: web.BaseRequest): @match_info_schema(V20CredExIdMatchInfoSchema()) @request_schema(V20CredBoundOfferRequestSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_bound_offer(request: web.BaseRequest): """Request handler for sending bound credential offer. @@ -1182,6 +1239,7 @@ async def credential_exchange_send_bound_offer(request: web.BaseRequest): ) @request_schema(V20CredRequestFreeSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_free_request(request: web.BaseRequest): """Request handler for sending free credential request. @@ -1280,6 +1338,7 @@ async def credential_exchange_send_free_request(request: web.BaseRequest): @match_info_schema(V20CredExIdMatchInfoSchema()) @request_schema(V20CredRequestRequestSchema()) @response_schema(V20CredExRecordSchema(), 200, description="") +@tenant_authentication async def credential_exchange_send_bound_request(request: web.BaseRequest): """Request handler for sending credential request. @@ -1399,6 +1458,7 @@ async def credential_exchange_send_bound_request(request: web.BaseRequest): @match_info_schema(V20CredExIdMatchInfoSchema()) @request_schema(V20CredIssueRequestSchema()) @response_schema(V20CredExRecordDetailSchema(), 200, description="") +@tenant_authentication async def credential_exchange_issue(request: web.BaseRequest): """Request handler for sending credential. @@ -1493,6 +1553,7 @@ async def credential_exchange_issue(request: web.BaseRequest): @match_info_schema(V20CredExIdMatchInfoSchema()) @request_schema(V20CredStoreRequestSchema()) @response_schema(V20CredExRecordDetailSchema(), 200, description="") +@tenant_authentication async def credential_exchange_store(request: web.BaseRequest): """Request handler for storing credential. @@ -1596,6 +1657,7 @@ async def credential_exchange_store(request: web.BaseRequest): ) @match_info_schema(V20CredExIdMatchInfoSchema()) @response_schema(V20IssueCredentialModuleResponseSchema(), 200, description="") +@tenant_authentication async def credential_exchange_remove(request: web.BaseRequest): """Request handler for removing a credential exchange record. @@ -1624,6 +1686,7 @@ async def credential_exchange_remove(request: web.BaseRequest): @match_info_schema(V20CredExIdMatchInfoSchema()) @request_schema(V20CredIssueProblemReportRequestSchema()) @response_schema(V20IssueCredentialModuleResponseSchema(), 200, description="") +@tenant_authentication async def credential_exchange_problem_report(request: web.BaseRequest): """Request handler for sending problem report. diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py index 0be8b63429..e25e088635 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/tests/test_routes.py @@ -1,14 +1,14 @@ -from .....vc.ld_proofs.error import LinkedDataProofException -from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase -from .....admin.request_context import AdminRequestContext +from aries_cloudagent.tests import mock +from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile +from .....vc.ld_proofs.error import LinkedDataProofException from .. import routes as test_module from ..formats.indy.handler import IndyCredFormatHandler from ..formats.ld_proof.handler import LDProofCredFormatHandler from ..messages.cred_format import V20CredFormat - from . import ( LD_PROOF_VC_DETAIL, TEST_DID, @@ -18,7 +18,12 @@ class TestV20CredRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -28,6 +33,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_validate_cred_filter_schema(self): @@ -105,6 +111,7 @@ async def test_credential_exchange_list(self): "cred_ex_record": mock_cx_rec.serialize.return_value, "indy": None, "ld_proof": None, + "vc_di": None, } ] } @@ -150,6 +157,7 @@ async def test_credential_exchange_retrieve(self): serialize=mock.MagicMock(return_value={"...": "..."}) ), None, # ld_proof + None, # vc_di ] ) @@ -160,6 +168,7 @@ async def test_credential_exchange_retrieve(self): "cred_ex_record": mock_cx_rec.serialize.return_value, "indy": {"...": "..."}, "ld_proof": None, + "vc_di": None, } ) @@ -186,6 +195,9 @@ async def test_credential_exchange_retrieve_indy_ld_proof(self): mock.MagicMock( # ld_proof serialize=mock.MagicMock(return_value={"ld": "proof"}) ), + mock.MagicMock( # vc_di + serialize=mock.MagicMock(return_value={"vc": "di"}) + ), ] ) @@ -196,6 +208,7 @@ async def test_credential_exchange_retrieve_indy_ld_proof(self): "cred_ex_record": mock_cx_rec.serialize.return_value, "indy": {"in": "dy"}, "ld_proof": {"ld": "proof"}, + "vc_di": {"vc": "di"}, } ) @@ -683,6 +696,48 @@ async def test_credential_exchange_send_free_offer(self): await test_module.credential_exchange_send_free_offer(self.request) mock_response.assert_called_once_with(mock_cx_rec.serialize.return_value) + async def test_credential_exchange_send_free_offer_vcdi(self): + self.request.json = mock.CoroutineMock( + return_value={ + "filter": {"vc_di": {"schema_version": "1.0"}}, + "comment": "This is a test comment.", + "auto_issue": True, + "auto-remove": True, + "replacement_id": "test_replacement_id", + "credential_preview": { + "@type": "https://didcomm.org/issue-credential/2.0/credential-preview", + "attributes": [ + {"name": "name", "value": "Alice Smith"}, + {"name": "date", "value": "2018-05-28"}, + {"name": "degree", "value": "Maths"}, + {"name": "birthdate_dateint", "value": "20000330"}, + {"name": "timestamp", "value": "1711836271"}, + ], + }, + } + ) + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec, mock.patch.object( + test_module, "V20CredManager", autospec=True + ) as mock_cred_mgr, mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + # Mock the creation of a credential offer, especially for handling VC-DI + mock_cred_mgr.return_value.create_offer = mock.CoroutineMock() + mock_cx_rec = mock.MagicMock() + mock_cred_mgr.return_value.create_offer.return_value = ( + mock_cx_rec, + mock.MagicMock(), + ) + + # Call the function you are testing + await test_module.credential_exchange_send_free_offer(self.request) + + # Validate that the response is correctly structured and called once + mock_response.assert_called_once_with(mock_cx_rec.serialize.return_value) + async def test_credential_exchange_send_free_offer_no_filter(self): self.request.json = mock.CoroutineMock( return_value={ @@ -980,6 +1035,35 @@ async def test_credential_exchange_send_request(self): mock_response.assert_called_once_with(mock_cx_rec.serialize.return_value) + async def test_credential_exchange_send_request_vcdi(self): + self.request.json = mock.CoroutineMock() + self.request.match_info = {"cred_ex_id": "dummy"} + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec, mock.patch.object( + test_module, "V20CredManager", autospec=True + ) as mock_cred_mgr, mock.patch.object( + test_module, "V20CredExRecord", autospec=True + ) as mock_cx_rec_cls, mock.patch.object( + test_module.web, "json_response" + ) as mock_response: + mock_cx_rec_cls.retrieve_by_id = mock.CoroutineMock() + mock_cx_rec_cls.retrieve_by_id.return_value.state = ( + test_module.V20CredExRecord.STATE_OFFER_RECEIVED + ) + + mock_cx_rec = mock.MagicMock() + + mock_cred_mgr.return_value.create_request.return_value = ( + mock_cx_rec, + mock.MagicMock(), + ) + + await test_module.credential_exchange_send_bound_request(self.request) + + mock_response.assert_called_once_with(mock_cx_rec.serialize.return_value) + async def test_credential_exchange_send_request_bad_cred_ex_id(self): self.request.json = mock.CoroutineMock() self.request.match_info = {"cred_ex_id": "dummy"} @@ -1199,6 +1283,7 @@ async def test_credential_exchange_issue(self): serialize=mock.MagicMock(return_value={"...": "..."}) ), None, # ld_proof + None, # vc_di ] ) @@ -1214,6 +1299,54 @@ async def test_credential_exchange_issue(self): "cred_ex_record": mock_cx_rec.serialize.return_value, "indy": {"...": "..."}, "ld_proof": None, + "vc_di": None, + } + ) + + async def test_credential_exchange_issue_vcdi(self): + self.request.json = mock.CoroutineMock() + self.request.match_info = {"cred_ex_id": "dummy"} + + with mock.patch.object( + test_module, "ConnRecord", autospec=True + ) as mock_conn_rec, mock.patch.object( + test_module, "V20CredManager", autospec=True + ) as mock_cred_mgr, mock.patch.object( + test_module, "V20CredExRecord", autospec=True + ) as mock_cx_rec_cls, mock.patch.object( + test_module.web, "json_response" + ) as mock_response, mock.patch.object( + V20CredFormat.Format, "handler" + ) as mock_handler: + mock_cx_rec_cls.retrieve_by_id = mock.CoroutineMock() + mock_cx_rec_cls.retrieve_by_id.return_value.state = ( + test_module.V20CredExRecord.STATE_REQUEST_RECEIVED + ) + mock_cx_rec = mock.MagicMock() + + mock_handler.return_value.get_detail_record = mock.CoroutineMock( + side_effect=[ + None, + None, # ld_proof + mock.MagicMock( # indy + serialize=mock.MagicMock(return_value={"...": "..."}) + ), # vc_di + ] + ) + + mock_cred_mgr.return_value.issue_credential.return_value = ( + mock_cx_rec, + mock.MagicMock(), + ) + + await test_module.credential_exchange_issue(self.request) + + mock_response.assert_called_once_with( + { + "cred_ex_record": mock_cx_rec.serialize.return_value, + "indy": None, + "ld_proof": None, + "vc_di": {"...": "..."}, } ) @@ -1424,6 +1557,7 @@ async def test_credential_exchange_store(self): serialize=mock.MagicMock(return_value={"...": "..."}) ), None, # ld_proof + None, # vc_di ] ) @@ -1442,6 +1576,7 @@ async def test_credential_exchange_store(self): "cred_ex_record": mock_cx_rec.serialize.return_value, "indy": {"...": "..."}, "ld_proof": None, + "vc_di": None, } ) @@ -1489,6 +1624,7 @@ async def test_credential_exchange_store_bad_cred_id_json(self): "cred_ex_record": mock_cx_rec.serialize.return_value, "indy": {"...": "..."}, "ld_proof": None, + "vc_di": None, } ) @@ -1595,6 +1731,7 @@ async def test_credential_exchange_store_x(self): serialize=mock.MagicMock(return_value={"...": "..."}) ), None, # ld_proof + None, # vc_di ] ) diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/routes.py b/aries_cloudagent/protocols/out_of_band/v1_0/routes.py index 96aeea265a..7fcd42c1e4 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/routes.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/routes.py @@ -6,14 +6,15 @@ from aiohttp import web from aiohttp_apispec import ( docs, + match_info_schema, querystring_schema, request_schema, - match_info_schema, response_schema, ) from marshmallow import fields, validate from marshmallow.exceptions import ValidationError +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....messaging.models.base import BaseModelError from ....messaging.models.openapi import OpenAPISchema @@ -225,6 +226,7 @@ class InvitationRecordMatchInfoSchema(OpenAPISchema): @querystring_schema(InvitationCreateQueryStringSchema()) @request_schema(InvitationCreateRequestSchema()) @response_schema(InvitationRecordSchema(), description="") +@tenant_authentication async def invitation_create(request: web.BaseRequest): """Request handler for creating a new connection invitation. @@ -293,6 +295,7 @@ async def invitation_create(request: web.BaseRequest): @querystring_schema(InvitationReceiveQueryStringSchema()) @request_schema(InvitationMessageSchema()) @response_schema(OobRecordSchema(), 200, description="") +@tenant_authentication async def invitation_receive(request: web.BaseRequest): """Request handler for receiving a new connection invitation. @@ -337,6 +340,7 @@ async def invitation_receive(request: web.BaseRequest): @docs(tags=["out-of-band"], summary="Delete records associated with invitation") @match_info_schema(InvitationRecordMatchInfoSchema()) @response_schema(InvitationRecordResponseSchema(), description="") +@tenant_authentication async def invitation_remove(request: web.BaseRequest): """Request handler for removing a invitation related conn and oob records. diff --git a/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py index 7a9384f1cc..fa61be97a2 100644 --- a/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/out_of_band/v1_0/tests/test_routes.py @@ -1,16 +1,20 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext from .....connections.models.conn_record import ConnRecord from .....core.in_memory import InMemoryProfile - from .. import routes as test_module class TestOutOfBandRoutes(IsolatedAsyncioTestCase): async def asyncSetUp(self): - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.context = AdminRequestContext.test_context(profile=self.profile) self.request_dict = { "context": self.context, @@ -21,6 +25,7 @@ async def asyncSetUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_invitation_create(self): diff --git a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py index cc7ae20645..d383de8da5 100644 --- a/aries_cloudagent/protocols/present_proof/dif/pres_exch.py +++ b/aries_cloudagent/protocols/present_proof/dif/pres_exch.py @@ -217,7 +217,7 @@ class Meta: @pre_load def extract_info(self, data, **kwargs): - """deserialize.""" + """Deserialize.""" new_data = {} if isinstance(data, dict): if "uri_groups" in data: @@ -824,12 +824,10 @@ class Meta: id = fields.Str( required=False, - validate=UUID4_VALIDATE, metadata={"description": "ID", "example": UUID4_EXAMPLE}, ) definition_id = fields.Str( required=False, - validate=UUID4_VALIDATE, metadata={"description": "DefinitionID", "example": UUID4_EXAMPLE}, ) descriptor_maps = fields.List( diff --git a/aries_cloudagent/protocols/present_proof/v1_0/routes.py b/aries_cloudagent/protocols/present_proof/v1_0/routes.py index a606e0ff89..3cf4ae38ee 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/routes.py @@ -10,9 +10,9 @@ request_schema, response_schema, ) - from marshmallow import fields, validate +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....indy.holder import IndyHolder, IndyHolderError @@ -289,6 +289,7 @@ class V10PresExIdMatchInfoSchema(OpenAPISchema): ) @querystring_schema(V10PresentationExchangeListQueryStringSchema) @response_schema(V10PresentationExchangeListSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_list(request: web.BaseRequest): """Request handler for searching presentation exchange records. @@ -330,6 +331,7 @@ async def presentation_exchange_list(request: web.BaseRequest): ) @match_info_schema(V10PresExIdMatchInfoSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_retrieve(request: web.BaseRequest): """Request handler for fetching a single presentation exchange record. @@ -379,6 +381,7 @@ async def presentation_exchange_retrieve(request: web.BaseRequest): @match_info_schema(V10PresExIdMatchInfoSchema()) @querystring_schema(CredentialsFetchQueryStringSchema()) @response_schema(IndyCredPrecisSchema(many=True), 200, description="") +@tenant_authentication async def presentation_exchange_credentials_list(request: web.BaseRequest): """Request handler for searching applicable credential records. @@ -459,6 +462,7 @@ async def presentation_exchange_credentials_list(request: web.BaseRequest): ) @request_schema(V10PresentationProposalRequestSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_send_proposal(request: web.BaseRequest): """Request handler for sending a presentation proposal. @@ -543,6 +547,7 @@ async def presentation_exchange_send_proposal(request: web.BaseRequest): ) @request_schema(V10PresentationCreateRequestRequestSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_create_request(request: web.BaseRequest): """Request handler for creating a free presentation request. @@ -621,6 +626,7 @@ async def presentation_exchange_create_request(request: web.BaseRequest): ) @request_schema(V10PresentationSendRequestRequestSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_send_free_request(request: web.BaseRequest): """Request handler for sending a presentation request free from any proposal. @@ -710,6 +716,7 @@ async def presentation_exchange_send_free_request(request: web.BaseRequest): @match_info_schema(V10PresExIdMatchInfoSchema()) @request_schema(V10PresentationSendRequestToProposalSchema()) @response_schema(V10PresentationExchangeSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_send_bound_request(request: web.BaseRequest): """Request handler for sending a presentation request bound to a proposal. @@ -806,6 +813,7 @@ async def presentation_exchange_send_bound_request(request: web.BaseRequest): @match_info_schema(V10PresExIdMatchInfoSchema()) @request_schema(V10PresentationSendRequestSchema()) @response_schema(V10PresentationExchangeSchema(), description="") +@tenant_authentication async def presentation_exchange_send_presentation(request: web.BaseRequest): """Request handler for sending a presentation. @@ -923,6 +931,7 @@ async def presentation_exchange_send_presentation(request: web.BaseRequest): ) @match_info_schema(V10PresExIdMatchInfoSchema()) @response_schema(V10PresentationExchangeSchema(), description="") +@tenant_authentication async def presentation_exchange_verify_presentation(request: web.BaseRequest): """Request handler for verifying a presentation request. @@ -998,6 +1007,7 @@ async def presentation_exchange_verify_presentation(request: web.BaseRequest): @match_info_schema(V10PresExIdMatchInfoSchema()) @request_schema(V10PresentationProblemReportRequestSchema()) @response_schema(V10PresentProofModuleResponseSchema(), 200, description="") +@tenant_authentication async def presentation_exchange_problem_report(request: web.BaseRequest): """Request handler for sending problem report. @@ -1039,6 +1049,7 @@ async def presentation_exchange_problem_report(request: web.BaseRequest): ) @match_info_schema(V10PresExIdMatchInfoSchema()) @response_schema(V10PresentProofModuleResponseSchema(), description="") +@tenant_authentication async def presentation_exchange_remove(request: web.BaseRequest): """Request handler for removing a presentation exchange record. diff --git a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py index ca3d8e6927..9b5889b973 100644 --- a/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v1_0/tests/test_routes.py @@ -1,23 +1,28 @@ import importlib - -from aries_cloudagent.tests import mock from unittest import IsolatedAsyncioTestCase from marshmallow import ValidationError +from aries_cloudagent.tests import mock + from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....indy.holder import IndyHolder from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....indy.verifier import IndyVerifier from .....ledger.base import BaseLedger from .....storage.error import StorageNotFoundError - from .. import routes as test_module class TestProofRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.context = AdminRequestContext.test_context() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(profile=profile) self.profile = self.context.profile self.request_dict = { "context": self.context, @@ -28,6 +33,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_validate_proof_req_attr_spec(self): diff --git a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py index af8e6f00de..82726993bd 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/formats/dif/handler.py @@ -5,7 +5,6 @@ from typing import Mapping, Optional, Sequence, Tuple from uuid import uuid4 -from marshmallow import RAISE from ......messaging.base_handler import BaseResponder from ......messaging.decorators.attach_decorator import AttachDecorator @@ -75,7 +74,7 @@ def validate_fields(cls, message_type: str, attachment_data: Mapping): Schema = mapping[message_type] # Validate, throw if not valid - Schema(unknown=RAISE).load(attachment_data) + Schema().load(attachment_data) def get_format_identifier(self, message_type: str) -> str: """Get attachment format identifier for format and message combination. diff --git a/aries_cloudagent/protocols/present_proof/v2_0/routes.py b/aries_cloudagent/protocols/present_proof/v2_0/routes.py index 22ec098c33..943494522d 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/routes.py @@ -11,12 +11,12 @@ request_schema, response_schema, ) - from marshmallow import ValidationError, fields, validate, validates_schema +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext -from ....connections.models.conn_record import ConnRecord from ....anoncreds.holder import AnonCredsHolder, AnonCredsHolderError +from ....connections.models.conn_record import ConnRecord from ....indy.holder import IndyHolder, IndyHolderError from ....indy.models.cred_precis import IndyCredPrecisSchema from ....indy.models.proof import IndyPresSpecSchema @@ -425,6 +425,7 @@ def _formats_attach(by_format: Mapping, msg_type: str, spec: str) -> Mapping: @docs(tags=["present-proof v2.0"], summary="Fetch all present-proof exchange records") @querystring_schema(V20PresExRecordListQueryStringSchema) @response_schema(V20PresExRecordListSchema(), 200, description="") +@tenant_authentication async def present_proof_list(request: web.BaseRequest): """Request handler for searching presentation exchange records. @@ -467,6 +468,7 @@ async def present_proof_list(request: web.BaseRequest): ) @match_info_schema(V20PresExIdMatchInfoSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") +@tenant_authentication async def present_proof_retrieve(request: web.BaseRequest): """Request handler for fetching a single presentation exchange record. @@ -513,6 +515,7 @@ async def present_proof_retrieve(request: web.BaseRequest): @match_info_schema(V20PresExIdMatchInfoSchema()) @querystring_schema(V20CredentialsFetchQueryStringSchema()) @response_schema(IndyCredPrecisSchema(many=True), 200, description="") +@tenant_authentication async def present_proof_credentials_list(request: web.BaseRequest): """Request handler for searching applicable credential records. @@ -802,6 +805,7 @@ async def retrieve_uri_list_from_schema_filter( @docs(tags=["present-proof v2.0"], summary="Sends a presentation proposal") @request_schema(V20PresProposalRequestSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") +@tenant_authentication async def present_proof_send_proposal(request: web.BaseRequest): """Request handler for sending a presentation proposal. @@ -884,6 +888,7 @@ async def present_proof_send_proposal(request: web.BaseRequest): ) @request_schema(V20PresCreateRequestRequestSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") +@tenant_authentication async def present_proof_create_request(request: web.BaseRequest): """Request handler for creating a free presentation request. @@ -960,6 +965,7 @@ async def present_proof_create_request(request: web.BaseRequest): ) @request_schema(V20PresSendRequestRequestSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") +@tenant_authentication async def present_proof_send_free_request(request: web.BaseRequest): """Request handler for sending a presentation request free from any proposal. @@ -1043,6 +1049,7 @@ async def present_proof_send_free_request(request: web.BaseRequest): @match_info_schema(V20PresExIdMatchInfoSchema()) @request_schema(V20PresentationSendRequestToProposalSchema()) @response_schema(V20PresExRecordSchema(), 200, description="") +@tenant_authentication async def present_proof_send_bound_request(request: web.BaseRequest): """Request handler for sending a presentation request bound to a proposal. @@ -1133,6 +1140,7 @@ async def present_proof_send_bound_request(request: web.BaseRequest): @match_info_schema(V20PresExIdMatchInfoSchema()) @request_schema(V20PresSpecByFormatRequestSchema()) @response_schema(V20PresExRecordSchema(), description="") +@tenant_authentication async def present_proof_send_presentation(request: web.BaseRequest): """Request handler for sending a presentation. @@ -1246,6 +1254,7 @@ async def present_proof_send_presentation(request: web.BaseRequest): @docs(tags=["present-proof v2.0"], summary="Verify a received presentation") @match_info_schema(V20PresExIdMatchInfoSchema()) @response_schema(V20PresExRecordSchema(), description="") +@tenant_authentication async def present_proof_verify_presentation(request: web.BaseRequest): """Request handler for verifying a presentation request. @@ -1314,6 +1323,7 @@ async def present_proof_verify_presentation(request: web.BaseRequest): @match_info_schema(V20PresExIdMatchInfoSchema()) @request_schema(V20PresProblemReportRequestSchema()) @response_schema(V20PresentProofModuleResponseSchema(), 200, description="") +@tenant_authentication async def present_proof_problem_report(request: web.BaseRequest): """Request handler for sending problem report. @@ -1352,6 +1362,7 @@ async def present_proof_problem_report(request: web.BaseRequest): ) @match_info_schema(V20PresExIdMatchInfoSchema()) @response_schema(V20PresentProofModuleResponseSchema(), description="") +@tenant_authentication async def present_proof_remove(request: web.BaseRequest): """Request handler for removing a presentation exchange record. diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py index 90ccebce43..328b2bf878 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes.py @@ -1,11 +1,14 @@ from copy import deepcopy -from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock -from marshmallow import ValidationError from time import time +from unittest import IsolatedAsyncioTestCase from unittest.mock import ANY +from marshmallow import ValidationError + +from aries_cloudagent.tests import mock + from .....admin.request_context import AdminRequestContext +from .....core.in_memory import InMemoryProfile from .....indy.holder import IndyHolder from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....indy.verifier import IndyVerifier @@ -13,9 +16,7 @@ from .....storage.error import StorageNotFoundError from .....storage.vc_holder.base import VCHolder from .....storage.vc_holder.vc_record import VCRecord - from ...dif.pres_exch import SchemaInputDescriptor - from .. import routes as test_module from ..messages.pres_format import V20PresFormat from ..models.pres_exchange import V20PresExRecord @@ -126,7 +127,12 @@ class TestPresentProofRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.context = AdminRequestContext.test_context() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(profile=profile) self.profile = self.context.profile injector = self.profile.context.injector @@ -181,6 +187,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_validate(self): diff --git a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py index 4740a46f3e..e79f0e0c74 100644 --- a/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py +++ b/aries_cloudagent/protocols/present_proof/v2_0/tests/test_routes_anoncreds.py @@ -1,22 +1,23 @@ -import pytest from copy import deepcopy -from unittest import IsolatedAsyncioTestCase -from aries_cloudagent.tests import mock -from marshmallow import ValidationError from time import time +from unittest import IsolatedAsyncioTestCase from unittest.mock import ANY +import pytest +from marshmallow import ValidationError + +from aries_cloudagent.tests import mock + from .....admin.request_context import AdminRequestContext from .....anoncreds.holder import AnonCredsHolder -from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....anoncreds.verifier import AnonCredsVerifier +from .....core.in_memory import InMemoryProfile +from .....indy.models.proof_request import IndyProofReqAttrSpecSchema from .....ledger.base import BaseLedger from .....storage.error import StorageNotFoundError from .....storage.vc_holder.base import VCHolder from .....storage.vc_holder.vc_record import VCRecord - from ...dif.pres_exch import SchemaInputDescriptor - from .. import routes as test_module from ..messages.pres_format import V20PresFormat from ..models.pres_exchange import V20PresExRecord @@ -127,7 +128,12 @@ class TestPresentProofRoutesAnonCreds(IsolatedAsyncioTestCase): def setUp(self): - self.context = AdminRequestContext.test_context() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(profile=profile) self.context.profile.settings.set_value("wallet.type", "askar-anoncreds") self.profile = self.context.profile injector = self.profile.context.injector @@ -183,6 +189,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_validate(self): diff --git a/aries_cloudagent/protocols/trustping/v1_0/routes.py b/aries_cloudagent/protocols/trustping/v1_0/routes.py index f8a41fd412..b1e850515f 100644 --- a/aries_cloudagent/protocols/trustping/v1_0/routes.py +++ b/aries_cloudagent/protocols/trustping/v1_0/routes.py @@ -2,9 +2,9 @@ from aiohttp import web from aiohttp_apispec import docs, match_info_schema, request_schema, response_schema - from marshmallow import fields +from ....admin.decorators.auth import tenant_authentication from ....admin.request_context import AdminRequestContext from ....connections.models.conn_record import ConnRecord from ....messaging.models.openapi import OpenAPISchema @@ -45,6 +45,7 @@ class PingConnIdMatchInfoSchema(OpenAPISchema): @match_info_schema(PingConnIdMatchInfoSchema()) @request_schema(PingRequestSchema()) @response_schema(PingRequestResponseSchema(), 200, description="") +@tenant_authentication async def connections_send_ping(request: web.BaseRequest): """Request handler for sending a trust ping to a connection. diff --git a/aries_cloudagent/protocols/trustping/v1_0/tests/test_routes.py b/aries_cloudagent/protocols/trustping/v1_0/tests/test_routes.py index 97cd67993a..541c4e7abe 100644 --- a/aries_cloudagent/protocols/trustping/v1_0/tests/test_routes.py +++ b/aries_cloudagent/protocols/trustping/v1_0/tests/test_routes.py @@ -1,15 +1,21 @@ from unittest import IsolatedAsyncioTestCase + from aries_cloudagent.tests import mock from .....admin.request_context import AdminRequestContext - +from .....core.in_memory import InMemoryProfile from .. import routes as test_module class TestTrustpingRoutes(IsolatedAsyncioTestCase): def setUp(self): self.session_inject = {} - self.context = AdminRequestContext.test_context(self.session_inject) + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) + self.context = AdminRequestContext.test_context(self.session_inject, profile) self.request_dict = { "context": self.context, "outbound_message_router": mock.CoroutineMock(), @@ -19,6 +25,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) async def test_connections_send_ping(self): diff --git a/aries_cloudagent/resolver/routes.py b/aries_cloudagent/resolver/routes.py index 85fdc2522c..a027577556 100644 --- a/aries_cloudagent/resolver/routes.py +++ b/aries_cloudagent/resolver/routes.py @@ -2,10 +2,10 @@ from aiohttp import web from aiohttp_apispec import docs, match_info_schema, response_schema -from pydid.common import DID_PATTERN - from marshmallow import fields, validate +from pydid.common import DID_PATTERN +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..messaging.models.openapi import OpenAPISchema from .base import DIDMethodNotSupported, DIDNotFound, ResolutionResult, ResolverError @@ -49,6 +49,7 @@ class DIDMatchInfoSchema(OpenAPISchema): @docs(tags=["resolver"], summary="Retrieve doc for requested did") @match_info_schema(DIDMatchInfoSchema()) @response_schema(ResolutionResultSchema(), 200) +@tenant_authentication async def resolve_did(request: web.Request): """Retrieve a did document.""" context: AdminRequestContext = request["context"] diff --git a/aries_cloudagent/resolver/tests/test_routes.py b/aries_cloudagent/resolver/tests/test_routes.py index bdb1c2fd73..311f60fbb2 100644 --- a/aries_cloudagent/resolver/tests/test_routes.py +++ b/aries_cloudagent/resolver/tests/test_routes.py @@ -3,11 +3,11 @@ # pylint: disable=redefined-outer-name import pytest -from aries_cloudagent.tests import mock from pydid import DIDDocument -from ...core.in_memory import InMemoryProfile +from aries_cloudagent.tests import mock +from ...core.in_memory import InMemoryProfile from .. import routes as test_module from ..base import ( DIDMethodNotSupported, @@ -18,7 +18,6 @@ ResolverType, ) from ..did_resolver import DIDResolver - from . import DOC @@ -59,7 +58,11 @@ def mock_resolver(resolution_result): @pytest.mark.asyncio async def test_resolver(mock_resolver, mock_response: mock.MagicMock, did_doc): - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) context = profile.context setattr(context, "profile", profile) session = await profile.session() @@ -77,6 +80,7 @@ async def test_resolver(mock_resolver, mock_response: mock.MagicMock, did_doc): query={}, json=mock.CoroutineMock(return_value={}), __getitem__=lambda _, k: request_dict[k], + headers={"x-api-key": "secret-key"}, ) with mock.patch.object( context.profile, @@ -100,7 +104,11 @@ async def test_resolver(mock_resolver, mock_response: mock.MagicMock, did_doc): async def test_resolver_not_found_error(mock_resolver, side_effect, error): mock_resolver.resolve_with_metadata = mock.CoroutineMock(side_effect=side_effect()) - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) context = profile.context setattr(context, "profile", profile) session = await profile.session() @@ -118,6 +126,7 @@ async def test_resolver_not_found_error(mock_resolver, side_effect, error): query={}, json=mock.CoroutineMock(return_value={}), __getitem__=lambda _, k: request_dict[k], + headers={"x-api-key": "secret-key"}, ) with mock.patch.object( context.profile, diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index eb67ab47eb..1d32cc3af6 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -338,10 +338,11 @@ async def clear_pending_revocations( async with self._profile.transaction() as txn: issuer_rr_recs = await IssuerRevRegRecord.query_by_pending(txn) for issuer_rr_rec in issuer_rr_recs: + if purge and issuer_rr_rec.revoc_reg_id not in purge: + continue rrid = issuer_rr_rec.revoc_reg_id await issuer_rr_rec.clear_pending(txn, (purge or {}).get(rrid)) - if issuer_rr_rec.pending_pub: - result[rrid] = issuer_rr_rec.pending_pub + result[rrid] = issuer_rr_rec.pending_pub notify.append(rrid) await txn.commit() diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index 3ff56d6cbf..c2e0c13782 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -18,6 +18,7 @@ from marshmallow import fields, validate, validates_schema from marshmallow.exceptions import ValidationError +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..connections.models.conn_record import ConnRecord from ..core.event_bus import Event, EventBus @@ -507,6 +508,7 @@ class RevRegConnIdMatchInfoSchema(OpenAPISchema): @querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) @querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema(), description="") +@tenant_authentication async def revoke(request: web.BaseRequest): """Request handler for storing a credential revocation. @@ -617,6 +619,7 @@ async def revoke(request: web.BaseRequest): @querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) @querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(TxnOrPublishRevocationsResultSchema(), 200, description="") +@tenant_authentication async def publish_revocations(request: web.BaseRequest): """Request handler for publishing pending revocations to the ledger. @@ -687,6 +690,7 @@ async def publish_revocations(request: web.BaseRequest): @docs(tags=["revocation"], summary="Clear pending revocations") @request_schema(ClearPendingRevocationsRequestSchema()) @response_schema(PublishRevocationsSchema(), 200, description="") +@tenant_authentication async def clear_pending_revocations(request: web.BaseRequest): """Request handler for clearing pending revocations. @@ -717,6 +721,7 @@ async def clear_pending_revocations(request: web.BaseRequest): @docs(tags=["revocation"], summary="Rotate revocation registry") @match_info_schema(RevocationCredDefIdMatchInfoSchema()) @response_schema(RevRegsCreatedSchema(), 200, description="") +@tenant_authentication async def rotate_rev_reg(request: web.BaseRequest): """Request handler to rotate the active revocation registries for cred. def. @@ -749,6 +754,7 @@ async def rotate_rev_reg(request: web.BaseRequest): @docs(tags=["revocation"], summary="Creates a new revocation registry") @request_schema(RevRegCreateRequestSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def create_rev_reg(request: web.BaseRequest): """Request handler to create a new revocation registry. @@ -802,6 +808,7 @@ async def create_rev_reg(request: web.BaseRequest): ) @querystring_schema(RevRegsCreatedQueryStringSchema()) @response_schema(RevRegsCreatedSchema(), 200, description="") +@tenant_authentication async def rev_regs_created(request: web.BaseRequest): """Request handler to get revocation registries that current agent created. @@ -842,6 +849,7 @@ async def rev_regs_created(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def get_rev_reg(request: web.BaseRequest): """Request handler to get a revocation registry by rev reg id. @@ -874,6 +882,7 @@ async def get_rev_reg(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevRegIssuedResultSchema(), 200, description="") +@tenant_authentication async def get_rev_reg_issued_count(request: web.BaseRequest): """Request handler to get number of credentials issued against revocation registry. @@ -909,6 +918,7 @@ async def get_rev_reg_issued_count(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(CredRevRecordDetailsResultSchema(), 200, description="") +@tenant_authentication async def get_rev_reg_issued(request: web.BaseRequest): """Request handler to get credentials issued against revocation registry. @@ -946,6 +956,7 @@ async def get_rev_reg_issued(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(CredRevIndyRecordsResultSchema(), 200, description="") +@tenant_authentication async def get_rev_reg_indy_recs(request: web.BaseRequest): """Request handler to get details of revoked credentials from ledger. @@ -980,6 +991,7 @@ async def get_rev_reg_indy_recs(request: web.BaseRequest): @match_info_schema(RevRegIdMatchInfoSchema()) @querystring_schema(RevRegUpdateRequestMatchInfoSchema()) @response_schema(RevRegWalletUpdatedResultSchema(), 200, description="") +@tenant_authentication async def update_rev_reg_revoked_state(request: web.BaseRequest): """Request handler to fix ledger entry of credentials revoked against registry. @@ -1071,6 +1083,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): ) @querystring_schema(CredRevRecordQueryStringSchema()) @response_schema(CredRevRecordResultSchema(), 200, description="") +@tenant_authentication async def get_cred_rev_record(request: web.BaseRequest): """Request handler to get credential revocation record. @@ -1112,6 +1125,7 @@ async def get_cred_rev_record(request: web.BaseRequest): ) @match_info_schema(RevocationCredDefIdMatchInfoSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def get_active_rev_reg(request: web.BaseRequest): """Request handler to get current active revocation registry by cred def id. @@ -1145,6 +1159,7 @@ async def get_active_rev_reg(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema, description="tails file") +@tenant_authentication async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: """Request handler to download tails file for revocation registry. @@ -1177,6 +1192,7 @@ async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevocationModuleResponseSchema(), description="") +@tenant_authentication async def upload_tails_file(request: web.BaseRequest): """Request handler to upload local tails file for revocation registry. @@ -1215,6 +1231,7 @@ async def upload_tails_file(request: web.BaseRequest): @querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) @querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(TxnOrRevRegResultSchema(), 200, description="") +@tenant_authentication async def send_rev_reg_def(request: web.BaseRequest): """Request handler to send revocation registry definition by rev reg id to ledger. @@ -1335,6 +1352,7 @@ async def send_rev_reg_def(request: web.BaseRequest): @querystring_schema(CreateRevRegTxnForEndorserOptionSchema()) @querystring_schema(RevRegConnIdMatchInfoSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def send_rev_reg_entry(request: web.BaseRequest): """Request handler to send rev reg entry by registry id to ledger. @@ -1454,6 +1472,7 @@ async def send_rev_reg_entry(request: web.BaseRequest): @match_info_schema(RevRegIdMatchInfoSchema()) @request_schema(RevRegUpdateTailsFileUriSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def update_rev_reg(request: web.BaseRequest): """Request handler to update a rev reg's public tails URI by registry id. @@ -1491,6 +1510,7 @@ async def update_rev_reg(request: web.BaseRequest): @match_info_schema(RevRegIdMatchInfoSchema()) @querystring_schema(SetRevRegStateQueryStringSchema()) @response_schema(RevRegResultSchema(), 200, description="") +@tenant_authentication async def set_rev_reg_state(request: web.BaseRequest): """Request handler to set a revocation registry state manually. @@ -1744,6 +1764,7 @@ class TailsDeleteResponseSchema(OpenAPISchema): @querystring_schema(RevRegId()) @response_schema(TailsDeleteResponseSchema()) @docs(tags=["revocation"], summary="Delete the tail files") +@tenant_authentication async def delete_tails(request: web.BaseRequest) -> json: """Delete Tails Files.""" context: AdminRequestContext = request["context"] diff --git a/aries_cloudagent/revocation/tests/test_manager.py b/aries_cloudagent/revocation/tests/test_manager.py index 6ebc48a330..d811b31a27 100644 --- a/aries_cloudagent/revocation/tests/test_manager.py +++ b/aries_cloudagent/revocation/tests/test_manager.py @@ -619,18 +619,17 @@ async def test_publish_pending_revocations_1_rev_reg_some(self): mock_issuer_rev_reg_records[1].clear_pending.assert_not_called() async def test_clear_pending(self): + REV_REG_ID_2 = f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2" mock_issuer_rev_reg_records = [ - mock.MagicMock( + test_module.IssuerRevRegRecord( revoc_reg_id=REV_REG_ID, tails_local_path=TAILS_LOCAL, - pending_pub=[], - clear_pending=mock.CoroutineMock(), + pending_pub=["1", "2"], ), - mock.MagicMock( - revoc_reg_id=f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2", + test_module.IssuerRevRegRecord( + revoc_reg_id=REV_REG_ID_2, tails_local_path=TAILS_LOCAL, - pending_pub=[], - clear_pending=mock.CoroutineMock(), + pending_pub=["9", "99"], ), ] with mock.patch.object( @@ -639,21 +638,22 @@ async def test_clear_pending(self): mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), ) as record: result = await self.manager.clear_pending_revocations() - assert result == {} + assert result[REV_REG_ID] == [] + assert result[REV_REG_ID_2] == [] async def test_clear_pending_1_rev_reg_all(self): + REV_REG_ID_2 = f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2" + mock_issuer_rev_reg_records = [ - mock.MagicMock( + test_module.IssuerRevRegRecord( revoc_reg_id=REV_REG_ID, tails_local_path=TAILS_LOCAL, pending_pub=["1", "2"], - clear_pending=mock.CoroutineMock(), ), - mock.MagicMock( - revoc_reg_id=f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2", + test_module.IssuerRevRegRecord( + revoc_reg_id=REV_REG_ID_2, tails_local_path=TAILS_LOCAL, pending_pub=["9", "99"], - clear_pending=mock.CoroutineMock(), ), ] with mock.patch.object( @@ -661,25 +661,22 @@ async def test_clear_pending_1_rev_reg_all(self): "query_by_pending", mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), ) as record: - result = await self.manager.clear_pending_revocations({REV_REG_ID: None}) - assert result == { - REV_REG_ID: ["1", "2"], - f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2": ["9", "99"], - } + result = await self.manager.clear_pending_revocations({REV_REG_ID: []}) + assert result[REV_REG_ID] == [] + assert result.get(REV_REG_ID_2) is None async def test_clear_pending_1_rev_reg_some(self): + REV_REG_ID_2 = f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2" mock_issuer_rev_reg_records = [ - mock.MagicMock( + test_module.IssuerRevRegRecord( revoc_reg_id=REV_REG_ID, tails_local_path=TAILS_LOCAL, pending_pub=["1", "2"], - clear_pending=mock.CoroutineMock(), ), - mock.MagicMock( - revoc_reg_id=f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2", + test_module.IssuerRevRegRecord( + revoc_reg_id=REV_REG_ID_2, tails_local_path=TAILS_LOCAL, pending_pub=["99"], - clear_pending=mock.CoroutineMock(), ), ] with mock.patch.object( @@ -688,10 +685,34 @@ async def test_clear_pending_1_rev_reg_some(self): mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), ) as record: result = await self.manager.clear_pending_revocations({REV_REG_ID: ["9"]}) - assert result == { - REV_REG_ID: ["1", "2"], - f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2": ["99"], - } + + assert result[REV_REG_ID] == ["1", "2"] + assert result.get(REV_REG_ID_2) is None + + async def test_clear_pending_both(self): + REV_REG_ID_2 = f"{TEST_DID}:4:{CRED_DEF_ID}:CL_ACCUM:tag2" + mock_issuer_rev_reg_records = [ + test_module.IssuerRevRegRecord( + revoc_reg_id=REV_REG_ID, + tails_local_path=TAILS_LOCAL, + pending_pub=["1", "2"], + ), + test_module.IssuerRevRegRecord( + revoc_reg_id=REV_REG_ID_2, + tails_local_path=TAILS_LOCAL, + pending_pub=["99"], + ), + ] + with mock.patch.object( + test_module.IssuerRevRegRecord, + "query_by_pending", + mock.CoroutineMock(return_value=mock_issuer_rev_reg_records), + ) as record: + result = await self.manager.clear_pending_revocations( + {REV_REG_ID: ["1"], REV_REG_ID_2: ["99"]} + ) + assert result[REV_REG_ID] == ["2"] + assert result[REV_REG_ID_2] == [] async def test_retrieve_records(self): session = await self.profile.session() diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index 71a9841d16..95e4eab0b5 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -17,7 +17,11 @@ class TestRevocationRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) self.context = self.profile.context setattr(self.context, "profile", self.profile) self.request_dict = { @@ -29,11 +33,16 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_did = "sample-did" - self.author_profile = InMemoryProfile.test_profile() + self.author_profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "author-key", + } + ) self.author_profile.settings.set_value("endorser.author", True) self.author_context = self.author_profile.context setattr(self.author_context, "profile", self.author_profile) @@ -46,6 +55,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.author_request_dict[k], + headers={"x-api-key": "author-key"}, ) async def test_validate_cred_rev_rec_qs_and_revoke_req(self): @@ -1054,7 +1064,7 @@ async def test_set_rev_reg_state_not_found(self): async def test_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet.type": "askar"}, + settings={"wallet.type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarAnoncredsProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -1067,6 +1077,7 @@ async def test_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( diff --git a/aries_cloudagent/revocation_anoncreds/routes.py b/aries_cloudagent/revocation_anoncreds/routes.py index 99b66f1bd2..1f1b034c1f 100644 --- a/aries_cloudagent/revocation_anoncreds/routes.py +++ b/aries_cloudagent/revocation_anoncreds/routes.py @@ -15,6 +15,7 @@ from marshmallow import fields, validate, validates_schema from marshmallow.exceptions import ValidationError +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..anoncreds.base import ( AnonCredsObjectNotFound, @@ -459,6 +460,7 @@ def validate_fields(self, data, **kwargs): ) @request_schema(RevokeRequestSchemaAnoncreds()) @response_schema(RevocationAnoncredsModuleResponseSchema(), description="") +@tenant_authentication async def revoke(request: web.BaseRequest): """Request handler for storing a credential revocation. @@ -512,6 +514,7 @@ async def revoke(request: web.BaseRequest): @docs(tags=[TAG_TITLE], summary="Publish pending revocations to ledger") @request_schema(PublishRevocationsSchemaAnoncreds()) @response_schema(PublishRevocationsResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def publish_revocations(request: web.BaseRequest): """Request handler for publishing pending revocations to the ledger. @@ -551,6 +554,7 @@ async def publish_revocations(request: web.BaseRequest): ) @querystring_schema(RevRegsCreatedQueryStringSchema()) @response_schema(RevRegsCreatedSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_rev_regs(request: web.BaseRequest): """Request handler to get revocation registries that current agent created. @@ -589,6 +593,7 @@ async def get_rev_regs(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevRegResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_rev_reg(request: web.BaseRequest): """Request handler to get a revocation registry by rev reg id. @@ -663,6 +668,7 @@ async def _get_issuer_rev_reg_record( ) @match_info_schema(RevocationCredDefIdMatchInfoSchema()) @response_schema(RevRegResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_active_rev_reg(request: web.BaseRequest): """Request handler to get current active revocation registry by cred def id. @@ -692,6 +698,7 @@ async def get_active_rev_reg(request: web.BaseRequest): @docs(tags=[TAG_TITLE], summary="Rotate revocation registry") @match_info_schema(RevocationCredDefIdMatchInfoSchema()) @response_schema(RevRegsCreatedSchemaAnoncreds(), 200, description="") +@tenant_authentication async def rotate_rev_reg(request: web.BaseRequest): """Request handler to rotate the active revocation registries for cred. def. @@ -724,6 +731,7 @@ async def rotate_rev_reg(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevRegIssuedResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_rev_reg_issued_count(request: web.BaseRequest): """Request handler to get number of credentials issued against revocation registry. @@ -764,6 +772,7 @@ async def get_rev_reg_issued_count(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(CredRevRecordDetailsResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_rev_reg_issued(request: web.BaseRequest): """Request handler to get credentials issued against revocation registry. @@ -805,6 +814,7 @@ async def get_rev_reg_issued(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(CredRevIndyRecordsResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_rev_reg_indy_recs(request: web.BaseRequest): """Request handler to get details of revoked credentials from ledger. @@ -850,6 +860,7 @@ async def get_rev_reg_indy_recs(request: web.BaseRequest): @match_info_schema(RevRegIdMatchInfoSchema()) @querystring_schema(RevRegUpdateRequestMatchInfoSchema()) @response_schema(RevRegWalletUpdatedResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def update_rev_reg_revoked_state(request: web.BaseRequest): """Request handler to fix ledger entry of credentials revoked against registry. @@ -945,6 +956,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): ) @querystring_schema(CredRevRecordQueryStringSchema()) @response_schema(CredRevRecordResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def get_cred_rev_record(request: web.BaseRequest): """Request handler to get credential revocation record. @@ -987,6 +999,7 @@ async def get_cred_rev_record(request: web.BaseRequest): ) @match_info_schema(RevRegIdMatchInfoSchema()) @response_schema(RevocationAnoncredsModuleResponseSchema, description="tails file") +@tenant_authentication async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: """Request handler to download tails file for revocation registry. @@ -1025,6 +1038,7 @@ async def get_tails_file(request: web.BaseRequest) -> web.FileResponse: @match_info_schema(RevRegIdMatchInfoSchema()) @querystring_schema(SetRevRegStateQueryStringSchema()) @response_schema(RevRegResultSchemaAnoncreds(), 200, description="") +@tenant_authentication async def set_rev_reg_state(request: web.BaseRequest): """Request handler to set a revocation registry state manually. diff --git a/aries_cloudagent/revocation_anoncreds/tests/test_routes.py b/aries_cloudagent/revocation_anoncreds/tests/test_routes.py index 2198e7668b..c3a102c513 100644 --- a/aries_cloudagent/revocation_anoncreds/tests/test_routes.py +++ b/aries_cloudagent/revocation_anoncreds/tests/test_routes.py @@ -20,7 +20,10 @@ class TestRevocationRoutes(IsolatedAsyncioTestCase): def setUp(self): - self.profile = InMemoryProfile.test_profile(profile_class=AskarAnoncredsProfile) + self.profile = InMemoryProfile.test_profile( + settings={"admin.admin_api_key": "secret-key"}, + profile_class=AskarAnoncredsProfile, + ) self.context = self.profile.context setattr(self.context, "profile", self.profile) self.request_dict = { @@ -32,6 +35,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_did = "sample-did" @@ -524,7 +528,7 @@ async def test_set_rev_reg_state_not_found(self): async def test_wrong_profile_403(self): self.profile = InMemoryProfile.test_profile( - settings={"wallet.type": "askar"}, + settings={"wallet.type": "askar", "admin.admin_api_key": "secret-key"}, profile_class=AskarProfile, ) self.context = AdminRequestContext.test_context({}, self.profile) @@ -537,6 +541,7 @@ async def test_wrong_profile_403(self): query={}, __getitem__=lambda _, k: self.request_dict[k], context=self.context, + headers={"x-api-key": "secret-key"}, ) self.request.json = mock.CoroutineMock( diff --git a/aries_cloudagent/settings/tests/test_routes.py b/aries_cloudagent/settings/tests/test_routes.py index 4c103bf2ba..3b2e3eb76b 100644 --- a/aries_cloudagent/settings/tests/test_routes.py +++ b/aries_cloudagent/settings/tests/test_routes.py @@ -3,13 +3,13 @@ # pylint: disable=redefined-outer-name import pytest + from aries_cloudagent.tests import mock from ...admin.request_context import AdminRequestContext from ...core.in_memory import InMemoryProfile from ...multitenant.base import BaseMultitenantManager from ...multitenant.manager import MultitenantManager - from .. import routes as test_module @@ -24,7 +24,11 @@ def mock_response(): @pytest.mark.asyncio async def test_get_profile_settings(mock_response): - profile = InMemoryProfile.test_profile() + profile = InMemoryProfile.test_profile( + settings={ + "admin.admin_api_key": "secret-key", + } + ) profile.settings.update( { "admin.admin_client_max_request_size": 1, @@ -45,6 +49,7 @@ async def test_get_profile_settings(mock_response): query={}, json=mock.CoroutineMock(return_value={}), __getitem__=lambda _, k: request_dict[k], + headers={"x-api-key": "secret-key"}, ) await test_module.get_profile_settings(request) assert mock_response.call_args[0][0] == { diff --git a/aries_cloudagent/storage/indy.py b/aries_cloudagent/storage/indy.py deleted file mode 100644 index 16bfab6624..0000000000 --- a/aries_cloudagent/storage/indy.py +++ /dev/null @@ -1,334 +0,0 @@ -"""Indy implementation of BaseStorage interface.""" - -import asyncio -import json -import logging -from typing import Mapping, Sequence - -from indy import non_secrets -from indy.error import IndyError, ErrorCode - -from .base import ( - DEFAULT_PAGE_SIZE, - BaseStorage, - BaseStorageSearch, - BaseStorageSearchSession, - validate_record, -) -from .error import ( - StorageError, - StorageDuplicateError, - StorageNotFoundError, - StorageSearchError, -) -from .record import StorageRecord -from ..indy.sdk.wallet_setup import IndyOpenWallet - -LOGGER = logging.getLogger(__name__) - - -class IndySdkStorage(BaseStorage, BaseStorageSearch): - """Indy Non-Secrets interface.""" - - def __init__(self, wallet: IndyOpenWallet): - """Initialize an `IndySdkStorage` instance. - - Args: - wallet: The indy wallet instance to use - - """ - self._wallet = wallet - - @property - def wallet(self) -> IndyOpenWallet: - """Accessor for IndyOpenWallet instance.""" - return self._wallet - - async def add_record(self, record: StorageRecord): - """Add a new record to the store. - - Args: - record: `StorageRecord` to be stored - - """ - validate_record(record) - tags_json = json.dumps(record.tags) if record.tags else None - try: - await non_secrets.add_wallet_record( - self._wallet.handle, record.type, record.id, record.value, tags_json - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletItemAlreadyExists: - raise StorageDuplicateError( - "Duplicate record ID: {}".format(record.id) - ) from x_indy - raise StorageError(str(x_indy)) from x_indy - - async def get_record( - self, record_type: str, record_id: str, options: Mapping = None - ) -> StorageRecord: - """Fetch a record from the store by type and ID. - - Args: - record_type: The record type - record_id: The record id - options: A dictionary of backend-specific options - - Returns: - A `StorageRecord` instance - - Raises: - StorageError: If the record is not provided - StorageError: If the record ID not provided - StorageNotFoundError: If the record is not found - StorageError: If record not found - - """ - if not record_type: - raise StorageError("Record type not provided") - if not record_id: - raise StorageError("Record ID not provided") - if not options: - options = {} - options_json = json.dumps( - { - "retrieveType": False, - "retrieveValue": True, - "retrieveTags": options.get("retrieveTags", True), - } - ) - try: - result_json = await non_secrets.get_wallet_record( - self._wallet.handle, record_type, record_id, options_json - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletItemNotFound: - raise StorageNotFoundError( - f"{record_type} record not found: {record_id}" - ) from x_indy - raise StorageError(str(x_indy)) from x_indy - result = json.loads(result_json) - return StorageRecord( - type=record_type, - id=result["id"], - value=result["value"], - tags=result["tags"] or {}, - ) - - async def update_record(self, record: StorageRecord, value: str, tags: Mapping): - """Update an existing stored record's value and tags. - - Args: - record: `StorageRecord` to update - value: The new value - tags: The new tags - - Raises: - StorageNotFoundError: If record not found - StorageError: If a libindy error occurs - - """ - validate_record(record) - tags_json = json.dumps(tags) if tags else "{}" - try: - await non_secrets.update_wallet_record_value( - self._wallet.handle, record.type, record.id, value - ) - await non_secrets.update_wallet_record_tags( - self._wallet.handle, record.type, record.id, tags_json - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletItemNotFound: - raise StorageNotFoundError(f"Record not found: {record.id}") - raise StorageError(str(x_indy)) - - async def delete_record(self, record: StorageRecord): - """Delete a record. - - Args: - record: `StorageRecord` to delete - - Raises: - StorageNotFoundError: If record not found - StorageError: If a libindy error occurs - - """ - validate_record(record, delete=True) - try: - await non_secrets.delete_wallet_record( - self._wallet.handle, record.type, record.id - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletItemNotFound: - raise StorageNotFoundError(f"Record not found: {record.id}") - raise StorageError(str(x_indy)) - - async def find_all_records( - self, - type_filter: str, - tag_query: Mapping = None, - options: Mapping = None, - ): - """Retrieve all records matching a particular type filter and tag query.""" - results = [] - search = self.search_records(type_filter, tag_query, options=options) - while True: - buf = await search.fetch() - if buf: - results.extend(buf) - else: - break - return results - - async def delete_all_records( - self, - type_filter: str, - tag_query: Mapping = None, - ): - """Remove all records matching a particular type filter and tag query.""" - async for row in self.search_records( - type_filter, tag_query, options={"retrieveTags": False} - ): - await self.delete_record(row) - - def search_records( - self, - type_filter: str, - tag_query: Mapping = None, - page_size: int = None, - options: Mapping = None, - ) -> "IndySdkStorageSearch": - """Search stored records. - - Args: - type_filter: Filter string - tag_query: Tags to query - page_size: Page size - options: Dictionary of backend-specific options - - Returns: - An instance of `IndySdkStorageSearch` - - """ - return IndySdkStorageSearch(self, type_filter, tag_query, page_size, options) - - -class IndySdkStorageSearch(BaseStorageSearchSession): - """Represent an active stored records search.""" - - def __init__( - self, - store: IndySdkStorage, - type_filter: str, - tag_query: Mapping, - page_size: int = None, - options: Mapping = None, - ): - """Initialize a `IndySdkStorageSearch` instance. - - Args: - store: `BaseStorage` to search - type_filter: Filter string - tag_query: Tags to search - page_size: Size of page to return - - """ - self._handle = None - self._done = False - self.store = store - self.options = options or {} - self.page_size = page_size or DEFAULT_PAGE_SIZE - self.tag_query = tag_query - self.type_filter = type_filter - - async def fetch(self, max_count: int = None) -> Sequence[StorageRecord]: - """Fetch the next list of results from the store. - - Args: - max_count: Max number of records to return. If not provided, - defaults to the backend's preferred page size - - Returns: - A list of `StorageRecord` instances - - Raises: - StorageSearchError: If the search query has not been opened - - """ - if self._done: - raise StorageSearchError("Search query is complete") - await self._open() - - try: - result_json = await non_secrets.fetch_wallet_search_next_records( - self.store.wallet.handle, self._handle, max_count or self.page_size - ) - except IndyError as x_indy: - raise StorageSearchError(str(x_indy)) from x_indy - - results = json.loads(result_json) - ret = [] - if results["records"]: - for row in results["records"]: - ret.append( - StorageRecord( - type=self.type_filter, - id=row["id"], - value=row["value"], - tags=row["tags"], - ) - ) - - if not ret: - await self.close() - - return ret - - async def _open(self): - """Start the search query.""" - if self._handle: - return - - query_json = json.dumps(self.tag_query or {}) - options_json = json.dumps( - { - "retrieveRecords": True, - "retrieveTotalCount": False, - "retrieveType": False, - "retrieveValue": True, - "retrieveTags": self.options.get("retrieveTags", True), - } - ) - try: - self._handle = await non_secrets.open_wallet_search( - self.store.wallet.handle, self.type_filter, query_json, options_json - ) - except IndyError as x_indy: - raise StorageSearchError(str(x_indy)) from x_indy - - async def close(self): - """Dispose of the search query.""" - try: - if self._handle: - await non_secrets.close_wallet_search(self._handle) - self._handle = None - self.store = None - self._done = True - except IndyError as x_indy: - raise StorageSearchError(str(x_indy)) from x_indy - - def __del__(self): - """Ensure the search is closed.""" - if self._handle: - - async def cleanup(handle): - LOGGER.warning("Indy wallet search was not closed manually") - try: - await non_secrets.close_wallet_search(handle) - except Exception: - LOGGER.exception("Exception when auto-closing Indy wallet search") - - loop = asyncio.get_event_loop() - task = loop.create_task(cleanup(self._handle)) - if not loop.is_running(): - loop.run_until_complete(task) diff --git a/aries_cloudagent/storage/tests/test_indy_storage.py b/aries_cloudagent/storage/tests/test_indy_storage.py deleted file mode 100644 index 77e4c8f3e2..0000000000 --- a/aries_cloudagent/storage/tests/test_indy_storage.py +++ /dev/null @@ -1,502 +0,0 @@ -import asyncio -import json -import pytest -import os - -import indy.anoncreds -import indy.crypto -import indy.did -import indy.wallet - -from indy.error import ErrorCode -from aries_cloudagent.tests import mock - -from ...config.injection_context import InjectionContext -from ...indy.sdk.profile import IndySdkProfileManager, IndySdkProfile -from ...storage.base import BaseStorage -from ...storage.error import StorageError, StorageSearchError -from ...storage.indy import IndySdkStorage -from ...storage.record import StorageRecord -from ...wallet.indy import IndySdkWallet -from ...ledger.indy import IndySdkLedgerPool - -from .. import indy as test_module -from . import test_in_memory_storage - - -async def make_profile(): - key = await IndySdkWallet.generate_wallet_key() - context = InjectionContext() - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - with mock.patch.object(IndySdkProfile, "_make_finalizer"): - return await IndySdkProfileManager().provision( - context, - { - "auto_recreate": True, - "auto_remove": True, - "name": "test-wallet", - "key": key, - "key_derivation_method": "RAW", # much slower tests with argon-hashed keys - }, - ) - - -@pytest.fixture() -async def store(): - profile = await make_profile() - async with profile.session() as session: - yield session.inject(BaseStorage) - await profile.close() - - -@pytest.fixture() -async def store_search(): - profile = await make_profile() - async with profile.session() as session: - yield session.inject(BaseStorage) - await profile.close() - - -@pytest.mark.indy -class TestIndySdkStorage(test_in_memory_storage.TestInMemoryStorage): - """Tests for indy storage.""" - - @pytest.mark.asyncio - async def test_record(self): - with mock.patch( - "aries_cloudagent.indy.sdk.wallet_plugin.load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete, mock.patch.object( - IndySdkProfile, "_make_finalizer" - ): - config = { - "auto_recreate": True, - "auto_remove": True, - "name": "test-wallet", - "key": await IndySdkWallet.generate_wallet_key(), - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": json.dumps({"url": "dummy"}), - "storage_creds": json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - } - ), - } - context = InjectionContext() - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - fake_profile = await IndySdkProfileManager().provision(context, config) - opened = await IndySdkProfileManager().open(context, config) # cover open() - await opened.close() - - session = await fake_profile.session() - storage = session.inject(BaseStorage) - - for record_x in [ - None, - StorageRecord( - type="connection", - value=json.dumps( - { - "initiator": "self", - "invitation_key": "9XgL7Y4TBTJyVJdomT6axZGUFg9npxcrXnRT4CG8fWYg", - "state": "invitation", - "routing_state": "none", - "error_msg": None, - "their_label": None, - "created_at": "2019-05-14 21:58:24.143260+00:00", - "updated_at": "2019-05-14 21:58:24.143260+00:00", - } - ), - tags={ - "initiator": "self", - "invitation_key": "9XgL7Y4TBTJyVJdomT6axZGUFg9npxcrXnRT4CG8fWYg", - "state": "invitation", - "routing_state": "none", - }, - id=None, - ), - StorageRecord( - type=None, - value=json.dumps( - { - "initiator": "self", - "invitation_key": "9XgL7Y4TBTJyVJdomT6axZGUFg9npxcrXnRT4CG8fWYg", - "state": "invitation", - "routing_state": "none", - "error_msg": None, - "their_label": None, - "created_at": "2019-05-14 21:58:24.143260+00:00", - "updated_at": "2019-05-14 21:58:24.143260+00:00", - } - ), - tags={ - "initiator": "self", - "invitation_key": "9XgL7Y4TBTJyVJdomT6axZGUFg9npxcrXnRT4CG8fWYg", - "state": "invitation", - "routing_state": "none", - }, - id="f96f76ec-0e9b-4f32-8237-f4219e6cf0c7", - ), - StorageRecord( - type="connection", - value=None, - tags={ - "initiator": "self", - "invitation_key": "9XgL7Y4TBTJyVJdomT6axZGUFg9npxcrXnRT4CG8fWYg", - "state": "invitation", - "routing_state": "none", - }, - id="f96f76ec-0e9b-4f32-8237-f4219e6cf0c7", - ), - ]: - with pytest.raises(StorageError): - await storage.add_record(record_x) - - with pytest.raises(StorageError): - await storage.get_record(None, "dummy-id") - with pytest.raises(StorageError): - await storage.get_record("connection", None) - - with mock.patch.object( - indy.non_secrets, "get_wallet_record", mock.CoroutineMock() - ) as mock_get_record: - mock_get_record.side_effect = test_module.IndyError( - ErrorCode.CommonInvalidStructure - ) - with pytest.raises(test_module.StorageError): - await storage.get_record("connection", "dummy-id") - - with mock.patch.object( - indy.non_secrets, - "update_wallet_record_value", - mock.CoroutineMock(), - ) as mock_update_value, mock.patch.object( - indy.non_secrets, - "update_wallet_record_tags", - mock.CoroutineMock(), - ) as mock_update_tags, mock.patch.object( - indy.non_secrets, - "delete_wallet_record", - mock.CoroutineMock(), - ) as mock_delete: - mock_update_value.side_effect = test_module.IndyError( - ErrorCode.CommonInvalidStructure - ) - mock_update_tags.side_effect = test_module.IndyError( - ErrorCode.CommonInvalidStructure - ) - mock_delete.side_effect = test_module.IndyError( - ErrorCode.CommonInvalidStructure - ) - - rec = StorageRecord( - type="connection", - value=json.dumps( - { - "initiator": "self", - "invitation_key": "9XgL7Y4TBTJyVJdomT6axZGUFg9npxcrXnRT4CG8fWYg", - "state": "invitation", - "routing_state": "none", - "error_msg": None, - "their_label": None, - "created_at": "2019-05-14 21:58:24.143260+00:00", - "updated_at": "2019-05-14 21:58:24.143260+00:00", - } - ), - tags={ - "initiator": "self", - "invitation_key": "9XgL7Y4TBTJyVJdomT6axZGUFg9npxcrXnRT4CG8fWYg", - "state": "invitation", - "routing_state": "none", - }, - id="f96f76ec-0e9b-4f32-8237-f4219e6cf0c7", - ) - - with pytest.raises(test_module.StorageError): - await storage.update_record(rec, "dummy-value", {"tag": "tag"}) - - with pytest.raises(test_module.StorageError): - await storage.delete_record(rec) - - @pytest.mark.asyncio - async def test_storage_search_x(self): - with mock.patch( - "aries_cloudagent.indy.sdk.wallet_plugin.load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete, mock.patch.object( - IndySdkProfile, "_make_finalizer" - ): - context = InjectionContext() - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - fake_profile = await IndySdkProfileManager().provision( - context, - { - "auto_recreate": True, - "auto_remove": True, - "name": "test_pg_wallet", - "key": await IndySdkWallet.generate_wallet_key(), - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": json.dumps({"url": "dummy"}), - "storage_creds": json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - } - ), - }, - ) - session = await fake_profile.session() - storage = session.inject(BaseStorage) - - search = storage.search_records("connection") - with pytest.raises(StorageSearchError): - await search.fetch(10) - - with mock.patch.object( - indy.non_secrets, "open_wallet_search", mock.CoroutineMock() - ) as mock_indy_open_search, mock.patch.object( - indy.non_secrets, "close_wallet_search", mock.CoroutineMock() - ) as mock_indy_close_search: - mock_indy_open_search.side_effect = test_module.IndyError("no open") - search = storage.search_records("connection") - with pytest.raises(StorageSearchError): - await search.fetch() - await search.close() - - with mock.patch.object( - indy.non_secrets, "open_wallet_search", mock.CoroutineMock() - ) as mock_indy_open_search, mock.patch.object( - indy.non_secrets, - "fetch_wallet_search_next_records", - mock.CoroutineMock(), - ) as mock_indy_fetch, mock.patch.object( - indy.non_secrets, "close_wallet_search", mock.CoroutineMock() - ) as mock_indy_close_search: - mock_indy_fetch.side_effect = test_module.IndyError("no fetch") - search = storage.search_records("connection") - with pytest.raises(StorageSearchError): - await search.fetch(10) - await search.close() - - with mock.patch.object( - indy.non_secrets, "open_wallet_search", mock.CoroutineMock() - ) as mock_indy_open_search, mock.patch.object( - indy.non_secrets, "close_wallet_search", mock.CoroutineMock() - ) as mock_indy_close_search: - mock_indy_close_search.side_effect = test_module.IndyError("no close") - search = storage.search_records("connection") - with pytest.raises(StorageSearchError): - await search.fetch() - - @pytest.mark.asyncio - async def test_storage_del_close(self): - with mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete, mock.patch.object( - IndySdkProfile, "_make_finalizer" - ): - context = InjectionContext() - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - fake_profile = await IndySdkProfileManager().provision( - context, - { - "auto_recreate": True, - "auto_remove": True, - "name": "test_indy_wallet", - "key": await IndySdkWallet.generate_wallet_key(), - "key_derivation_method": "RAW", - }, - ) - session = await fake_profile.session() - storage = session.inject(BaseStorage) - - with mock.patch.object( - indy.non_secrets, "open_wallet_search", mock.CoroutineMock() - ) as mock_indy_open_search, mock.patch.object( - indy.non_secrets, "close_wallet_search", mock.CoroutineMock() - ) as mock_indy_close_search: - mock_indy_open_search.return_value = 1 - search = storage.search_records("connection") - mock_indy_open_search.assert_not_awaited() - await search._open() - mock_indy_open_search.assert_awaited_once() - del search - c = 0 - # give the pending cleanup task time to be scheduled - while not mock_indy_close_search.await_count and c < 10: - await asyncio.sleep(0.1) - c += 1 - mock_indy_close_search.assert_awaited_with(1) - - with mock.patch.object( # error on close - indy.non_secrets, "open_wallet_search", mock.CoroutineMock() - ) as mock_indy_open_search, mock.patch.object( - indy.non_secrets, "close_wallet_search", mock.CoroutineMock() - ) as mock_indy_close_search: - mock_indy_close_search.side_effect = test_module.IndyError("no close") - mock_indy_open_search.return_value = 1 - search = storage.search_records("connection") - await search._open() - with pytest.raises(StorageSearchError): - await search.close() - - with mock.patch.object( # run on event loop until complete - indy.non_secrets, "open_wallet_search", mock.CoroutineMock() - ) as mock_indy_open_search, mock.patch.object( - indy.non_secrets, "close_wallet_search", mock.CoroutineMock() - ) as mock_indy_close_search, mock.patch.object( - asyncio, "get_event_loop", mock.MagicMock() - ) as mock_get_event_loop: - coros = [] - mock_get_event_loop.return_value = mock.MagicMock( - create_task=lambda c: coros.append(c), - is_running=mock.MagicMock(return_value=False), - run_until_complete=mock.MagicMock(), - ) - mock_indy_open_search.return_value = 1 - search = storage.search_records("connection") - await search._open() - del search - assert ( - coros - and len(coros) - == mock_get_event_loop.return_value.run_until_complete.call_count - ) - # now run the cleanup task - for coro in coros: - await coro - - with mock.patch.object( # run on event loop until complete - indy.non_secrets, "open_wallet_search", mock.CoroutineMock() - ) as mock_indy_open_search, mock.patch.object( - indy.non_secrets, "close_wallet_search", mock.CoroutineMock() - ) as mock_indy_close_search, mock.patch.object( - asyncio, "get_event_loop", mock.MagicMock() - ) as mock_get_event_loop: - coros = [] - mock_get_event_loop.return_value = mock.MagicMock( - create_task=lambda c: coros.append(c), - is_running=mock.MagicMock(return_value=False), - run_until_complete=mock.MagicMock(), - ) - mock_indy_open_search.return_value = 1 - mock_indy_close_search.side_effect = ValueError("Dave's not here") - search = storage.search_records("connection") - await search._open() - del search - assert ( - coros - and len(coros) - == mock_get_event_loop.return_value.run_until_complete.call_count - ) - # now run the cleanup task - for coro in coros: - await coro - - # TODO get these to run in docker ci/cd - @pytest.mark.asyncio - @pytest.mark.postgres - async def test_postgres_wallet_storage_works(self): - """ - Ensure that postgres wallet operations work (create and open wallet, store and search, drop wallet) - """ - postgres_url = os.environ.get("POSTGRES_URL") - if not postgres_url: - pytest.fail("POSTGRES_URL not configured") - - wallet_key = await IndySdkWallet.generate_wallet_key() - postgres_wallet = IndySdkWallet( - { - "auto_recreate": True, - "auto_remove": True, - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": '{"url":"' + postgres_url + '", "max_connections":5}', - "storage_creds": '{"account":"postgres","password":"mysecretpassword","admin_account":"postgres","admin_password":"mysecretpassword"}', - } - ) - await postgres_wallet.create() - await postgres_wallet.open() - - storage = IndySdkStorage(postgres_wallet) - - # add and then fetch a record - record = StorageRecord( - type="connection", - value=json.dumps( - { - "initiator": "self", - "invitation_key": "9XgL7Y4TBTJyVJdomT6axZGUFg9npxcrXnRT4CG8fWYg", - "state": "invitation", - "routing_state": "none", - "error_msg": None, - "their_label": None, - "created_at": "2019-05-14 21:58:24.143260+00:00", - "updated_at": "2019-05-14 21:58:24.143260+00:00", - } - ), - tags={ - "initiator": "self", - "invitation_key": "9XgL7Y4TBTJyVJdomT6axZGUFg9npxcrXnRT4CG8fWYg", - "state": "invitation", - "routing_state": "none", - }, - id="f96f76ec-0e9b-4f32-8237-f4219e6cf0c7", - ) - await storage.add_record(record) - g_rec = await storage.get_record(record.type, record.id) - - # now try search - search = None - try: - search = storage.search_records("connection") - await search.open() - records = await search.fetch(10) - finally: - if search: - await search.close() - - await postgres_wallet.close() - await postgres_wallet.remove() - - -@pytest.mark.indy -class TestIndySdkStorageSearch(test_in_memory_storage.TestInMemoryStorageSearch): - pass diff --git a/aries_cloudagent/storage/type.py b/aries_cloudagent/storage/type.py index 7a0cc9aab7..ea4279377f 100644 --- a/aries_cloudagent/storage/type.py +++ b/aries_cloudagent/storage/type.py @@ -1,3 +1,7 @@ """Library version information.""" RECORD_TYPE_ACAPY_STORAGE_TYPE = "acapy_storage_type" +RECORD_TYPE_ACAPY_UPGRADING = "acapy_upgrading" + +STORAGE_TYPE_VALUE_ANONCREDS = "askar-anoncreds" +STORAGE_TYPE_VALUE_ASKAR = "askar" diff --git a/aries_cloudagent/storage/vc_holder/indy.py b/aries_cloudagent/storage/vc_holder/indy.py deleted file mode 100644 index 60a96aa1bb..0000000000 --- a/aries_cloudagent/storage/vc_holder/indy.py +++ /dev/null @@ -1,161 +0,0 @@ -"""Indy-SDK storage implementation of VC holder interface.""" - -from dateutil.parser import parse as dateutil_parser -from dateutil.parser import ParserError -from typing import Mapping, Sequence - -from ...indy.sdk.wallet_setup import IndyOpenWallet - -from ..indy import IndySdkStorage, IndySdkStorageSearch - -from .base import VCHolder, VCRecordSearch -from .vc_record import VCRecord -from .xform import storage_to_vc_record, vc_to_storage_record, VC_CRED_RECORD_TYPE - - -class IndySdkVCHolder(VCHolder): - """Indy-SDK storage class.""" - - def __init__(self, wallet: IndyOpenWallet): - """Initialize the Indy-SDK VC holder instance.""" - self._wallet = wallet - self._store = IndySdkStorage(wallet) - - def build_type_or_schema_query(self, uri_list: Sequence[str]) -> dict: - """Build and return indy-specific type_or_schema_query.""" - type_or_schema_query = {"$and": []} - for uri in uri_list: - tag_or_list = [] - tag_or_list.append({f"type:{uri}": "1"}) - tag_or_list.append({f"schm:{uri}": "1"}) - type_or_schema_query["$and"].append({"$or": tag_or_list}) - return type_or_schema_query - - async def store_credential(self, cred: VCRecord): - """Add a new VC record to the store. - - Args: - cred: The VCRecord instance to store - Raises: - StorageDuplicateError: If the record_id is not unique - - """ - record = vc_to_storage_record(cred) - await self._store.add_record(record) - - async def retrieve_credential_by_id(self, record_id: str) -> VCRecord: - """Fetch a VC record by its record ID. - - Raises: - StorageNotFoundError: If the record is not found - - """ - record = await self._store.get_record(VC_CRED_RECORD_TYPE, record_id) - return storage_to_vc_record(record) - - async def retrieve_credential_by_given_id(self, given_id: str) -> VCRecord: - """Fetch a VC record by its given ID ('id' property). - - Raises: - StorageNotFoundError: If the record is not found - - """ - record = await self._store.find_record( - VC_CRED_RECORD_TYPE, {"given_id": given_id} - ) - return storage_to_vc_record(record) - - async def delete_credential(self, cred: VCRecord): - """Remove a previously-stored VC record. - - Raises: - StorageNotFoundError: If the record is not found - - """ - await self._store.delete_record(vc_to_storage_record(cred)) - - def search_credentials( - self, - contexts: Sequence[str] = None, - types: Sequence[str] = None, - schema_ids: Sequence[str] = None, - issuer_id: str = None, - subject_ids: str = None, - proof_types: Sequence[str] = None, - given_id: str = None, - tag_query: Mapping = None, - pd_uri_list: Sequence[str] = None, - ) -> "VCRecordSearch": - """Start a new VC record search. - - Args: - contexts: An inclusive list of JSON-LD contexts to match - types: An inclusive list of JSON-LD types to match - schema_ids: An inclusive list of credential schema identifiers - issuer_id: The ID of the credential issuer - subject_ids: The IDs of credential subjects all of which to match - proof_types: The signature suite types used for the proof objects. - given_id: The given id of the credential - tag_query: A tag filter clause - - """ - query = {} - if contexts: - for ctx_val in contexts: - query[f"ctxt:{ctx_val}"] = "1" - if types: - for type_val in types: - query[f"type:{type_val}"] = "1" - if schema_ids: - for schema_val in schema_ids: - query[f"schm:{schema_val}"] = "1" - if subject_ids: - for subject_id in subject_ids: - query[f"subj:{subject_id}"] = "1" - if proof_types: - for proof_type in proof_types: - query[f"ptyp:{proof_type}"] = "1" - if issuer_id: - query["issuer_id"] = issuer_id - if given_id: - query["given_id"] = given_id - if tag_query: - query.update(tag_query) - if pd_uri_list: - query.update(self.build_type_or_schema_query(pd_uri_list)) - search = self._store.search_records(VC_CRED_RECORD_TYPE, query) - return IndySdkVCRecordSearch(search) - - -class IndySdkVCRecordSearch(VCRecordSearch): - """Indy-SDK storage search for VC records.""" - - def __init__(self, search: IndySdkStorageSearch): - """Initialize the Indy-SDK VC record search.""" - self._search = search - - async def close(self): - """Dispose of the search query.""" - await self._search.close() - - async def fetch(self, max_count: int = None) -> Sequence[VCRecord]: - """Fetch the next list of VC records from the store. - - Args: - max_count: Max number of records to return. If not provided, - defaults to the backend's preferred page size - - Returns: - A list of `VCRecord` instances - - """ - rows = await self._search.fetch(max_count) - records = [storage_to_vc_record(r) for r in rows] - try: - records.sort( - key=lambda v: dateutil_parser(v.cred_value.get("issuanceDate")), - reverse=True, - ) - return records - except ParserError: - return records diff --git a/aries_cloudagent/storage/vc_holder/tests/test_indy_vc_holder.py b/aries_cloudagent/storage/vc_holder/tests/test_indy_vc_holder.py deleted file mode 100644 index 3009e61b1e..0000000000 --- a/aries_cloudagent/storage/vc_holder/tests/test_indy_vc_holder.py +++ /dev/null @@ -1,44 +0,0 @@ -import pytest -from unittest import mock - - -from ....config.injection_context import InjectionContext -from ....indy.sdk.profile import IndySdkProfileManager, IndySdkProfile -from ....ledger.indy import IndySdkLedgerPool -from ....wallet.indy import IndySdkWallet - -from ..base import VCHolder - -from . import test_in_memory_vc_holder as in_memory - - -async def make_profile(): - key = await IndySdkWallet.generate_wallet_key() - context = InjectionContext() - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - - with mock.patch.object(IndySdkProfile, "_make_finalizer"): - return await IndySdkProfileManager().provision( - context, - { - "auto_recreate": True, - "auto_remove": True, - "name": "test-wallet", - "key": key, - "key_derivation_method": "RAW", # much slower tests with argon-hashed keys - }, - ) - - -@pytest.fixture() -async def holder(): - profile = await make_profile() - async with profile.session() as session: - yield session.inject(VCHolder) - await profile.close() - - -@pytest.mark.indy -class TestIndySdkVCHolder(in_memory.TestInMemoryVCHolder): - # run same test suite with different holder fixture - pass diff --git a/aries_cloudagent/tails/tests/test_indy.py b/aries_cloudagent/tails/tests/test_indy.py index 1bbdcbefe4..8e9eddb208 100644 --- a/aries_cloudagent/tails/tests/test_indy.py +++ b/aries_cloudagent/tails/tests/test_indy.py @@ -44,37 +44,6 @@ async def test_upload(self): text == context.settings["tails_server_upload_url"] + "/" + REV_REG_ID ) - async def test_upload_indy_sdk(self): - profile = InMemoryProfile.test_profile() - profile.settings["tails_server_upload_url"] = "http://1.2.3.4:8088" - profile.context.injector.bind_instance( - BaseMultipleLedgerManager, - mock.MagicMock( - get_write_ledgers=mock.CoroutineMock( - return_value=[ - "test_ledger_id_1", - "test_ledger_id_2", - ] - ) - ), - ) - profile.context.injector.bind_instance(BaseLedger, mock.MagicMock()) - indy_tails = test_module.IndyTailsServer() - - with mock.patch.object( - test_module, "put_file", mock.CoroutineMock() - ) as mock_put: - mock_put.return_value = "tails-hash" - (ok, text) = await indy_tails.upload_tails_file( - profile.context, - REV_REG_ID, - "/tmp/dummy/path", - ) - assert ok - assert ( - text == profile.settings["tails_server_upload_url"] + "/" + REV_REG_ID - ) - async def test_upload_indy_vdr(self): profile = InMemoryProfile.test_profile() profile.settings["tails_server_upload_url"] = "http://1.2.3.4:8088" diff --git a/aries_cloudagent/utils/dependencies.py b/aries_cloudagent/utils/dependencies.py index 461c70664c..ec4c220871 100644 --- a/aries_cloudagent/utils/dependencies.py +++ b/aries_cloudagent/utils/dependencies.py @@ -3,27 +3,6 @@ import sys -def is_indy_sdk_module_installed(): - """Check whether indy (indy-sdk) module is installed. - - Returns: - bool: Whether indy (indy-sdk) is installed. - - """ - try: - # Check if already imported - if "indy" in sys.modules: - return True - - # Try to import - import indy # noqa: F401 - - return True - except ModuleNotFoundError: - # Not installed if import went wrong - return False - - def is_ursa_bbs_signatures_module_installed(): """Check whether ursa_bbs_signatures module is installed. diff --git a/aries_cloudagent/utils/general.py b/aries_cloudagent/utils/general.py new file mode 100644 index 0000000000..7c01793a07 --- /dev/null +++ b/aries_cloudagent/utils/general.py @@ -0,0 +1,10 @@ +"""Utility functions for the admin server.""" + +from hmac import compare_digest + + +def const_compare(string1, string2): + """Compare two strings in constant time.""" + if string1 is None or string2 is None: + return False + return compare_digest(string1.encode(), string2.encode()) diff --git a/aries_cloudagent/utils/profiles.py b/aries_cloudagent/utils/profiles.py index 45a440ed79..d5433f3afd 100644 --- a/aries_cloudagent/utils/profiles.py +++ b/aries_cloudagent/utils/profiles.py @@ -1,10 +1,15 @@ """Profile utilities.""" +import json + from aiohttp import web from ..anoncreds.error_messages import ANONCREDS_PROFILE_REQUIRED_MSG from ..askar.profile_anon import AskarAnoncredsProfile from ..core.profile import Profile +from ..multitenant.manager import MultitenantManager +from ..storage.base import BaseStorageSearch +from ..wallet.models.wallet_record import WalletRecord def is_anoncreds_profile_raise_web_exception(profile: Profile) -> None: @@ -29,3 +34,26 @@ def subwallet_type_not_same_as_base_wallet_raise_web_exception( raise web.HTTPForbidden( reason="Subwallet type must be the same as the base wallet type" ) + + +async def get_subwallet_profiles_from_storage(root_profile: Profile) -> list[Profile]: + """Get subwallet profiles from storage.""" + subwallet_profiles = [] + base_storage_search = root_profile.inject(BaseStorageSearch) + search_session = base_storage_search.search_records( + type_filter=WalletRecord.RECORD_TYPE, page_size=10 + ) + while search_session._done is False: + wallet_storage_records = await search_session.fetch() + for wallet_storage_record in wallet_storage_records: + wallet_record = WalletRecord.from_storage( + wallet_storage_record.id, + json.loads(wallet_storage_record.value), + ) + subwallet_profiles.append( + await MultitenantManager(root_profile).get_wallet_profile( + base_context=root_profile.context, + wallet_record=wallet_record, + ) + ) + return subwallet_profiles diff --git a/aries_cloudagent/vc/routes.py b/aries_cloudagent/vc/routes.py index 3cafdff542..8a78610dcc 100644 --- a/aries_cloudagent/vc/routes.py +++ b/aries_cloudagent/vc/routes.py @@ -1,32 +1,33 @@ """VC-API Routes.""" +import uuid + from aiohttp import web from aiohttp_apispec import docs, request_schema, response_schema from marshmallow.exceptions import ValidationError -import uuid + +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext -from ..storage.error import StorageError, StorageNotFoundError, StorageDuplicateError -from ..wallet.error import WalletError -from ..wallet.base import BaseWallet from ..config.base import InjectionError from ..resolver.base import ResolverError +from ..storage.error import StorageDuplicateError, StorageError, StorageNotFoundError from ..storage.vc_holder.base import VCHolder +from ..wallet.base import BaseWallet +from ..wallet.error import WalletError +from .vc_ld.manager import VcLdpManager, VcLdpManagerError from .vc_ld.models import web_schemas -from .vc_ld.manager import VcLdpManager -from .vc_ld.manager import VcLdpManagerError from .vc_ld.models.credential import ( VerifiableCredential, ) - +from .vc_ld.models.options import LDProofVCOptions from .vc_ld.models.presentation import ( VerifiablePresentation, ) -from .vc_ld.models.options import LDProofVCOptions - @docs(tags=["vc-api"], summary="List credentials") @response_schema(web_schemas.ListCredentialsResponse(), 200, description="") +@tenant_authentication async def list_credentials_route(request: web.BaseRequest): """Request handler for issuing a credential. @@ -46,6 +47,7 @@ async def list_credentials_route(request: web.BaseRequest): @docs(tags=["vc-api"], summary="Fetch credential by ID") @response_schema(web_schemas.FetchCredentialResponse(), 200, description="") +@tenant_authentication async def fetch_credential_route(request: web.BaseRequest): """Request handler for issuing a credential. @@ -66,6 +68,7 @@ async def fetch_credential_route(request: web.BaseRequest): @docs(tags=["vc-api"], summary="Issue a credential") @request_schema(web_schemas.IssueCredentialRequest()) @response_schema(web_schemas.IssueCredentialResponse(), 200, description="") +@tenant_authentication async def issue_credential_route(request: web.BaseRequest): """Request handler for issuing a credential. @@ -107,6 +110,7 @@ async def issue_credential_route(request: web.BaseRequest): @docs(tags=["vc-api"], summary="Verify a credential") @request_schema(web_schemas.VerifyCredentialRequest()) @response_schema(web_schemas.VerifyCredentialResponse(), 200, description="") +@tenant_authentication async def verify_credential_route(request: web.BaseRequest): """Request handler for verifying a credential. @@ -171,6 +175,7 @@ async def store_credential_route(request: web.BaseRequest): @docs(tags=["vc-api"], summary="Prove a presentation") @request_schema(web_schemas.ProvePresentationRequest()) @response_schema(web_schemas.ProvePresentationResponse(), 200, description="") +@tenant_authentication async def prove_presentation_route(request: web.BaseRequest): """Request handler for proving a presentation. @@ -211,6 +216,7 @@ async def prove_presentation_route(request: web.BaseRequest): @docs(tags=["vc-api"], summary="Verify a Presentation") @request_schema(web_schemas.VerifyPresentationRequest()) @response_schema(web_schemas.VerifyPresentationResponse(), 200, description="") +@tenant_authentication async def verify_presentation_route(request: web.BaseRequest): """Request handler for verifying a presentation. diff --git a/aries_cloudagent/vc/vc_ld/models/linked_data_proof.py b/aries_cloudagent/vc/vc_ld/models/linked_data_proof.py index 40e5a2b7db..6787e82be7 100644 --- a/aries_cloudagent/vc/vc_ld/models/linked_data_proof.py +++ b/aries_cloudagent/vc/vc_ld/models/linked_data_proof.py @@ -105,9 +105,6 @@ class Meta: domain = fields.Str( required=False, - # TODO the domain can be more than a Uri, provide a less restrictive validation - # https://www.w3.org/TR/vc-data-integrity/#defn-domain - validate=Uri(), metadata={ "description": ( "A string value specifying the restricted domain of the signature." diff --git a/aries_cloudagent/wallet/anoncreds_upgrade.py b/aries_cloudagent/wallet/anoncreds_upgrade.py new file mode 100644 index 0000000000..4e9f16e8bc --- /dev/null +++ b/aries_cloudagent/wallet/anoncreds_upgrade.py @@ -0,0 +1,719 @@ +"""Functions for upgrading records to anoncreds.""" + +import asyncio +import json +import logging +from typing import Optional + +from anoncreds import ( + CredentialDefinition, + CredentialDefinitionPrivate, + KeyCorrectnessProof, + RevocationRegistryDefinitionPrivate, + Schema, +) +from aries_askar import AskarError +from indy_credx import LinkSecret + +from ..anoncreds.issuer import ( + CATEGORY_CRED_DEF, + CATEGORY_CRED_DEF_KEY_PROOF, + CATEGORY_CRED_DEF_PRIVATE, + CATEGORY_SCHEMA, +) +from ..anoncreds.models.anoncreds_cred_def import CredDef, CredDefState +from ..anoncreds.models.anoncreds_revocation import ( + RevList, + RevListState, + RevRegDef, + RevRegDefState, + RevRegDefValue, +) +from ..anoncreds.models.anoncreds_schema import SchemaState +from ..anoncreds.revocation import ( + CATEGORY_REV_LIST, + CATEGORY_REV_REG_DEF, + CATEGORY_REV_REG_DEF_PRIVATE, +) +from ..cache.base import BaseCache +from ..core.profile import Profile +from ..indy.credx.holder import CATEGORY_LINK_SECRET, IndyCredxHolder +from ..ledger.multiple_ledger.ledger_requests_executor import ( + GET_CRED_DEF, + GET_SCHEMA, + IndyLedgerRequestsExecutor, +) +from ..messaging.credential_definitions.util import CRED_DEF_SENT_RECORD_TYPE +from ..messaging.schemas.util import SCHEMA_SENT_RECORD_TYPE +from ..multitenant.base import BaseMultitenantManager +from ..revocation.models.issuer_cred_rev_record import IssuerCredRevRecord +from ..revocation.models.issuer_rev_reg_record import IssuerRevRegRecord +from ..storage.base import BaseStorage +from ..storage.error import StorageNotFoundError +from ..storage.record import StorageRecord +from ..storage.type import ( + RECORD_TYPE_ACAPY_STORAGE_TYPE, + RECORD_TYPE_ACAPY_UPGRADING, + STORAGE_TYPE_VALUE_ANONCREDS, +) +from .singletons import IsAnoncredsSingleton, UpgradeInProgressSingleton + +LOGGER = logging.getLogger(__name__) + +UPGRADING_RECORD_IN_PROGRESS = "anoncreds_in_progress" +UPGRADING_RECORD_FINISHED = "anoncreds_finished" + +# Number of times to retry upgrading records +max_retries = 5 + + +class SchemaUpgradeObj: + """Schema upgrade object.""" + + def __init__( + self, + schema_id: str, + schema: Schema, + name: str, + version: str, + issuer_id: str, + old_record_id: str, + ): + """Initialize schema upgrade object.""" + self.schema_id = schema_id + self.schema = schema + self.name = name + self.version = version + self.issuer_id = issuer_id + self.old_record_id = old_record_id + + +class CredDefUpgradeObj: + """Cred def upgrade object.""" + + def __init__( + self, + cred_def_id: str, + cred_def: CredentialDefinition, + cred_def_private: CredentialDefinitionPrivate, + key_proof: KeyCorrectnessProof, + revocation: Optional[bool] = None, + askar_cred_def: Optional[any] = None, + max_cred_num: Optional[int] = None, + ): + """Initialize cred def upgrade object.""" + self.cred_def_id = cred_def_id + self.cred_def = cred_def + self.cred_def_private = cred_def_private + self.key_proof = key_proof + self.revocation = revocation + self.askar_cred_def = askar_cred_def + self.max_cred_num = max_cred_num + + +class RevRegDefUpgradeObj: + """Rev reg def upgrade object.""" + + def __init__( + self, + rev_reg_def_id: str, + rev_reg_def: RevRegDef, + rev_reg_def_private: RevocationRegistryDefinitionPrivate, + active: bool = False, + ): + """Initialize rev reg def upgrade object.""" + self.rev_reg_def_id = rev_reg_def_id + self.rev_reg_def = rev_reg_def + self.rev_reg_def_private = rev_reg_def_private + self.active = active + + +class RevListUpgradeObj: + """Rev entry upgrade object.""" + + def __init__( + self, + rev_list: RevList, + pending: list, + rev_reg_def_id: str, + cred_rev_records: list, + ): + """Initialize rev entry upgrade object.""" + self.rev_list = rev_list + self.pending = pending + self.rev_reg_def_id = rev_reg_def_id + self.cred_rev_records = cred_rev_records + + +async def get_schema_upgrade_object( + profile: Profile, schema_id: str, askar_schema +) -> SchemaUpgradeObj: + """Get schema upgrade object.""" + + async with profile.session() as session: + schema_id = askar_schema.tags.get("schema_id") + issuer_did = askar_schema.tags.get("schema_issuer_did") + # Need to get schema from the ledger because the attribute names + # are not stored in the wallet + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) + else: + ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) + + _, ledger = await ledger_exec_inst.get_ledger_for_identifier( + schema_id, + txn_record_type=GET_SCHEMA, + ) + async with ledger: + schema_from_ledger = await ledger.get_schema(schema_id) + + return SchemaUpgradeObj( + schema_id, + Schema.create( + schema_id, + askar_schema.tags.get("schema_name"), + issuer_did, + schema_from_ledger["attrNames"], + ), + askar_schema.tags.get("schema_name"), + askar_schema.tags.get("schema_version"), + issuer_did, + askar_schema.id, + ) + + +async def get_cred_def_upgrade_object( + profile: Profile, askar_cred_def +) -> CredDefUpgradeObj: + """Get cred def upgrade object.""" + cred_def_id = askar_cred_def.tags.get("cred_def_id") + async with profile.session() as session: + # Need to get cred_def from the ledger because the tag + # is not stored in the wallet and don't know wether it supports revocation + multitenant_mgr = session.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) + else: + ledger_exec_inst = session.inject(IndyLedgerRequestsExecutor) + _, ledger = await ledger_exec_inst.get_ledger_for_identifier( + cred_def_id, + txn_record_type=GET_CRED_DEF, + ) + async with ledger: + cred_def_from_ledger = await ledger.get_credential_definition(cred_def_id) + + async with profile.session() as session: + storage = session.inject(BaseStorage) + askar_cred_def_private = await storage.get_record( + CATEGORY_CRED_DEF_PRIVATE, cred_def_id + ) + askar_cred_def_key_proof = await storage.get_record( + CATEGORY_CRED_DEF_KEY_PROOF, cred_def_id + ) + + cred_def = CredDef( + issuer_id=askar_cred_def.tags.get("issuer_did"), + schema_id=askar_cred_def.tags.get("schema_id"), + tag=cred_def_from_ledger["tag"], + type=cred_def_from_ledger["type"], + value=cred_def_from_ledger["value"], + ) + + return CredDefUpgradeObj( + cred_def_id, + cred_def, + askar_cred_def_private.value, + askar_cred_def_key_proof.value, + cred_def_from_ledger["value"].get("revocation", None), + askar_cred_def=askar_cred_def, + ) + + +async def get_rev_reg_def_upgrade_object( + profile: Profile, + cred_def_upgrade_obj: CredDefUpgradeObj, + askar_issuer_rev_reg_def, + is_active: bool, +) -> RevRegDefUpgradeObj: + """Get rev reg def upgrade object.""" + rev_reg_def_id = askar_issuer_rev_reg_def.tags.get("revoc_reg_id") + + async with profile.session() as session: + storage = session.inject(BaseStorage) + askar_reg_rev_def_private = await storage.get_record( + CATEGORY_REV_REG_DEF_PRIVATE, rev_reg_def_id + ) + + revoc_reg_def_values = json.loads(askar_issuer_rev_reg_def.value) + + reg_def_value = RevRegDefValue( + revoc_reg_def_values["revoc_reg_def"]["value"]["publicKeys"], + revoc_reg_def_values["revoc_reg_def"]["value"]["maxCredNum"], + revoc_reg_def_values["revoc_reg_def"]["value"]["tailsLocation"], + revoc_reg_def_values["revoc_reg_def"]["value"]["tailsHash"], + ) + + rev_reg_def = RevRegDef( + issuer_id=askar_issuer_rev_reg_def.tags.get("issuer_did"), + cred_def_id=cred_def_upgrade_obj.cred_def_id, + tag=revoc_reg_def_values["tag"], + type=revoc_reg_def_values["revoc_def_type"], + value=reg_def_value, + ) + + return RevRegDefUpgradeObj( + rev_reg_def_id, rev_reg_def, askar_reg_rev_def_private.value, is_active + ) + + +async def get_rev_list_upgrade_object( + profile: Profile, rev_reg_def_upgrade_obj: RevRegDefUpgradeObj +) -> RevListUpgradeObj: + """Get revocation entry upgrade object.""" + rev_reg = rev_reg_def_upgrade_obj.rev_reg_def + async with profile.session() as session: + storage = session.inject(BaseStorage) + askar_cred_rev_records = await storage.find_all_records( + IssuerCredRevRecord.RECORD_TYPE, + {"rev_reg_id": rev_reg_def_upgrade_obj.rev_reg_def_id}, + ) + + revocation_list = [0] * rev_reg.value.max_cred_num + for askar_cred_rev_record in askar_cred_rev_records: + if askar_cred_rev_record.tags.get("state") == "revoked": + revocation_list[int(askar_cred_rev_record.tags.get("cred_rev_id")) - 1] = 1 + + rev_list = RevList( + issuer_id=rev_reg.issuer_id, + rev_reg_def_id=rev_reg_def_upgrade_obj.rev_reg_def_id, + revocation_list=revocation_list, + current_accumulator=json.loads( + rev_reg_def_upgrade_obj.askar_issuer_rev_reg_def.value + )["revoc_reg_entry"]["value"]["accum"], + ) + + return RevListUpgradeObj( + rev_list, + json.loads(rev_reg_def_upgrade_obj.askar_issuer_rev_reg_def.value)[ + "pending_pub" + ], + rev_reg_def_upgrade_obj.rev_reg_def_id, + askar_cred_rev_records, + ) + + +async def upgrade_and_delete_schema_records( + txn, schema_upgrade_obj: SchemaUpgradeObj +) -> None: + """Upgrade and delete schema records.""" + schema_anoncreds = schema_upgrade_obj.schema + await txn.handle.remove("schema_sent", schema_upgrade_obj.old_record_id) + await txn.handle.replace( + CATEGORY_SCHEMA, + schema_upgrade_obj.schema_id, + schema_anoncreds.to_json(), + { + "name": schema_upgrade_obj.name, + "version": schema_upgrade_obj.version, + "issuer_id": schema_upgrade_obj.issuer_id, + "state": SchemaState.STATE_FINISHED, + }, + ) + + +async def upgrade_and_delete_cred_def_records( + txn, anoncreds_schema, cred_def_upgrade_obj: CredDefUpgradeObj +) -> None: + """Upgrade and delete cred def records.""" + cred_def_id = cred_def_upgrade_obj.cred_def_id + anoncreds_schema = anoncreds_schema.to_dict() + askar_cred_def = cred_def_upgrade_obj.askar_cred_def + await txn.handle.remove("cred_def_sent", askar_cred_def.id) + await txn.handle.replace( + CATEGORY_CRED_DEF, + cred_def_id, + cred_def_upgrade_obj.cred_def.to_json(), + tags={ + "schema_id": askar_cred_def.tags.get("schema_id"), + "schema_issuer_id": anoncreds_schema["issuerId"], + "issuer_id": askar_cred_def.tags.get("issuer_did"), + "schema_name": anoncreds_schema["name"], + "schema_version": anoncreds_schema["version"], + "state": CredDefState.STATE_FINISHED, + "epoch": askar_cred_def.tags.get("epoch"), + # TODO We need to keep track of these but tags probably + # isn't ideal. This suggests that a full record object + # is necessary for non-private values + "support_revocation": json.dumps(cred_def_upgrade_obj.revocation), + "max_cred_num": str(cred_def_upgrade_obj.max_cred_num or 0), + }, + ) + await txn.handle.replace( + CATEGORY_CRED_DEF_PRIVATE, + cred_def_id, + CredentialDefinitionPrivate.load( + cred_def_upgrade_obj.cred_def_private + ).to_json_buffer(), + ) + await txn.handle.replace( + CATEGORY_CRED_DEF_KEY_PROOF, + cred_def_id, + KeyCorrectnessProof.load(cred_def_upgrade_obj.key_proof).to_json_buffer(), + ) + + +rev_reg_states_mapping = { + "init": RevRegDefState.STATE_WAIT, + "generated": RevRegDefState.STATE_ACTION, + "posted": RevRegDefState.STATE_FINISHED, + "active": RevRegDefState.STATE_FINISHED, + "full": RevRegDefState.STATE_FULL, + "decommissioned": RevRegDefState.STATE_DECOMMISSIONED, +} + + +async def upgrade_and_delete_rev_reg_def_records( + txn, rev_reg_def_upgrade_obj: RevRegDefUpgradeObj +) -> None: + """Upgrade and delete rev reg def records.""" + rev_reg_def_id = rev_reg_def_upgrade_obj.rev_reg_def_id + askar_issuer_rev_reg_def = rev_reg_def_upgrade_obj.askar_issuer_rev_reg_def + await txn.handle.remove(IssuerRevRegRecord.RECORD_TYPE, askar_issuer_rev_reg_def.id) + await txn.handle.replace( + CATEGORY_REV_REG_DEF, + rev_reg_def_id, + rev_reg_def_upgrade_obj.rev_reg_def.to_json(), + tags={ + "cred_def_id": rev_reg_def_upgrade_obj.rev_reg_def.cred_def_id, + "issuer_id": askar_issuer_rev_reg_def.tags.get("issuer_did"), + "state": rev_reg_states_mapping[askar_issuer_rev_reg_def.tags.get("state")], + "active": json.dumps(rev_reg_def_upgrade_obj.active), + }, + ) + await txn.handle.replace( + CATEGORY_REV_REG_DEF_PRIVATE, + rev_reg_def_id, + RevocationRegistryDefinitionPrivate.load( + rev_reg_def_upgrade_obj.rev_reg_def_private + ).to_json_buffer(), + ) + + +async def upgrade_and_delete_rev_entry_records( + txn, rev_list_upgrade_obj: RevListUpgradeObj +) -> None: + """Upgrade and delete revocation entry records.""" + next_index = 0 + for cred_rev_record in rev_list_upgrade_obj.cred_rev_records: + if int(cred_rev_record.tags.get("cred_rev_id")) > next_index: + next_index = int(cred_rev_record.tags.get("cred_rev_id")) + await txn.handle.remove(IssuerCredRevRecord.RECORD_TYPE, cred_rev_record.id) + + await txn.handle.insert( + CATEGORY_REV_LIST, + rev_list_upgrade_obj.rev_reg_def_id, + value_json={ + "rev_list": rev_list_upgrade_obj.rev_list.serialize(), + "pending": rev_list_upgrade_obj.pending, + "next_index": next_index + 1, + }, + tags={ + "state": RevListState.STATE_FINISHED, + "pending": json.dumps(rev_list_upgrade_obj.pending is not None), + }, + ) + + +async def upgrade_all_records_with_transaction( + txn: any, + schema_upgrade_objs: list[SchemaUpgradeObj], + cred_def_upgrade_objs: list[CredDefUpgradeObj], + rev_reg_def_upgrade_objs: list[RevRegDefUpgradeObj], + rev_list_upgrade_objs: list[RevListUpgradeObj], + link_secret: Optional[LinkSecret] = None, +) -> None: + """Upgrade all objects with transaction.""" + for schema_upgrade_obj in schema_upgrade_objs: + await upgrade_and_delete_schema_records(txn, schema_upgrade_obj) + for cred_def_upgrade_obj in cred_def_upgrade_objs: + await upgrade_and_delete_cred_def_records( + txn, schema_upgrade_obj.schema, cred_def_upgrade_obj + ) + for rev_reg_def_upgrade_obj in rev_reg_def_upgrade_objs: + await upgrade_and_delete_rev_reg_def_records(txn, rev_reg_def_upgrade_obj) + for rev_list_upgrade_obj in rev_list_upgrade_objs: + await upgrade_and_delete_rev_entry_records(txn, rev_list_upgrade_obj) + + if link_secret: + await txn.handle.replace( + CATEGORY_LINK_SECRET, + IndyCredxHolder.LINK_SECRET_ID, + link_secret.to_dict()["value"]["ms"].encode("ascii"), + ) + + await txn.commit() + + +async def get_rev_reg_def_upgrade_objs( + profile: Profile, + cred_def_upgrade_obj: CredDefUpgradeObj, + rev_list_upgrade_objs: list[RevListUpgradeObj], +) -> list[RevRegDefUpgradeObj]: + """Get rev reg def upgrade objects.""" + + rev_reg_def_upgrade_objs = [] + async with profile.session() as session: + storage = session.inject(BaseStorage) + # Must be sorted to find the active rev reg def + askar_issuer_rev_reg_def_records = sorted( + await storage.find_all_records( + IssuerRevRegRecord.RECORD_TYPE, + {"cred_def_id": cred_def_upgrade_obj.cred_def_id}, + ), + key=lambda x: json.loads(x.value)["created_at"], + ) + found_active = False + for askar_issuer_rev_reg_def in askar_issuer_rev_reg_def_records: + # active rev reg def is the oldest non-full and active rev reg def + if ( + not found_active + and askar_issuer_rev_reg_def.tags.get("state") != "full" + and askar_issuer_rev_reg_def.tags.get("state") == "active" + ): + found_active = True + is_active = True + + rev_reg_def_upgrade_obj = await get_rev_reg_def_upgrade_object( + profile, + cred_def_upgrade_obj, + askar_issuer_rev_reg_def, + is_active, + ) + is_active = False + rev_reg_def_upgrade_obj.askar_issuer_rev_reg_def = askar_issuer_rev_reg_def + + rev_reg_def_upgrade_objs.append(rev_reg_def_upgrade_obj) + + # add the revocation list upgrade object from reg def upgrade object + rev_list_upgrade_objs.append( + await get_rev_list_upgrade_object(profile, rev_reg_def_upgrade_obj) + ) + return rev_reg_def_upgrade_objs + + +async def convert_records_to_anoncreds(profile) -> None: + """Convert and delete old askar records.""" + async with profile.session() as session: + storage = session.inject(BaseStorage) + askar_schema_records = await storage.find_all_records(SCHEMA_SENT_RECORD_TYPE) + + schema_upgrade_objs = [] + cred_def_upgrade_objs = [] + rev_reg_def_upgrade_objs = [] + rev_list_upgrade_objs = [] + + # Schemas + for askar_schema in askar_schema_records: + schema_upgrade_objs.append( + await get_schema_upgrade_object(profile, askar_schema.id, askar_schema) + ) + + # CredDefs and Revocation Objects + askar_cred_def_records = await storage.find_all_records( + CRED_DEF_SENT_RECORD_TYPE, {} + ) + for askar_cred_def in askar_cred_def_records: + cred_def_upgrade_obj = await get_cred_def_upgrade_object( + profile, askar_cred_def + ) + rev_reg_def_upgrade_objs = await get_rev_reg_def_upgrade_objs( + profile, cred_def_upgrade_obj, rev_list_upgrade_objs + ) + # update the cred_def with the max_cred_num from first rev_reg_def + if rev_reg_def_upgrade_objs: + cred_def_upgrade_obj.max_cred_num = rev_reg_def_upgrade_objs[ + 0 + ].rev_reg_def.value.max_cred_num + cred_def_upgrade_objs.append(cred_def_upgrade_obj) + + # Link secret + link_secret_record = None + try: + link_secret_record = await session.handle.fetch( + CATEGORY_LINK_SECRET, IndyCredxHolder.LINK_SECRET_ID + ) + except AskarError: + pass + + link_secret = None + if link_secret_record: + link_secret = LinkSecret.load(link_secret_record.raw_value) + + async with profile.transaction() as txn: + try: + await upgrade_all_records_with_transaction( + txn, + schema_upgrade_objs, + cred_def_upgrade_objs, + rev_reg_def_upgrade_objs, + rev_list_upgrade_objs, + link_secret, + ) + except Exception as e: + await txn.rollback() + raise e + + +async def retry_converting_records( + profile: Profile, upgrading_record: StorageRecord, retry: int, is_subwallet=False +) -> None: + """Retry converting records to anoncreds.""" + + async def fail_upgrade(): + async with profile.session() as session: + storage = session.inject(BaseStorage) + await storage.delete_record(upgrading_record) + + try: + await convert_records_to_anoncreds(profile) + await finish_upgrade_by_updating_profile_or_shutting_down(profile, is_subwallet) + LOGGER.info(f"Upgrade complete via retry for wallet: {profile.name}") + except Exception as e: + LOGGER.error(f"Error when upgrading records for wallet {profile.name} : {e} ") + if retry < max_retries: + LOGGER.info(f"Retry attempt {retry + 1} to upgrade wallet {profile.name}") + await asyncio.sleep(1) + await retry_converting_records( + profile, upgrading_record, retry + 1, is_subwallet + ) + else: + LOGGER.error( + f"""Failed to upgrade wallet: {profile.name} after 5 retries. + Try fixing any connection issues and re-running the update""" + ) + await fail_upgrade() + + +async def upgrade_wallet_to_anoncreds_if_requested( + profile: Profile, is_subwallet=False +) -> None: + """Get upgrading record and attempt to upgrade wallet to anoncreds.""" + async with profile.session() as session: + storage = session.inject(BaseStorage) + try: + upgrading_record = await storage.find_record( + RECORD_TYPE_ACAPY_UPGRADING, {} + ) + if upgrading_record.value == UPGRADING_RECORD_FINISHED: + IsAnoncredsSingleton().set_wallet(profile.name) + return + except StorageNotFoundError: + return + + try: + LOGGER.info("Upgrade in process for wallet: %s", profile.name) + await convert_records_to_anoncreds(profile) + await finish_upgrade_by_updating_profile_or_shutting_down( + profile, is_subwallet + ) + except Exception as e: + LOGGER.error(f"Error when upgrading wallet {profile.name} : {e} ") + await retry_converting_records(profile, upgrading_record, 0, is_subwallet) + + +async def finish_upgrade(profile: Profile): + """Finish record by setting records and caches.""" + async with profile.session() as session: + storage = session.inject(BaseStorage) + try: + storage_type_record = await storage.find_record( + type_filter=RECORD_TYPE_ACAPY_STORAGE_TYPE, tag_query={} + ) + await storage.update_record( + storage_type_record, STORAGE_TYPE_VALUE_ANONCREDS, {} + ) + # This should only happen for subwallets + except StorageNotFoundError: + await storage.add_record( + StorageRecord( + RECORD_TYPE_ACAPY_STORAGE_TYPE, + STORAGE_TYPE_VALUE_ANONCREDS, + ) + ) + await finish_upgrading_record(profile) + IsAnoncredsSingleton().set_wallet(profile.name) + UpgradeInProgressSingleton().remove_wallet(profile.name) + + +async def finish_upgrading_record(profile: Profile): + """Update upgrading record to finished.""" + async with profile.session() as session: + storage = session.inject(BaseStorage) + try: + upgrading_record = await storage.find_record( + RECORD_TYPE_ACAPY_UPGRADING, tag_query={} + ) + await storage.update_record(upgrading_record, UPGRADING_RECORD_FINISHED, {}) + except StorageNotFoundError: + return + + +async def upgrade_subwallet(profile: Profile) -> None: + """Upgrade subwallet to anoncreds.""" + async with profile.session() as session: + multitenant_mgr = session.inject_or(BaseMultitenantManager) + wallet_id = profile.settings.get("wallet.id") + cache = profile.inject_or(BaseCache) + await cache.flush() + settings = {"wallet.type": STORAGE_TYPE_VALUE_ANONCREDS} + await multitenant_mgr.update_wallet(wallet_id, settings) + + +async def finish_upgrade_by_updating_profile_or_shutting_down( + profile: Profile, is_subwallet=False +): + """Upgrade wallet to anoncreds and set storage type.""" + if is_subwallet: + await upgrade_subwallet(profile) + await finish_upgrade(profile) + LOGGER.info( + f"""Upgrade of subwallet {profile.settings.get('wallet.name')} has completed. Profile is now askar-anoncreds""" # noqa: E501 + ) + else: + await finish_upgrade(profile) + LOGGER.info( + f"Upgrade of base wallet {profile.settings.get('wallet.name')} to anoncreds has completed. Shutting down agent." # noqa: E501 + ) + asyncio.get_event_loop().stop() + + +async def check_upgrade_completion_loop(profile: Profile, is_subwallet=False): + """Check if upgrading is complete.""" + async with profile.session() as session: + while True: + storage = session.inject(BaseStorage) + LOGGER.debug(f"Checking upgrade completion for wallet: {profile.name}") + try: + upgrading_record = await storage.find_record( + RECORD_TYPE_ACAPY_UPGRADING, tag_query={} + ) + if upgrading_record.value == UPGRADING_RECORD_FINISHED: + IsAnoncredsSingleton().set_wallet(profile.name) + UpgradeInProgressSingleton().remove_wallet(profile.name) + if is_subwallet: + await upgrade_subwallet(profile) + LOGGER.info( + f"""Upgrade of subwallet {profile.settings.get('wallet.name')} has completed. Profile is now askar-anoncreds""" # noqa: E501 + ) + return + LOGGER.info( + f"Upgrade complete for wallet: {profile.name}, shutting down agent." # noqa: E501 + ) + # Shut down agent if base wallet + asyncio.get_event_loop().stop() + except StorageNotFoundError: + # If the record is not found, the upgrade failed + return + + await asyncio.sleep(1) diff --git a/aries_cloudagent/wallet/indy.py b/aries_cloudagent/wallet/indy.py deleted file mode 100644 index e8d33b6652..0000000000 --- a/aries_cloudagent/wallet/indy.py +++ /dev/null @@ -1,953 +0,0 @@ -"""Indy implementation of BaseWallet interface.""" - -import json -import logging - -from typing import List, Optional, Sequence, Tuple, Union - -import indy.anoncreds -import indy.did -import indy.crypto -import indy.wallet - -from indy.error import IndyError, ErrorCode - -from ..did.did_key import DIDKey -from ..indy.sdk.error import IndyErrorHandler -from ..indy.sdk.wallet_setup import IndyOpenWallet -from ..ledger.base import BaseLedger -from ..ledger.endpoint_type import EndpointType -from ..ledger.error import LedgerConfigError -from ..storage.indy import IndySdkStorage -from ..storage.error import StorageDuplicateError, StorageNotFoundError -from ..storage.record import StorageRecord - -from .base import BaseWallet -from .crypto import ( - create_keypair, - sign_message, - validate_seed, - verify_signed_message, -) -from .did_info import DIDInfo, KeyInfo -from .did_method import SOV, KEY, DIDMethod -from .error import WalletError, WalletDuplicateError, WalletNotFoundError -from .key_pair import KeyPairStorageManager -from .key_type import BLS12381G2, ED25519, KeyType, KeyTypes -from .util import b58_to_bytes, bytes_to_b58, bytes_to_b64 - - -LOGGER = logging.getLogger(__name__) - -RECORD_TYPE_CONFIG = "config" -RECORD_NAME_PUBLIC_DID = "default_public_did" - - -class IndySdkWallet(BaseWallet): - """Indy identity wallet implementation.""" - - def __init__(self, opened: IndyOpenWallet): - """Create a new IndySdkWallet instance.""" - self.opened: IndyOpenWallet = opened - - def __did_info_from_indy_info(self, info): - metadata = json.loads(info["metadata"]) if info["metadata"] else {} - did: str = info["did"] - verkey = info["verkey"] - - method = KEY if did.startswith("did:key") else SOV - key_type = ED25519 - - if method == KEY: - did = DIDKey.from_public_key_b58(info["verkey"], key_type).did - - return DIDInfo( - did=did, verkey=verkey, metadata=metadata, method=method, key_type=key_type - ) - - def __did_info_from_key_pair_info(self, info: dict): - metadata = info["metadata"] - verkey = info["verkey"] - - # TODO: inject context to support did method registry - method = SOV if metadata.get("method", "key") == SOV.method_name else KEY - # TODO: inject context to support keytype registry - key_types = KeyTypes() - key_type = key_types.from_key_type(info["key_type"]) - - if method == KEY: - did = DIDKey.from_public_key_b58(info["verkey"], key_type).did - - return DIDInfo( - did=did, verkey=verkey, metadata=metadata, method=method, key_type=key_type - ) - - async def __create_indy_signing_key( - self, key_type: KeyType, metadata: dict, seed: str = None - ) -> str: - if key_type != ED25519: - raise WalletError(f"Unsupported key type: {key_type.key_type}") - - args = {} - if seed: - args["seed"] = bytes_to_b64(validate_seed(seed)) - try: - verkey = await indy.crypto.create_key(self.opened.handle, json.dumps(args)) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletItemAlreadyExists: - raise WalletDuplicateError("Verification key already present in wallet") - raise IndyErrorHandler.wrap_error( - x_indy, "Wallet {} error".format(self.opened.name), WalletError - ) from x_indy - - await indy.crypto.set_key_metadata( - self.opened.handle, verkey, json.dumps(metadata) - ) - - return verkey - - async def __create_keypair_signing_key( - self, key_type: KeyType, metadata: dict, seed: str = None - ) -> str: - if key_type != BLS12381G2: - raise WalletError(f"Unsupported key type: {key_type.key_type}") - - public_key, secret_key = create_keypair(key_type, validate_seed(seed)) - verkey = bytes_to_b58(public_key) - key_pair_mgr = KeyPairStorageManager(IndySdkStorage(self.opened)) - - # Check if key already exists - try: - key_info = await self.__get_keypair_signing_key(verkey) - if key_info: - raise WalletDuplicateError("Verification key already present in wallet") - except WalletNotFoundError: - # If we can't find the key, it means it doesn't exist already - # this is good - pass - - await key_pair_mgr.store_key_pair( - public_key=public_key, - secret_key=secret_key, - key_type=key_type, - metadata=metadata, - ) - - return verkey - - async def create_signing_key( - self, - key_type: KeyType, - seed: Optional[str] = None, - metadata: Optional[dict] = None, - ) -> KeyInfo: - """Create a new public/private signing keypair. - - Args: - seed: Seed for key - metadata: Optional metadata to store with the keypair - - Returns: - A `KeyInfo` representing the new record - - Raises: - WalletDuplicateError: If the resulting verkey already exists in the wallet - WalletError: If there is a libindy error - - """ - return await self.create_key(key_type, seed, metadata) - - async def create_key( - self, - key_type: KeyType, - seed: Optional[str] = None, - metadata: Optional[dict] = None, - ) -> KeyInfo: - """Create a new public/private keypair. - - Args: - key_type: Key type to create - seed: Seed for key - metadata: Optional metadata to store with the keypair - - Returns: - A `KeyInfo` representing the new record - - Raises: - WalletDuplicateError: If the resulting verkey already exists in the wallet - WalletError: If there is another backend error - """ - - # must save metadata to allow identity check - # otherwise get_key_metadata just returns WalletItemNotFound - if metadata is None: - metadata = {} - - # All ed25519 keys are handled by indy - if key_type == ED25519: - verkey = await self.__create_indy_signing_key(key_type, metadata, seed) - # All other (only bls12381g2 atm) are handled outside of indy - else: - verkey = await self.__create_keypair_signing_key(key_type, metadata, seed) - - return KeyInfo(verkey=verkey, metadata=metadata, key_type=key_type) - - async def __get_indy_signing_key(self, verkey: str) -> KeyInfo: - try: - metadata = await indy.crypto.get_key_metadata(self.opened.handle, verkey) - - return KeyInfo( - verkey=verkey, - metadata=json.loads(metadata) if metadata else {}, - key_type=ED25519, - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletItemNotFound: - raise WalletNotFoundError(f"Unknown key: {verkey}") - # # If we resolve a key that is not 32 bytes we get CommonInvalidStructure - # elif x_indy.error_code == ErrorCode.CommonInvalidStructure: - # raise WalletNotFoundError(f"Unknown key: {verkey}") - else: - raise IndyErrorHandler.wrap_error( - x_indy, "Wallet {} error".format(self.opened.name), WalletError - ) from x_indy - - async def __get_keypair_signing_key(self, verkey: str) -> KeyInfo: - try: - key_pair_mgr = KeyPairStorageManager(IndySdkStorage(self.opened)) - key_pair = await key_pair_mgr.get_key_pair(verkey) - # TODO: inject context to support more keytypes - key_types = KeyTypes() - return KeyInfo( - verkey=verkey, - metadata=key_pair["metadata"], - key_type=key_types.from_key_type(key_pair["key_type"]) or BLS12381G2, - ) - except StorageNotFoundError: - raise WalletNotFoundError(f"Unknown key: {verkey}") - except StorageDuplicateError: - raise WalletDuplicateError(f"Multiple keys exist for verkey: {verkey}") - - async def get_signing_key(self, verkey: str) -> KeyInfo: - """Fetch info for a signing keypair. - - Args: - verkey: The verification key of the keypair - - Returns: - A `KeyInfo` representing the keypair - - Raises: - WalletNotFoundError: If no keypair is associated with the verification key - WalletError: If there is a libindy error - - """ - if not verkey: - raise WalletError("Missing required input parameter: verkey") - - # Only try to load indy signing key if the verkey is 32 bytes - # this may change if indy is going to support verkeys of different byte length - if len(b58_to_bytes(verkey)) == 32: - try: - return await self.__get_indy_signing_key(verkey) - except WalletNotFoundError: - return await self.__get_keypair_signing_key(verkey) - else: - return await self.__get_keypair_signing_key(verkey) - - async def replace_signing_key_metadata(self, verkey: str, metadata: dict): - """Replace the metadata associated with a signing keypair. - - Args: - verkey: The verification key of the keypair - metadata: The new metadata to store - - Raises: - WalletNotFoundError: if no keypair is associated with the verification key - - """ - metadata = metadata or {} - - # throw exception if key is undefined - key_info = await self.get_signing_key(verkey) - - # All ed25519 keys are handled by indy - if key_info.key_type == ED25519: - await indy.crypto.set_key_metadata( - self.opened.handle, verkey, json.dumps(metadata) - ) - # All other (only bls12381g2 atm) are handled outside of indy - else: - key_pair_mgr = KeyPairStorageManager(IndySdkStorage(self.opened)) - await key_pair_mgr.update_key_pair_metadata( - verkey=key_info.verkey, metadata=metadata - ) - - async def rotate_did_keypair_start(self, did: str, next_seed: str = None) -> str: - """Begin key rotation for DID that wallet owns: generate new keypair. - - Args: - did: signing DID - next_seed: incoming replacement seed (default random) - - Returns: - The new verification key - - """ - # Check if DID can rotate keys - # TODO: inject context for did method registry support - method_name = did.split(":")[1] if did.startswith("did:") else SOV.method_name - did_method = SOV if method_name == SOV.method_name else KEY - if not did_method.supports_rotation: - raise WalletError( - f"DID method '{did_method.method_name}' does not support key rotation." - ) - - try: - verkey = await indy.did.replace_keys_start( - self.opened.handle, - did, - json.dumps( - {"seed": bytes_to_b64(validate_seed(next_seed))} - if next_seed - else {} - ), - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletItemNotFound: - raise WalletNotFoundError("Wallet owns no such DID: {}".format(did)) - raise IndyErrorHandler.wrap_error( - x_indy, "Wallet {} error".format(self.opened.name), WalletError - ) from x_indy - - return verkey - - async def rotate_did_keypair_apply(self, did: str) -> DIDInfo: - """Apply temporary keypair as main for DID that wallet owns. - - Args: - did: signing DID - - Returns: - DIDInfo with new verification key and metadata for DID - - """ - try: - await indy.did.replace_keys_apply(self.opened.handle, did) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletItemNotFound: - raise WalletNotFoundError("Wallet owns no such DID: {}".format(did)) - raise IndyErrorHandler.wrap_error( - x_indy, "Wallet {} error".format(self.opened.name), WalletError - ) from x_indy - - async def __create_indy_local_did( - self, - method: DIDMethod, - key_type: KeyType, - metadata: dict = None, - seed: str = None, - *, - did: str = None, - ) -> DIDInfo: - if method not in [SOV, KEY]: - raise WalletError( - f"Unsupported DID method for indy storage: {method.method_name}" - ) - if key_type != ED25519: - raise WalletError( - f"Unsupported key type for indy storage: {key_type.key_type}" - ) - - cfg = {} - if seed: - cfg["seed"] = bytes_to_b64(validate_seed(seed)) - if did: - cfg["did"] = did - # Create fully qualified did. This helps with determining the - # did method when retrieving - if method != SOV: - cfg["method_name"] = method.method_name - did_json = json.dumps(cfg) - # crypto_type, cid - optional parameters skipped - try: - did, verkey = await indy.did.create_and_store_my_did( - self.opened.handle, did_json - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.DidAlreadyExistsError: - raise WalletDuplicateError("DID already present in wallet") - raise IndyErrorHandler.wrap_error( - x_indy, "Wallet {} error".format(self.opened.name), WalletError - ) from x_indy - - # did key uses different format - if method == KEY: - did = DIDKey.from_public_key_b58(verkey, key_type).did - - await self.replace_local_did_metadata(did, metadata or {}) - - return DIDInfo( - did=did, - verkey=verkey, - metadata=metadata or {}, - method=method, - key_type=key_type, - ) - - async def __create_keypair_local_did( - self, - method: DIDMethod, - key_type: KeyType, - metadata: dict = None, - seed: str = None, - ) -> DIDInfo: - if method != KEY: - raise WalletError( - f"Unsupported DID method for keypair storage: {method.method_name}" - ) - if key_type != BLS12381G2: - raise WalletError( - f"Unsupported key type for keypair storage: {key_type.key_type}" - ) - - public_key, secret_key = create_keypair(key_type, validate_seed(seed)) - key_pair_mgr = KeyPairStorageManager(IndySdkStorage(self.opened)) - # should change if other did methods are supported - did_key = DIDKey.from_public_key(public_key, key_type) - - if not metadata: - metadata = {} - metadata["method"] = method.method_name - - await key_pair_mgr.store_key_pair( - public_key=public_key, - secret_key=secret_key, - key_type=key_type, - metadata=metadata, - tags={"method": method.method_name}, - ) - - return DIDInfo( - did=did_key.did, - verkey=did_key.public_key_b58, - metadata=metadata, - method=method, - key_type=key_type, - ) - - async def create_local_did( - self, - method: DIDMethod, - key_type: KeyType, - seed: Optional[str] = None, - did: Optional[str] = None, - metadata: Optional[dict] = None, - ) -> DIDInfo: - """Create and store a new local DID. - - Args: - method: The method to use for the DID - key_type: The key type to use for the DID - seed: Optional seed to use for DID - did: The DID to use - metadata: Metadata to store with DID - - Returns: - A `DIDInfo` instance representing the created DID - - Raises: - WalletDuplicateError: If the DID already exists in the wallet - WalletError: If there is a libindy error - - """ - - # validate key_type - if not method.supports_key_type(key_type): - raise WalletError( - f"Invalid key type {key_type.key_type}" - f" for DID method {method.method_name}" - ) - - if method == KEY and did: - raise WalletError("Not allowed to set DID for DID method 'key'") - - # All ed25519 keys are handled by indy - if key_type == ED25519: - return await self.__create_indy_local_did( - method, key_type, metadata, seed, did=did - ) - # All other (only bls12381g2 atm) are handled outside of indy - else: - return await self.__create_keypair_local_did( - method, key_type, metadata, seed - ) - - async def store_did(self, did_info: DIDInfo) -> DIDInfo: - """Store a DID in the wallet. - - This enables components external to the wallet to define how a DID - is created and then store it in the wallet for later use. - - Args: - did_info: The DID to store - - Returns: - The stored `DIDInfo` - """ - raise WalletError("This operation is not supported by Indy-SDK wallets") - - async def get_local_dids(self) -> Sequence[DIDInfo]: - """Get list of defined local DIDs. - - Returns: - A list of locally stored DIDs as `DIDInfo` instances - - """ - # retrieve indy dids - info_json = await indy.did.list_my_dids_with_meta(self.opened.handle) - info = json.loads(info_json) - ret = [] - for did in info: - ret.append(self.__did_info_from_indy_info(did)) - - # retrieve key pairs with method set to key - # this needs to change if more did methods are added - key_pair_mgr = KeyPairStorageManager(IndySdkStorage(self.opened)) - key_pairs = await key_pair_mgr.find_key_pairs( - tag_query={"method": KEY.method_name} - ) - for key_pair in key_pairs: - ret.append(self.__did_info_from_key_pair_info(key_pair)) - - return ret - - async def __get_indy_local_did( - self, method: DIDMethod, key_type: KeyType, did: str - ) -> DIDInfo: - if method not in [SOV, KEY]: - raise WalletError( - f"Unsupported DID method for indy storage: {method.method_name}" - ) - if key_type != ED25519: - raise WalletError( - f"Unsupported DID type for indy storage: {key_type.key_type}" - ) - - # key type is always ed25519, method not always key - if method == KEY and key_type == ED25519: - did_key = DIDKey.from_did(did) - - # Ed25519 did:keys are masked indy dids so transform to indy - # did with did:key prefix. - did = "did:key:" + bytes_to_b58(did_key.public_key[:16]) - try: - info_json = await indy.did.get_my_did_with_meta(self.opened.handle, did) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.WalletItemNotFound: - raise WalletNotFoundError("Unknown DID: {}".format(did)) - raise IndyErrorHandler.wrap_error( - x_indy, "Wallet {} error".format(self.opened.name), WalletError - ) from x_indy - info = json.loads(info_json) - return self.__did_info_from_indy_info(info) - - async def __get_keypair_local_did( - self, method: DIDMethod, key_type: KeyType, did: str - ): - if method != KEY: - raise WalletError( - f"Unsupported DID method for keypair storage: {method.method_name}" - ) - if key_type != BLS12381G2: - raise WalletError( - f"Unsupported DID type for keypair storage: {key_type.key_type}" - ) - - # method is always did:key - did_key = DIDKey.from_did(did) - - key_pair_mgr = KeyPairStorageManager(IndySdkStorage(self.opened)) - key_pair = await key_pair_mgr.get_key_pair(verkey=did_key.public_key_b58) - return self.__did_info_from_key_pair_info(key_pair) - - async def get_local_did(self, did: str) -> DIDInfo: - """Find info for a local DID. - - Args: - did: The DID for which to get info - - Returns: - A `DIDInfo` instance representing the found DID - - Raises: - WalletNotFoundError: If the DID is not found - WalletError: If there is a libindy error - - """ - # TODO: inject context for did method registry support - method_name = did.split(":")[1] if did.startswith("did:") else SOV.method_name - method = SOV if method_name == SOV.method_name else KEY - key_type = ED25519 - - # If did key, the key type can differ - if method == KEY: - did_key = DIDKey.from_did(did) - key_type = did_key.key_type - - if key_type == ED25519: - return await self.__get_indy_local_did(method, key_type, did) - else: - return await self.__get_keypair_local_did(method, key_type, did) - - async def get_local_did_for_verkey(self, verkey: str) -> DIDInfo: - """Resolve a local DID from a verkey. - - Args: - verkey: The verkey for which to get the local DID - - Returns: - A `DIDInfo` instance representing the found DID - - Raises: - WalletNotFoundError: If the verkey is not found - - """ - - dids = await self.get_local_dids() - for info in dids: - if info.verkey == verkey: - return info - raise WalletNotFoundError("No DID defined for verkey: {}".format(verkey)) - - async def replace_local_did_metadata(self, did: str, metadata: dict): - """Replace metadata for a local DID. - - Args: - did: The DID for which to replace metadata - metadata: The new metadata - - """ - if not metadata: - metadata = {} - did_info = await self.get_local_did(did) # throw exception if undefined - - # ed25519 keys are handled by indy - if did_info.key_type == ED25519: - try: - await indy.did.set_did_metadata( - self.opened.handle, did, json.dumps(metadata) - ) - except IndyError as x_indy: - raise IndyErrorHandler.wrap_error( - x_indy, "Wallet {} error".format(self.opened.name), WalletError - ) from x_indy - # all other keys are handled by key pair - else: - key_pair_mgr = KeyPairStorageManager(IndySdkStorage(self.opened)) - await key_pair_mgr.update_key_pair_metadata( - verkey=did_info.verkey, metadata=metadata - ) - - async def get_public_did(self) -> DIDInfo: - """Retrieve the public DID. - - Returns: - The currently public `DIDInfo`, if any - - """ - - public_did = None - public_info = None - public_item = None - storage = IndySdkStorage(self.opened) - try: - public_item = await storage.get_record( - RECORD_TYPE_CONFIG, RECORD_NAME_PUBLIC_DID - ) - except StorageNotFoundError: - # populate public DID record - # this should only happen once, for an upgraded wallet - # the 'public' metadata flag is no longer used - dids = await self.get_local_dids() - for info in dids: - if info.metadata.get("public"): - public_did = info.did - public_info = info - break - try: - # even if public is not set, store a record - # to avoid repeated queries - await storage.add_record( - StorageRecord( - type=RECORD_TYPE_CONFIG, - id=RECORD_NAME_PUBLIC_DID, - value=json.dumps({"did": public_did}), - ) - ) - except StorageDuplicateError: - # another process stored the record first - public_item = await storage.get_record( - RECORD_TYPE_CONFIG, RECORD_NAME_PUBLIC_DID - ) - if public_item: - public_did = json.loads(public_item.value)["did"] - if public_did: - try: - public_info = await self.get_local_did(public_did) - except WalletNotFoundError: - pass - - return public_info - - async def set_public_did(self, did: Union[str, DIDInfo]) -> DIDInfo: - """Assign the public DID. - - Returns: - The updated `DIDInfo` - - """ - - if isinstance(did, str): - # will raise an exception if not found - info = await self.get_local_did(did) - else: - info = did - - public = await self.get_public_did() - if not public or public.did != info.did: - if not info.metadata.get("posted"): - metadata = {**info.metadata, "posted": True} - await self.replace_local_did_metadata(info.did, metadata) - info = info._replace(metadata=metadata) - storage = IndySdkStorage(self.opened) - await storage.update_record( - StorageRecord( - type=RECORD_TYPE_CONFIG, - id=RECORD_NAME_PUBLIC_DID, - value="{}", - ), - value=json.dumps({"did": info.did}), - tags=None, - ) - public = info - - return public - - async def set_did_endpoint( - self, - did: str, - endpoint: str, - ledger: BaseLedger, - endpoint_type: EndpointType = None, - write_ledger: bool = True, - endorser_did: str = None, - routing_keys: List[str] = None, - ): - """Update the endpoint for a DID in the wallet, send to ledger if posted. - - Args: - did: DID for which to set endpoint - endpoint: the endpoint to set, None to clear - ledger: the ledger to which to send endpoint update if - DID is public or posted - endpoint_type: the type of the endpoint/service. Only endpoint_type - 'endpoint' affects local wallet - """ - did_info = await self.get_local_did(did) - if did_info.method != SOV: - raise WalletError("Setting DID endpoint is only allowed for did:sov DIDs") - - metadata = {**did_info.metadata} - if not endpoint_type: - endpoint_type = EndpointType.ENDPOINT - if endpoint_type == EndpointType.ENDPOINT: - metadata[endpoint_type.indy] = endpoint - - wallet_public_didinfo = await self.get_public_did() - if ( - wallet_public_didinfo and wallet_public_didinfo.did == did - ) or did_info.metadata.get("posted"): - # if DID on ledger, set endpoint there first - if not ledger: - raise LedgerConfigError( - f"No ledger available but DID {did} is public: missing wallet-type?" - ) - if not ledger.read_only: - async with ledger: - attrib_def = await ledger.update_endpoint_for_did( - did, - endpoint, - endpoint_type, - write_ledger=write_ledger, - endorser_did=endorser_did, - routing_keys=routing_keys, - ) - if not write_ledger: - return attrib_def - - await self.replace_local_did_metadata(did, metadata) - - async def sign_message(self, message: bytes, from_verkey: str) -> bytes: - """Sign a message using the private key associated with a given verkey. - - Args: - message: Message bytes to sign - from_verkey: The verkey to use to sign - - Returns: - A signature - - Raises: - WalletError: If the message is not provided - WalletError: If the verkey is not provided - WalletError: If a libindy error occurs - - """ - if not message: - raise WalletError("Message not provided") - if not from_verkey: - raise WalletError("Verkey not provided") - - try: - key_info = await self.get_signing_key(from_verkey) - except WalletNotFoundError: - key_info = await self.get_local_did_for_verkey(from_verkey) - - # ed25519 keys are handled by indy - if key_info.key_type == ED25519: - try: - result = await indy.crypto.crypto_sign( - self.opened.handle, from_verkey, message - ) - except IndyError: - raise WalletError("Exception when signing message") - # other keys are handled outside of indy - else: - key_pair_mgr = KeyPairStorageManager(IndySdkStorage(self.opened)) - key_pair = await key_pair_mgr.get_key_pair(verkey=key_info.verkey) - result = sign_message( - message=message, - secret=b58_to_bytes(key_pair["secret_key"]), - key_type=key_info.key_type, - ) - - return result - - async def verify_message( - self, - message: Union[List[bytes], bytes], - signature: bytes, - from_verkey: str, - key_type: KeyType, - ) -> bool: - """Verify a signature against the public key of the signer. - - Args: - message: Message to verify - signature: Signature to verify - from_verkey: Verkey to use in verification - - Returns: - True if verified, else False - - Raises: - WalletError: If the verkey is not provided - WalletError: If the signature is not provided - WalletError: If the message is not provided - WalletError: If a libindy error occurs - - """ - if not from_verkey: - raise WalletError("Verkey not provided") - if not signature: - raise WalletError("Signature not provided") - if not message: - raise WalletError("Message not provided") - - # ed25519 keys are handled by indy - if key_type == ED25519: - try: - result = await indy.crypto.crypto_verify( - from_verkey, message, signature - ) - except IndyError as x_indy: - if x_indy.error_code == ErrorCode.CommonInvalidStructure: - result = False - else: - raise IndyErrorHandler.wrap_error( - x_indy, "Wallet {} error".format(self.opened.name), WalletError - ) from x_indy - return result - # all other keys (only bls12381g2 atm) are handled outside of indy - else: - return verify_signed_message( - message=message, - signature=signature, - verkey=b58_to_bytes(from_verkey), - key_type=key_type, - ) - - async def pack_message( - self, message: str, to_verkeys: Sequence[str], from_verkey: str = None - ) -> bytes: - """Pack a message for one or more recipients. - - Args: - message: The message to pack - to_verkeys: List of verkeys for which to pack - from_verkey: Sender verkey from which to pack - - Returns: - The resulting packed message bytes - - Raises: - WalletError: If no message is provided - WalletError: If a libindy error occurs - - """ - if message is None: - raise WalletError("Message not provided") - try: - result = await indy.crypto.pack_message( - self.opened.handle, message, to_verkeys, from_verkey - ) - except IndyError as x_indy: - raise IndyErrorHandler.wrap_error( - x_indy, "Exception when packing message", WalletError - ) from x_indy - - return result - - async def unpack_message(self, enc_message: bytes) -> Tuple[str, str, str]: - """Unpack a message. - - Args: - enc_message: The packed message bytes - - Returns: - A tuple: (message, from_verkey, to_verkey) - - Raises: - WalletError: If the message is not provided - WalletError: If a libindy error occurs - - """ - if not enc_message: - raise WalletError("Message not provided") - try: - unpacked_json = await indy.crypto.unpack_message( - self.opened.handle, enc_message - ) - except IndyError: - raise WalletError("Exception when unpacking message") - unpacked = json.loads(unpacked_json) - message = unpacked["message"] - to_verkey = unpacked.get("recipient_verkey", None) - from_verkey = unpacked.get("sender_verkey", None) - return message, from_verkey, to_verkey - - @classmethod - async def generate_wallet_key(self, seed: str = None) -> str: - """Generate a raw Indy wallet key.""" - return await indy.wallet.generate_wallet_key(seed) diff --git a/aries_cloudagent/wallet/routes.py b/aries_cloudagent/wallet/routes.py index 5dc222c0c7..7fb9c75ea5 100644 --- a/aries_cloudagent/wallet/routes.py +++ b/aries_cloudagent/wallet/routes.py @@ -1,5 +1,6 @@ """Wallet admin routes.""" +import asyncio import json import logging from typing import List, Optional, Tuple, Union @@ -10,6 +11,7 @@ from aries_cloudagent.connections.base_manager import BaseConnectionManager +from ..admin.decorators.auth import tenant_authentication from ..admin.request_context import AdminRequestContext from ..config.injection_context import InjectionContext from ..connections.models.conn_record import ConnRecord @@ -55,15 +57,23 @@ is_author_role, ) from ..resolver.base import ResolverError +from ..storage.base import BaseStorage from ..storage.error import StorageError, StorageNotFoundError +from ..storage.record import StorageRecord +from ..storage.type import RECORD_TYPE_ACAPY_UPGRADING from ..wallet.jwt import jwt_sign, jwt_verify from ..wallet.sd_jwt import sd_jwt_sign, sd_jwt_verify +from .anoncreds_upgrade import ( + UPGRADING_RECORD_IN_PROGRESS, + upgrade_wallet_to_anoncreds_if_requested, +) from .base import BaseWallet from .did_info import DIDInfo from .did_method import KEY, PEER2, PEER4, SOV, DIDMethod, DIDMethods, HolderDefinedDid from .did_posture import DIDPosture from .error import WalletError, WalletNotFoundError from .key_type import BLS12381G2, ED25519, KeyTypes +from .singletons import UpgradeInProgressSingleton from .util import EVENT_LISTENER_PATTERN LOGGER = logging.getLogger(__name__) @@ -425,6 +435,7 @@ def format_did_info(info: DIDInfo): @docs(tags=["wallet"], summary="List wallet DIDs") @querystring_schema(DIDListQueryStringSchema()) @response_schema(DIDListSchema, 200, description="") +@tenant_authentication async def wallet_did_list(request: web.BaseRequest): """Request handler for searching wallet DIDs. @@ -532,6 +543,7 @@ async def wallet_did_list(request: web.BaseRequest): @docs(tags=["wallet"], summary="Create a local DID") @request_schema(DIDCreateSchema()) @response_schema(DIDResultSchema, 200, description="") +@tenant_authentication async def wallet_create_did(request: web.BaseRequest): """Request handler for creating a new local DID in the wallet. @@ -653,6 +665,7 @@ async def wallet_create_did(request: web.BaseRequest): @docs(tags=["wallet"], summary="Fetch the current public DID") @response_schema(DIDResultSchema, 200, description="") +@tenant_authentication async def wallet_get_public_did(request: web.BaseRequest): """Request handler for fetching the current public DID. @@ -683,6 +696,7 @@ async def wallet_get_public_did(request: web.BaseRequest): @querystring_schema(AttribConnIdMatchInfoSchema()) @querystring_schema(MediationIDSchema()) @response_schema(DIDResultSchema, 200, description="") +@tenant_authentication async def wallet_set_public_did(request: web.BaseRequest): """Request handler for setting the current public DID. @@ -928,6 +942,7 @@ async def promote_wallet_public_did( @querystring_schema(CreateAttribTxnForEndorserOptionSchema()) @querystring_schema(AttribConnIdMatchInfoSchema()) @response_schema(WalletModuleResponseSchema(), description="") +@tenant_authentication async def wallet_set_did_endpoint(request: web.BaseRequest): """Request handler for setting an endpoint for a DID. @@ -1046,6 +1061,7 @@ async def wallet_set_did_endpoint(request: web.BaseRequest): @docs(tags=["wallet"], summary="Create a EdDSA jws using did keys with a given payload") @request_schema(JWSCreateSchema) @response_schema(WalletModuleResponseSchema(), description="") +@tenant_authentication async def wallet_jwt_sign(request: web.BaseRequest): """Request handler for jws creation using did. @@ -1082,6 +1098,7 @@ async def wallet_jwt_sign(request: web.BaseRequest): ) @request_schema(SDJWSCreateSchema) @response_schema(WalletModuleResponseSchema(), description="") +@tenant_authentication async def wallet_sd_jwt_sign(request: web.BaseRequest): """Request handler for sd-jws creation using did. @@ -1118,6 +1135,7 @@ async def wallet_sd_jwt_sign(request: web.BaseRequest): @docs(tags=["wallet"], summary="Verify a EdDSA jws using did keys with a given JWS") @request_schema(JWSVerifySchema()) @response_schema(JWSVerifyResponseSchema(), 200, description="") +@tenant_authentication async def wallet_jwt_verify(request: web.BaseRequest): """Request handler for jws validation using did. @@ -1151,6 +1169,7 @@ async def wallet_jwt_verify(request: web.BaseRequest): ) @request_schema(SDJWSVerifySchema()) @response_schema(SDJWSVerifyResponseSchema(), 200, description="") +@tenant_authentication async def wallet_sd_jwt_verify(request: web.BaseRequest): """Request handler for sd-jws validation using did. @@ -1173,6 +1192,7 @@ async def wallet_sd_jwt_verify(request: web.BaseRequest): @docs(tags=["wallet"], summary="Query DID endpoint in wallet") @querystring_schema(DIDQueryStringSchema()) @response_schema(DIDEndpointSchema, 200, description="") +@tenant_authentication async def wallet_get_did_endpoint(request: web.BaseRequest): """Request handler for getting the current DID endpoint from the wallet. @@ -1206,6 +1226,7 @@ async def wallet_get_did_endpoint(request: web.BaseRequest): @docs(tags=["wallet"], summary="Rotate keypair for a DID not posted to the ledger") @querystring_schema(DIDQueryStringSchema()) @response_schema(WalletModuleResponseSchema(), description="") +@tenant_authentication async def wallet_rotate_did_keypair(request: web.BaseRequest): """Request handler for rotating local DID keypair. @@ -1241,6 +1262,74 @@ async def wallet_rotate_did_keypair(request: web.BaseRequest): return web.json_response({}) +class UpgradeVerificationSchema(OpenAPISchema): + """Parameters and validators for triggering an upgrade to anoncreds.""" + + wallet_name = fields.Str( + required=True, + metadata={ + "description": "Name of wallet to upgrade to anoncreds", + "example": "base-wallet", + }, + ) + + +class UpgradeResultSchema(OpenAPISchema): + """Result schema for upgrade.""" + + +@docs( + tags=["anoncreds - wallet upgrade"], + summary=""" + Upgrade the wallet from askar to anoncreds - Be very careful with this! You + cannot go back! See migration guide for more information. + """, +) +@querystring_schema(UpgradeVerificationSchema()) +@response_schema(UpgradeResultSchema(), description="") +@tenant_authentication +async def upgrade_anoncreds(request: web.BaseRequest): + """Request handler for triggering an upgrade to anoncreds. + + Args: + request: aiohttp request object + + Returns: + An empty JSON response + + """ + context: AdminRequestContext = request["context"] + profile = context.profile + + if profile.settings.get("wallet.name") != request.query.get("wallet_name"): + raise web.HTTPBadRequest( + reason="Wallet name parameter does not match the agent which triggered the upgrade" # noqa: E501 + ) + + if profile.settings.get("wallet.type") == "askar-anoncreds": + raise web.HTTPBadRequest(reason="Wallet type is already anoncreds") + + async with profile.session() as session: + storage = session.inject(BaseStorage) + upgrading_record = StorageRecord( + RECORD_TYPE_ACAPY_UPGRADING, + UPGRADING_RECORD_IN_PROGRESS, + ) + await storage.add_record(upgrading_record) + is_subwallet = context.metadata and "wallet_id" in context.metadata + asyncio.create_task( + upgrade_wallet_to_anoncreds_if_requested(profile, is_subwallet) + ) + UpgradeInProgressSingleton().set_wallet(profile.name) + + return web.json_response( + { + "success": True, + "message": f"Upgrade to anoncreds has been triggered for wallet {profile.name}", # noqa: E501 + } + ) + + def register_events(event_bus: EventBus): """Subscribe to any events we need to support.""" event_bus.subscribe(EVENT_LISTENER_PATTERN, on_register_nym_event) @@ -1333,6 +1422,7 @@ async def register(app: web.Application): "/wallet/get-did-endpoint", wallet_get_did_endpoint, allow_head=False ), web.patch("/wallet/did/local/rotate-keypair", wallet_rotate_did_keypair), + web.post("/anoncreds/wallet/upgrade", upgrade_anoncreds), ] ) @@ -1356,3 +1446,13 @@ def post_process_routes(app: web.Application): }, } ) + app._state["swagger_dict"]["tags"].append( + { + "name": "anoncreds - wallet upgrade", + "description": "Anoncreds wallet upgrade", + "externalDocs": { + "description": "Specification", + "url": "https://hyperledger.github.io/anoncreds-spec", + }, + } + ) diff --git a/aries_cloudagent/wallet/singletons.py b/aries_cloudagent/wallet/singletons.py new file mode 100644 index 0000000000..9a7a91d057 --- /dev/null +++ b/aries_cloudagent/wallet/singletons.py @@ -0,0 +1,43 @@ +"""Module that contains singleton classes for wallet operations.""" + + +class IsAnoncredsSingleton: + """Singleton class used as cache for anoncreds wallet-type queries.""" + + instance = None + wallets = set() + + def __new__(cls, *args, **kwargs): + """Create a new instance of the class.""" + if cls.instance is None: + cls.instance = super().__new__(cls) + return cls.instance + + def set_wallet(self, wallet: str): + """Set a wallet name.""" + self.wallets.add(wallet) + + def remove_wallet(self, wallet: str): + """Remove a wallet name.""" + self.wallets.discard(wallet) + + +class UpgradeInProgressSingleton: + """Singleton class used as cache for upgrade in progress.""" + + instance = None + wallets = set() + + def __new__(cls, *args, **kwargs): + """Create a new instance of the class.""" + if cls.instance is None: + cls.instance = super().__new__(cls) + return cls.instance + + def set_wallet(self, wallet: str): + """Set a wallet name.""" + self.wallets.add(wallet) + + def remove_wallet(self, wallet: str): + """Remove a wallet name.""" + self.wallets.discard(wallet) diff --git a/aries_cloudagent/wallet/tests/test_anoncreds_upgrade.py b/aries_cloudagent/wallet/tests/test_anoncreds_upgrade.py new file mode 100644 index 0000000000..00c52bc623 --- /dev/null +++ b/aries_cloudagent/wallet/tests/test_anoncreds_upgrade.py @@ -0,0 +1,406 @@ +import asyncio +from time import time +from unittest import IsolatedAsyncioTestCase + +from aries_cloudagent.tests import mock +from aries_cloudagent.wallet import singletons + +from ...anoncreds.issuer import CATEGORY_CRED_DEF_PRIVATE +from ...cache.base import BaseCache +from ...core.in_memory.profile import InMemoryProfile, InMemoryProfileSession +from ...indy.credx.issuer import CATEGORY_CRED_DEF_KEY_PROOF +from ...messaging.credential_definitions.util import CRED_DEF_SENT_RECORD_TYPE +from ...messaging.schemas.util import SCHEMA_SENT_RECORD_TYPE +from ...multitenant.base import BaseMultitenantManager +from ...multitenant.manager import MultitenantManager +from ...storage.base import BaseStorage +from ...storage.record import StorageRecord +from ...storage.type import ( + RECORD_TYPE_ACAPY_STORAGE_TYPE, + RECORD_TYPE_ACAPY_UPGRADING, + STORAGE_TYPE_VALUE_ANONCREDS, +) +from .. import anoncreds_upgrade + + +class TestAnoncredsUpgrade(IsolatedAsyncioTestCase): + def setUp(self) -> None: + self.profile = InMemoryProfile.test_profile( + settings={"wallet.type": "askar", "wallet.id": "test-wallet-id"} + ) + self.context = self.profile.context + self.context.injector.bind_instance( + BaseMultitenantManager, mock.MagicMock(MultitenantManager, autospec=True) + ) + self.context.injector.bind_instance( + BaseCache, mock.MagicMock(BaseCache, autospec=True) + ) + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_convert_records_to_anoncreds(self, mock_handle): + async with self.profile.session() as session: + storage = session.inject(BaseStorage) + mock_handle.fetch = mock.CoroutineMock(return_value=None) + + schema_id = "GHjSbphAcdsrZrLjSvsjMp:2:faber-simple:1.1" + schema_id_parts = schema_id.split(":") + schema_tags = { + "schema_id": schema_id, + "schema_issuer_did": schema_id_parts[0], + "schema_name": schema_id_parts[-2], + "schema_version": schema_id_parts[-1], + "epoch": str(int(time())), + } + await storage.add_record( + StorageRecord(SCHEMA_SENT_RECORD_TYPE, schema_id, schema_tags) + ) + + credential_definition_id = "GHjSbphAcdsrZrLjSvsjMp:3:CL:8:default" + cred_def_tags = { + "schema_id": schema_id, + "schema_issuer_did": schema_id_parts[0], + "schema_name": schema_id_parts[-2], + "schema_version": schema_id_parts[-1], + "issuer_did": "GHjSbphAcdsrZrLjSvsjMp", + "cred_def_id": credential_definition_id, + "epoch": str(int(time())), + } + await storage.add_record( + StorageRecord( + CRED_DEF_SENT_RECORD_TYPE, credential_definition_id, cred_def_tags + ) + ) + storage.get_record = mock.CoroutineMock( + side_effect=[ + StorageRecord( + CATEGORY_CRED_DEF_PRIVATE, + {"p_key": {"p": "123...782", "q": "234...456"}, "r_key": None}, + {}, + ), + StorageRecord( + CATEGORY_CRED_DEF_KEY_PROOF, + {"c": "103...961", "xz_cap": "563...205", "xr_cap": []}, + {}, + ), + ] + ) + anoncreds_upgrade.IndyLedgerRequestsExecutor = mock.MagicMock() + anoncreds_upgrade.IndyLedgerRequestsExecutor.return_value.get_ledger_for_identifier = mock.CoroutineMock( + return_value=( + None, + mock.MagicMock( + get_schema=mock.CoroutineMock( + return_value={ + "attrNames": [ + "name", + "age", + ], + }, + ), + get_credential_definition=mock.CoroutineMock( + return_value={ + "type": "CL", + "tag": "default", + "value": { + "primary": { + "n": "123", + }, + }, + }, + ), + ), + ) + ) + + with mock.patch.object( + anoncreds_upgrade, "upgrade_and_delete_schema_records" + ), mock.patch.object( + anoncreds_upgrade, "upgrade_and_delete_cred_def_records" + ): + await anoncreds_upgrade.convert_records_to_anoncreds(self.profile) + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_retry_converting_records(self, mock_handle): + mock_handle.fetch = mock.CoroutineMock(return_value=None) + with mock.patch.object( + anoncreds_upgrade, "convert_records_to_anoncreds", mock.CoroutineMock() + ) as mock_convert_records_to_anoncreds: + mock_convert_records_to_anoncreds.side_effect = [ + Exception("Error"), + Exception("Error"), + None, + ] + async with self.profile.session() as session: + storage = session.inject(BaseStorage) + upgrading_record = StorageRecord( + RECORD_TYPE_ACAPY_UPGRADING, + anoncreds_upgrade.UPGRADING_RECORD_IN_PROGRESS, + ) + await storage.add_record(upgrading_record) + await anoncreds_upgrade.retry_converting_records( + self.profile, upgrading_record, 0 + ) + + assert mock_convert_records_to_anoncreds.call_count == 3 + storage_type_record = await storage.find_record( + RECORD_TYPE_ACAPY_STORAGE_TYPE, tag_query={} + ) + upgrading_record = await storage.find_record( + RECORD_TYPE_ACAPY_UPGRADING, tag_query={} + ) + assert storage_type_record.value == STORAGE_TYPE_VALUE_ANONCREDS + assert ( + upgrading_record.value + == anoncreds_upgrade.UPGRADING_RECORD_FINISHED + ) + assert "test-profile" in singletons.IsAnoncredsSingleton().wallets + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_upgrade_wallet_to_anoncreds(self, mock_handle): + mock_handle.fetch = mock.CoroutineMock(return_value=None) + + # upgrading record not present + await anoncreds_upgrade.upgrade_wallet_to_anoncreds_if_requested(self.profile) + + # upgrading record present + async with self.profile.session() as session: + storage = session.inject(BaseStorage) + await storage.add_record( + StorageRecord( + RECORD_TYPE_ACAPY_UPGRADING, + anoncreds_upgrade.UPGRADING_RECORD_IN_PROGRESS, + ) + ) + await anoncreds_upgrade.upgrade_wallet_to_anoncreds_if_requested( + self.profile + ) + storage_type_record = await storage.find_record( + RECORD_TYPE_ACAPY_STORAGE_TYPE, tag_query={} + ) + upgrading_record = await storage.find_record( + RECORD_TYPE_ACAPY_UPGRADING, tag_query={} + ) + assert storage_type_record.value == STORAGE_TYPE_VALUE_ANONCREDS + assert upgrading_record.value == anoncreds_upgrade.UPGRADING_RECORD_FINISHED + assert "test-profile" in singletons.IsAnoncredsSingleton().wallets + + # retry called on exception + with mock.patch.object( + anoncreds_upgrade, + "convert_records_to_anoncreds", + mock.CoroutineMock(side_effect=[Exception("Error")]), + ), mock.patch.object( + anoncreds_upgrade, "retry_converting_records", mock.CoroutineMock() + ) as mock_retry_converting_records: + async with self.profile.session() as session: + storage = session.inject(BaseStorage) + upgrading_record = await storage.find_record( + RECORD_TYPE_ACAPY_UPGRADING, tag_query={} + ) + await storage.update_record( + upgrading_record, anoncreds_upgrade.UPGRADING_RECORD_IN_PROGRESS, {} + ) + await anoncreds_upgrade.upgrade_wallet_to_anoncreds_if_requested( + self.profile + ) + assert mock_retry_converting_records.called + + async def test_set_storage_type_to_anoncreds_no_existing_record(self): + await anoncreds_upgrade.finish_upgrade(self.profile) + _, storage_type_record = next(iter(self.profile.records.items())) + assert storage_type_record.value == STORAGE_TYPE_VALUE_ANONCREDS + + async def test_set_storage_type_to_anoncreds_has_existing_record(self): + async with self.profile.session() as session: + storage = session.inject(BaseStorage) + await storage.add_record( + StorageRecord( + RECORD_TYPE_ACAPY_STORAGE_TYPE, + "askar", + ) + ) + await anoncreds_upgrade.finish_upgrade(self.profile) + _, storage_type_record = next(iter(self.profile.records.items())) + assert storage_type_record.value == STORAGE_TYPE_VALUE_ANONCREDS + + async def test_update_if_subwallet_and_set_storage_type_with_subwallet(self): + + await anoncreds_upgrade.finish_upgrade_by_updating_profile_or_shutting_down( + self.profile, True + ) + _, storage_type_record = next(iter(self.profile.records.items())) + assert storage_type_record.value == STORAGE_TYPE_VALUE_ANONCREDS + assert self.profile.context.injector.get_provider( + BaseCache + )._instance.flush.called + + async def test_update_if_subwallet_and_set_storage_type_with_base_wallet(self): + + await anoncreds_upgrade.finish_upgrade_by_updating_profile_or_shutting_down( + self.profile, False + ) + _, storage_type_record = next(iter(self.profile.records.items())) + assert storage_type_record.value == STORAGE_TYPE_VALUE_ANONCREDS + + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_failed_upgrade(self, mock_handle): + mock_handle.fetch = mock.CoroutineMock(return_value=None) + async with self.profile.session() as session: + storage = session.inject(BaseStorage) + + schema_id = "GHjSbphAcdsrZrLjSvsjMp:2:faber-simple:1.1" + schema_id_parts = schema_id.split(":") + schema_tags = { + "schema_id": schema_id, + "schema_issuer_did": schema_id_parts[0], + "schema_name": schema_id_parts[-2], + "schema_version": schema_id_parts[-1], + "epoch": str(int(time())), + } + await storage.add_record( + StorageRecord(SCHEMA_SENT_RECORD_TYPE, schema_id, schema_tags) + ) + await storage.add_record( + StorageRecord( + RECORD_TYPE_ACAPY_STORAGE_TYPE, + "askar", + ) + ) + await storage.add_record( + StorageRecord( + RECORD_TYPE_ACAPY_UPGRADING, + "true", + ) + ) + + credential_definition_id = "GHjSbphAcdsrZrLjSvsjMp:3:CL:8:default" + cred_def_tags = { + "schema_id": schema_id, + "schema_issuer_did": schema_id_parts[0], + "schema_name": schema_id_parts[-2], + "schema_version": schema_id_parts[-1], + "issuer_did": "GHjSbphAcdsrZrLjSvsjMp", + "cred_def_id": credential_definition_id, + "epoch": str(int(time())), + } + await storage.add_record( + StorageRecord( + CRED_DEF_SENT_RECORD_TYPE, credential_definition_id, cred_def_tags + ) + ) + storage.get_record = mock.CoroutineMock( + side_effect=[ + StorageRecord( + CATEGORY_CRED_DEF_PRIVATE, + {"p_key": {"p": "123...782", "q": "234...456"}, "r_key": None}, + {}, + ), + StorageRecord( + CATEGORY_CRED_DEF_KEY_PROOF, + {"c": "103...961", "xz_cap": "563...205", "xr_cap": []}, + {}, + ), + ] + ) + anoncreds_upgrade.IndyLedgerRequestsExecutor = mock.MagicMock() + anoncreds_upgrade.IndyLedgerRequestsExecutor.return_value.get_ledger_for_identifier = mock.CoroutineMock( + return_value=( + None, + mock.MagicMock( + get_schema=mock.CoroutineMock( + return_value={ + "attrNames": [ + "name", + "age", + ], + }, + ), + get_credential_definition=mock.CoroutineMock( + return_value={ + "type": "CL", + "tag": "default", + "value": { + "primary": { + "n": "123", + }, + }, + }, + ), + ), + ) + ) + + with mock.patch.object( + anoncreds_upgrade, "upgrade_and_delete_schema_records" + ), mock.patch.object( + anoncreds_upgrade, "upgrade_and_delete_cred_def_records" + ), mock.patch.object( + InMemoryProfileSession, "rollback" + ) as mock_rollback, mock.patch.object( + InMemoryProfileSession, + "commit", + # Don't wait for sleep in retry to speed up test + ) as mock_commit, mock.patch.object( + asyncio, "sleep" + ): + """ + Only tests schemas and cred_defs failing to upgrade because the other objects are + hard to mock. These tests should be enough to cover them as the logic is the same. + """ + + # Schemas fails to upgrade + anoncreds_upgrade.upgrade_and_delete_schema_records = mock.CoroutineMock( + # Needs to fail 5 times because of the retry logic + side_effect=[ + Exception("Error"), + Exception("Error"), + Exception("Error"), + Exception("Error"), + Exception("Error"), + ] + ) + await anoncreds_upgrade.upgrade_wallet_to_anoncreds_if_requested( + self.profile + ) + assert mock_rollback.called + assert not mock_commit.called + # Upgrading record should not be deleted + with self.assertRaises(Exception): + await storage.find_record( + type_filter=RECORD_TYPE_ACAPY_UPGRADING, tag_query={} + ) + + storage_type_record = await storage.find_record( + type_filter=RECORD_TYPE_ACAPY_STORAGE_TYPE, tag_query={} + ) + # Storage type should not be updated + assert storage_type_record.value == "askar" + + # Cred_defs fails to upgrade + anoncreds_upgrade.upgrade_and_delete_cred_def_records = ( + mock.CoroutineMock( + side_effect=[ + Exception("Error"), + Exception("Error"), + Exception("Error"), + Exception("Error"), + Exception("Error"), + ] + ) + ) + await anoncreds_upgrade.upgrade_wallet_to_anoncreds_if_requested( + self.profile + ) + assert mock_rollback.called + assert not mock_commit.called + # Upgrading record should not be deleted + with self.assertRaises(Exception): + await storage.find_record( + type_filter=RECORD_TYPE_ACAPY_UPGRADING, tag_query={} + ) + + storage_type_record = await storage.find_record( + type_filter=RECORD_TYPE_ACAPY_STORAGE_TYPE, tag_query={} + ) + # Storage type should not be updated + assert storage_type_record.value == "askar" diff --git a/aries_cloudagent/wallet/tests/test_indy_wallet.py b/aries_cloudagent/wallet/tests/test_indy_wallet.py deleted file mode 100644 index 1da9d7968d..0000000000 --- a/aries_cloudagent/wallet/tests/test_indy_wallet.py +++ /dev/null @@ -1,904 +0,0 @@ -import json -import os -from typing import cast - -import indy.anoncreds -import indy.crypto -import indy.did -import indy.wallet -import pytest -from aries_cloudagent.tests import mock - -from ...config.injection_context import InjectionContext -from ...core.error import ProfileDuplicateError, ProfileError, ProfileNotFoundError -from ...core.in_memory import InMemoryProfile -from ...indy.sdk import wallet_setup as test_setup_module -from ...indy.sdk.profile import IndySdkProfile, IndySdkProfileManager -from ...indy.sdk.wallet_setup import IndyWalletConfig -from ...ledger.endpoint_type import EndpointType -from ...ledger.indy import IndySdkLedgerPool -from ...wallet.did_method import SOV, DIDMethods -from ...wallet.key_type import ED25519 -from .. import indy as test_module -from ..base import BaseWallet -from ..in_memory import InMemoryWallet -from ..indy import IndySdkWallet -from . import test_in_memory_wallet - - -@pytest.fixture() -async def in_memory_wallet(): - profile = InMemoryProfile.test_profile(bind={DIDMethods: DIDMethods()}) - wallet = InMemoryWallet(profile) - yield wallet - - -@pytest.fixture() -async def wallet(): - key = await IndySdkWallet.generate_wallet_key() - context = InjectionContext() - context.injector.bind_instance(IndySdkLedgerPool, IndySdkLedgerPool("name")) - context.injector.bind_instance(DIDMethods, DIDMethods()) - with mock.patch.object(IndySdkProfile, "_make_finalizer"): - profile = cast( - IndySdkProfile, - await IndySdkProfileManager().provision( - context, - { - "auto_recreate": True, - "auto_remove": True, - "name": "test-wallet", - "key": key, - "key_derivation_method": "RAW", # much slower tests with argon-hashed keys - }, - ), - ) - async with profile.session() as session: - yield session.inject(BaseWallet) - await profile.close() - - -@pytest.mark.indy -class TestIndySdkWallet(test_in_memory_wallet.TestInMemoryWallet): - """Apply all InMemoryWallet tests against IndySdkWallet""" - - @pytest.mark.asyncio - async def test_rotate_did_keypair_x(self, wallet: IndySdkWallet): - info = await wallet.create_local_did( - SOV, ED25519, self.test_seed, self.test_sov_did - ) - - with mock.patch.object( - indy.did, "replace_keys_start", mock.CoroutineMock() - ) as mock_repl_start: - mock_repl_start.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.rotate_did_keypair_start(self.test_sov_did) - assert "outlier" in str(excinfo.value) - - with mock.patch.object( - indy.did, "replace_keys_apply", mock.CoroutineMock() - ) as mock_repl_apply: - mock_repl_apply.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.rotate_did_keypair_apply(self.test_sov_did) - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_create_signing_key_x(self, wallet: IndySdkWallet): - with mock.patch.object( - indy.crypto, "create_key", mock.CoroutineMock() - ) as mock_create_key: - mock_create_key.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.create_signing_key(ED25519) - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_create_local_did_x(self, wallet: IndySdkWallet): - with mock.patch.object( - indy.did, "create_and_store_my_did", mock.CoroutineMock() - ) as mock_create: - mock_create.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.create_local_did(SOV, ED25519) - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_set_did_endpoint_ledger(self, wallet: IndySdkWallet): - mock_ledger = mock.MagicMock( - read_only=False, update_endpoint_for_did=mock.CoroutineMock() - ) - info_pub = await wallet.create_public_did( - SOV, - ED25519, - ) - await wallet.set_did_endpoint(info_pub.did, "https://example.com", mock_ledger) - mock_ledger.update_endpoint_for_did.assert_called_once_with( - info_pub.did, - "https://example.com", - EndpointType.ENDPOINT, - endorser_did=None, - write_ledger=True, - routing_keys=None, - ) - info_pub2 = await wallet.get_public_did() - assert info_pub2.metadata["endpoint"] == "https://example.com" - - with pytest.raises(test_module.LedgerConfigError) as excinfo: - await wallet.set_did_endpoint(info_pub.did, "https://example.com", None) - assert "No ledger available" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_set_did_endpoint_ledger_with_routing_keys( - self, wallet: IndySdkWallet - ): - routing_keys = ["3YJCx3TqotDWFGv7JMR5erEvrmgu5y4FDqjR7sKWxgXn"] - mock_ledger = mock.MagicMock( - read_only=False, update_endpoint_for_did=mock.CoroutineMock() - ) - info_pub = await wallet.create_public_did(SOV, ED25519) - await wallet.set_did_endpoint( - info_pub.did, "https://example.com", mock_ledger, routing_keys=routing_keys - ) - - mock_ledger.update_endpoint_for_did.assert_called_once_with( - info_pub.did, - "https://example.com", - EndpointType.ENDPOINT, - endorser_did=None, - write_ledger=True, - routing_keys=routing_keys, - ) - - @pytest.mark.asyncio - async def test_set_did_endpoint_readonly_ledger(self, wallet: IndySdkWallet): - mock_ledger = mock.MagicMock( - read_only=True, update_endpoint_for_did=mock.CoroutineMock() - ) - info_pub = await wallet.create_public_did( - SOV, - ED25519, - ) - await wallet.set_did_endpoint(info_pub.did, "https://example.com", mock_ledger) - mock_ledger.update_endpoint_for_did.assert_not_called() - info_pub2 = await wallet.get_public_did() - assert info_pub2.metadata["endpoint"] == "https://example.com" - - with pytest.raises(test_module.LedgerConfigError) as excinfo: - await wallet.set_did_endpoint(info_pub.did, "https://example.com", None) - assert "No ledger available" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_get_signing_key_x(self, wallet: IndySdkWallet): - with mock.patch.object( - indy.crypto, "get_key_metadata", mock.CoroutineMock() - ) as mock_signing: - mock_signing.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.get_signing_key(None) - assert "Missing required input parameter: verkey" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_get_local_did_x(self, wallet: IndySdkWallet): - with mock.patch.object( - indy.did, "get_my_did_with_meta", mock.CoroutineMock() - ) as mock_my: - mock_my.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.get_local_did("did:sov") - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_replace_local_did_metadata_x(self, wallet: IndySdkWallet): - info = await wallet.create_local_did( - SOV, - ED25519, - self.test_seed, - self.test_sov_did, - self.test_metadata, - ) - assert info.did == self.test_sov_did - assert info.verkey == self.test_ed25519_verkey - assert info.metadata == self.test_metadata - - with mock.patch.object( - indy.did, "set_did_metadata", mock.CoroutineMock() - ) as mock_set_did_metadata: - mock_set_did_metadata.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.replace_local_did_metadata(info.did, info.metadata) - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_verify_message_x(self, wallet: IndySdkWallet): - with mock.patch.object( - indy.crypto, "crypto_verify", mock.CoroutineMock() - ) as mock_verify: - mock_verify.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.verify_message( - b"hello world", - b"signature", - self.test_ed25519_verkey, - ED25519, - ) - assert "outlier" in str(excinfo.value) - - mock_verify.side_effect = test_module.IndyError( # plain wrong - test_module.ErrorCode.CommonInvalidStructure - ) - assert not await wallet.verify_message( - b"hello world", b"signature", self.test_ed25519_verkey, ED25519 - ) - - @pytest.mark.asyncio - async def test_pack_message_x(self, wallet: IndySdkWallet): - with mock.patch.object( - indy.crypto, "pack_message", mock.CoroutineMock() - ) as mock_pack: - mock_pack.side_effect = test_module.IndyError( # outlier - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - with pytest.raises(test_module.WalletError) as excinfo: - await wallet.pack_message( - b"hello world", - [ - self.test_ed25519_verkey, - ], - ) - assert "outlier" in str(excinfo.value) - - -@pytest.mark.indy -class TestWalletCompat: - """Tests for wallet compatibility.""" - - test_seed = "testseed000000000000000000000001" - test_did = "55GkHamhTU1ZbTbV2ab9DE" - test_verkey = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx" - test_message = "test message" - - @pytest.mark.asyncio - async def test_compare_pack_unpack(self, in_memory_wallet, wallet: IndySdkWallet): - """ - Ensure that python-based pack/unpack is compatible with indy-sdk implementation - """ - await in_memory_wallet.create_local_did(SOV, ED25519, self.test_seed) - py_packed = await in_memory_wallet.pack_message( - self.test_message, [self.test_verkey], self.test_verkey - ) - - await wallet.create_local_did(SOV, ED25519, self.test_seed) - packed = await wallet.pack_message( - self.test_message, [self.test_verkey], self.test_verkey - ) - - py_unpacked, from_vk, to_vk = await in_memory_wallet.unpack_message(packed) - assert self.test_message == py_unpacked - - unpacked, from_vk, to_vk = await wallet.unpack_message(py_packed) - assert self.test_message == unpacked - - @pytest.mark.asyncio - async def test_mock_coverage(self): - """ - Coverage through mock framework. - """ - wallet_key = await IndySdkWallet.generate_wallet_key() - storage_config_json = json.dumps({"url": "dummy"}) - storage_creds_json = json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - }, - ) - with mock.patch.object( - test_setup_module, - "load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete: - fake_wallet = IndyWalletConfig( - { - "auto_recreate": True, - "auto_remove": False, - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": storage_config_json, - "storage_creds": storage_creds_json, - } - ) - mock_load.assert_called_once_with(storage_config_json, storage_creds_json) - assert fake_wallet.wallet_access - opened = await fake_wallet.create_wallet() - await opened.close() - await fake_wallet.remove_wallet() - - @pytest.mark.asyncio - async def test_mock_coverage_wallet_exists_x(self): - """ - Coverage through mock framework: raise on creation of existing wallet - """ - wallet_key = await IndySdkWallet.generate_wallet_key() - storage_config_json = json.dumps({"url": "dummy"}) - storage_creds_json = json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - }, - ) - with mock.patch.object( - test_setup_module, - "load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete: - mock_create.side_effect = test_module.IndyError( - test_module.ErrorCode.WalletAlreadyExistsError - ) - fake_wallet = IndyWalletConfig( - { - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": storage_config_json, - "storage_creds": storage_creds_json, - } - ) - with pytest.raises(ProfileDuplicateError) as excinfo: - await fake_wallet.create_wallet() - - @pytest.mark.asyncio - async def test_mock_coverage_wallet_create_x(self): - """ - Coverage through mock framework: raise on creation outlier - """ - wallet_key = await IndySdkWallet.generate_wallet_key() - storage_config_json = json.dumps({"url": "dummy"}) - storage_creds_json = json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - }, - ) - with mock.patch.object( - test_setup_module, - "load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete: - mock_create.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - fake_wallet = IndyWalletConfig( - { - "auto_recreate": True, - "auto_remove": True, - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": storage_config_json, - "storage_creds": storage_creds_json, - } - ) - with pytest.raises(ProfileError) as excinfo: - await fake_wallet.create_wallet() - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_mock_coverage_remove_x(self): - """ - Coverage through mock framework: exception on removal. - """ - wallet_key = await IndySdkWallet.generate_wallet_key() - storage_config_json = json.dumps({"url": "dummy"}) - storage_creds_json = json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - }, - ) - with mock.patch.object( - test_setup_module, - "load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete: - mock_delete.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - fake_wallet = IndyWalletConfig( - { - "auto_recreate": False, - "auto_remove": False, - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": storage_config_json, - "storage_creds": storage_creds_json, - } - ) - mock_load.assert_called_once_with(storage_config_json, storage_creds_json) - assert fake_wallet.wallet_access - opened = await fake_wallet.create_wallet() - await opened.close() - with pytest.raises(ProfileError) as excinfo: - await fake_wallet.remove_wallet() - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_mock_coverage_not_found_after_creation(self): - """ - Coverage through mock framework: missing created wallet. - """ - wallet_key = await IndySdkWallet.generate_wallet_key() - storage_config_json = json.dumps({"url": "dummy"}) - storage_creds_json = json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - }, - ) - with mock.patch.object( - test_setup_module, - "load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete: - mock_open.side_effect = test_module.IndyError( - test_module.ErrorCode.WalletNotFoundError, {"message": "outlier"} - ) - fake_wallet = IndyWalletConfig( - { - "auto_recreate": True, - "auto_remove": True, - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": storage_config_json, - "storage_creds": storage_creds_json, - } - ) - mock_load.assert_called_once_with(storage_config_json, storage_creds_json) - with pytest.raises(ProfileError) as excinfo: - await fake_wallet.create_wallet() - assert "not found" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_mock_coverage_open_not_found(self): - """ - Coverage through mock framework: missing wallet on open. - """ - wallet_key = await IndySdkWallet.generate_wallet_key() - storage_config_json = json.dumps({"url": "dummy"}) - storage_creds_json = json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - }, - ) - with mock.patch.object( - test_setup_module, - "load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete: - mock_open.side_effect = test_module.IndyError( - test_module.ErrorCode.WalletNotFoundError, {"message": "outlier"} - ) - fake_wallet = IndyWalletConfig( - { - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": storage_config_json, - "storage_creds": storage_creds_json, - } - ) - mock_load.assert_called_once_with(storage_config_json, storage_creds_json) - with pytest.raises(ProfileNotFoundError) as excinfo: - await fake_wallet.open_wallet() - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_mock_coverage_open_indy_already_open_x(self): - """ - Coverage through mock framework: indy thinks wallet is open, aca-py does not. - """ - wallet_key = await IndySdkWallet.generate_wallet_key() - storage_config_json = json.dumps({"url": "dummy"}) - storage_creds_json = json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - }, - ) - with mock.patch.object( - test_setup_module, - "load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete: - mock_open.side_effect = test_module.IndyError( - test_module.ErrorCode.WalletAlreadyOpenedError, {"message": "outlier"} - ) - fake_wallet = IndyWalletConfig( - { - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": storage_config_json, - "storage_creds": storage_creds_json, - } - ) - mock_load.assert_called_once_with(storage_config_json, storage_creds_json) - with pytest.raises(ProfileError) as excinfo: - await fake_wallet.open_wallet() - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_mock_coverage_open_x(self): - """ - Coverage through mock framework: outlier on wallet open. - """ - wallet_key = await IndySdkWallet.generate_wallet_key() - storage_config_json = json.dumps({"url": "dummy"}) - storage_creds_json = json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - }, - ) - with mock.patch.object( - test_setup_module, - "load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete: - mock_open.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - fake_wallet = IndyWalletConfig( - { - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": storage_config_json, - "storage_creds": storage_creds_json, - } - ) - mock_load.assert_called_once_with(storage_config_json, storage_creds_json) - with pytest.raises(ProfileError) as excinfo: - await fake_wallet.open_wallet() - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_mock_coverage_open_master_secret_x(self): - """ - Coverage through mock framework: outlier on master secret creation - """ - wallet_key = await IndySdkWallet.generate_wallet_key() - storage_config_json = json.dumps({"url": "dummy"}) - storage_creds_json = json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - }, - ) - with mock.patch.object( - test_setup_module, - "load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete: - mock_master.side_effect = test_module.IndyError( - test_module.ErrorCode.CommonIOError, {"message": "outlier"} - ) - fake_wallet = IndyWalletConfig( - { - "auto_recreate": True, - "auto_remove": True, - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": storage_config_json, - "storage_creds": storage_creds_json, - } - ) - mock_load.assert_called_once_with(storage_config_json, storage_creds_json) - with pytest.raises(ProfileError) as excinfo: - await fake_wallet.create_wallet() - assert "outlier" in str(excinfo.value) - - @pytest.mark.asyncio - async def test_mock_coverage_open_master_secret_exists(self): - """ - Coverage through mock framework: open, master secret exists (OK). - """ - wallet_key = await IndySdkWallet.generate_wallet_key() - storage_config_json = json.dumps({"url": "dummy"}) - storage_creds_json = json.dumps( - { - "account": "postgres", - "password": "mysecretpassword", - "admin_account": "postgres", - "admin_password": "mysecretpassword", - }, - ) - with mock.patch.object( - test_setup_module, - "load_postgres_plugin", - mock.MagicMock(), - ) as mock_load, mock.patch.object( - indy.wallet, "create_wallet", mock.CoroutineMock() - ) as mock_create, mock.patch.object( - indy.wallet, "open_wallet", mock.CoroutineMock() - ) as mock_open, mock.patch.object( - indy.anoncreds, "prover_create_master_secret", mock.CoroutineMock() - ) as mock_master, mock.patch.object( - indy.wallet, "close_wallet", mock.CoroutineMock() - ) as mock_close, mock.patch.object( - indy.wallet, "delete_wallet", mock.CoroutineMock() - ) as mock_delete: - mock_master.side_effect = test_module.IndyError( - test_module.ErrorCode.AnoncredsMasterSecretDuplicateNameError - ) - fake_wallet = IndyWalletConfig( - { - "auto_recreate": True, - "auto_remove": True, - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": storage_config_json, - "storage_creds": storage_creds_json, - } - ) - mock_load.assert_called_once_with(storage_config_json, storage_creds_json) - assert fake_wallet.wallet_access - opened = await fake_wallet.create_wallet() - assert opened.master_secret_id == fake_wallet.name - await opened.close() - await fake_wallet.remove_wallet() - - # TODO get these to run in docker ci/cd - @pytest.mark.asyncio - @pytest.mark.postgres - async def test_postgres_wallet_works(self): - """ - Ensure that postgres wallet operations work (create and open wallet, create did, drop wallet) - """ - postgres_url = os.environ.get("POSTGRES_URL") - if not postgres_url: - pytest.fail("POSTGRES_URL not configured") - - wallet_key = await IndySdkWallet.generate_wallet_key() - postgres_wallet = IndyWalletConfig( - { - "auto_recreate": True, - "auto_remove": True, - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": '{"url":"' + postgres_url + '"}', - "storage_creds": '{"account":"postgres","password":"mysecretpassword","admin_account":"postgres","admin_password":"mysecretpassword"}', - } - ) - assert postgres_wallet.wallet_access - opened = await postgres_wallet.create_wallet() - wallet = IndySdkWallet(opened) - - await wallet.create_local_did(SOV, ED25519, self.test_seed) - py_packed = await wallet.pack_message( - self.test_message, [self.test_verkey], self.test_verkey - ) - - await wallet.close() - await postgres_wallet.remove_wallet() - - # TODO get these to run in docker ci/cd - @pytest.mark.asyncio - @pytest.mark.postgres - async def test_postgres_wallet_scheme_works(self): - """ - Ensure that postgres wallet operations work (create and open wallet, create did, drop wallet) - """ - postgres_url = os.environ.get("POSTGRES_URL") - if not postgres_url: - pytest.fail("POSTGRES_URL not configured") - - wallet_key = await IndySdkWallet.generate_wallet_key() - postgres_wallet = IndyWalletConfig( - { - "auto_recreate": True, - "auto_remove": True, - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": '{"url":"' - + postgres_url - + '", "wallet_scheme":"MultiWalletSingleTable"}', - "storage_creds": '{"account":"postgres","password":"mysecretpassword","admin_account":"postgres","admin_password":"mysecretpassword"}', - } - ) - assert postgres_wallet.wallet_access - opened = await postgres_wallet.create_wallet() - - with pytest.raises(ProfileError) as excinfo: - await postgres_wallet.create_wallet() - assert "Wallet was not removed" in str(excinfo.value) - - wallet = IndySdkWallet(opened) - await wallet.create_local_did(SOV, ED25519, self.test_seed) - py_packed = await wallet.pack_message( - self.test_message, [self.test_verkey], self.test_verkey - ) - - await wallet.close() - await postgres_wallet.remove_wallet() - - # TODO get these to run in docker ci/cd - @pytest.mark.asyncio - @pytest.mark.postgres - async def test_postgres_wallet_scheme2_works(self): - """ - Ensure that postgres wallet operations work (create and open wallet, create did, drop wallet) - """ - postgres_url = os.environ.get("POSTGRES_URL") - if not postgres_url: - pytest.fail("POSTGRES_URL not configured") - - wallet_key = await IndySdkWallet.generate_wallet_key() - postgres_wallet = IndyWalletConfig( - { - "auto_recreate": True, - "auto_remove": True, - "name": "test_pg_wallet", - "key": wallet_key, - "key_derivation_method": "RAW", - "storage_type": "postgres_storage", - "storage_config": '{"url":"' - + postgres_url - + '", "wallet_scheme":"MultiWalletSingleTableSharedPool"}', - "storage_creds": '{"account":"postgres","password":"mysecretpassword","admin_account":"postgres","admin_password":"mysecretpassword"}', - } - ) - opened = await postgres_wallet.create_wallet() - wallet = IndySdkWallet(opened) - - await wallet.create_local_did(SOV, ED25519, self.test_seed) - py_packed = await wallet.pack_message( - self.test_message, [self.test_verkey], self.test_verkey - ) - - await wallet.close() - await postgres_wallet.remove_wallet() diff --git a/aries_cloudagent/wallet/tests/test_routes.py b/aries_cloudagent/wallet/tests/test_routes.py index f2b756de23..ba8fc4db4d 100644 --- a/aries_cloudagent/wallet/tests/test_routes.py +++ b/aries_cloudagent/wallet/tests/test_routes.py @@ -3,6 +3,7 @@ from aiohttp.web import HTTPForbidden from aries_cloudagent.tests import mock +from aries_cloudagent.wallet import singletons from ...admin.request_context import AdminRequestContext from ...core.in_memory import InMemoryProfile @@ -11,6 +12,7 @@ from ...wallet.did_method import SOV, DIDMethod, DIDMethods, HolderDefinedDid from ...wallet.key_type import ED25519, KeyTypes from .. import routes as test_module +from ..anoncreds_upgrade import UPGRADING_RECORD_IN_PROGRESS from ..base import BaseWallet from ..did_info import DIDInfo from ..did_posture import DIDPosture @@ -27,7 +29,9 @@ class TestWalletRoutes(IsolatedAsyncioTestCase): def setUp(self): self.wallet = mock.create_autospec(BaseWallet) self.session_inject = {BaseWallet: self.wallet} - self.profile = InMemoryProfile.test_profile() + self.profile = InMemoryProfile.test_profile( + settings={"admin.admin_api_key": "secret-key"} + ) self.context = AdminRequestContext.test_context( self.session_inject, self.profile ) @@ -41,6 +45,7 @@ def setUp(self): match_info={}, query={}, __getitem__=lambda _, k: self.request_dict[k], + headers={"x-api-key": "secret-key"}, ) self.test_did = "did" @@ -1006,6 +1011,26 @@ async def test_rotate_did_keypair_x(self): with self.assertRaises(test_module.web.HTTPBadRequest): await test_module.wallet_rotate_did_keypair(self.request) + async def test_upgrade_anoncreds(self): + self.profile.settings["wallet.name"] = "test_wallet" + self.request.query = {"wallet_name": "not_test_wallet"} + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.upgrade_anoncreds(self.request) + + self.request.query = {"wallet_name": "not_test_wallet"} + self.profile.settings["wallet.type"] = "askar-anoncreds" + with self.assertRaises(test_module.web.HTTPBadRequest): + await test_module.upgrade_anoncreds(self.request) + + self.request.query = {"wallet_name": "test_wallet"} + self.profile.settings["wallet.type"] = "askar" + result = await test_module.upgrade_anoncreds(self.request) + print(result) + _, upgrade_record = next(iter(self.profile.records.items())) + assert upgrade_record.type == "acapy_upgrading" + assert upgrade_record.value == UPGRADING_RECORD_IN_PROGRESS + assert "test-profile" in singletons.UpgradeInProgressSingleton().wallets + async def test_register(self): mock_app = mock.MagicMock() mock_app.add_routes = mock.MagicMock() diff --git a/conftest.py b/conftest.py index cedd929864..5ab1eeb148 100644 --- a/conftest.py +++ b/conftest.py @@ -24,41 +24,6 @@ def stop(self): self.inner and self.inner.stop() -def stub_indy() -> Stub: - # detect indy module - try: - from indy.libindy import _cdll - - _cdll() - - return Stub(None) - except ImportError: - print("Skipping Indy-specific tests: python3-indy module not installed.") - except OSError: - print( - "Skipping Indy-specific tests: libindy shared library could not be loaded." - ) - - modules = {} - package_name = "indy" - modules[package_name] = mock.MagicMock() - for mod in [ - "anoncreds", - "blob_storage", - "crypto", - "did", - "error", - "pool", - "ledger", - "non_secrets", - "pairwise", - "wallet", - ]: - submod = f"{package_name}.{mod}" - modules[submod] = mock.MagicMock() - return Stub(mock.patch.dict(sys.modules, modules)) - - def stub_anoncreds() -> Stub: # detect anoncreds library try: @@ -200,7 +165,6 @@ def pytest_sessionstart(session): { "anoncreds": stub_anoncreds(), "askar": stub_askar(), - "indy": stub_indy(), "indy_credx": stub_indy_credx(), "indy_vdr": stub_indy_vdr(), "ursa_bbs_signatures": stub_ursa_bbs_signatures(), diff --git a/demo/features/0453-issue-credential.feature b/demo/features/0453-issue-credential.feature index 0085a393e9..27f51cf062 100644 --- a/demo/features/0453-issue-credential.feature +++ b/demo/features/0453-issue-credential.feature @@ -28,6 +28,7 @@ Feature: RFC 0453 Aries agent issue credential Examples: | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Acme_extra | Bob_extra | | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | | | + | --public-did --wallet-type askar-anoncreds --cred-type vc_di | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | | | @GHA @WalletType_Askar_AnonCreds @AltTests Examples: @@ -47,6 +48,26 @@ Feature: RFC 0453 Aries agent issue credential | --did-exchange --wallet-type askar-anoncreds --emit-did-peer-4 | --did-exchange --wallet-type askar-anoncreds --emit-did-peer-4 | driverslicense | Data_DL_NormalizedValues | | | | --did-exchange --wallet-type askar-anoncreds --reuse-connections --emit-did-peer-4| --did-exchange --wallet-type askar-anoncreds --reuse-connections --emit-did-peer-4| driverslicense | Data_DL_NormalizedValues | | | + @T003-RFC0453 + Scenario Outline: Issue a credential with the Issuer beginning with an offer + Given we have "2" agents + | name | role | capabilities | extra | + | Acme | issuer | | | + | Bob | holder | | | + And "Acme" and "Bob" have an existing connection + And "Acme" is ready to issue a credential for + When "Acme" offers a credential with data + When "Bob" has the credential issued + When "Acme" sets the credential type to + When "Acme" offers a credential with data + Then "Bob" has the credential issued + + @WalletType_Askar_AnonCreds @SwitchCredTypeTest + Examples: + | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Acme_extra | Bob_extra | New_Cred_Type | + | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | | | vc_di | + | --public-did --wallet-type askar-anoncreds --cred-type vc_di | --wallet-type askar-anoncreds | driverslicense | Data_DL_NormalizedValues | | | indy | + @T003-RFC0453 Scenario Outline: Holder accepts a deleted credential offer Given we have "2" agents diff --git a/demo/features/0454-present-proof.feature b/demo/features/0454-present-proof.feature index 8787541703..ebc2a87792 100644 --- a/demo/features/0454-present-proof.feature +++ b/demo/features/0454-present-proof.feature @@ -262,3 +262,28 @@ Feature: RFC 0454 Aries agent present proof Examples: | issuer1 | Acme1_capabilities | issuer2 | Acme2_capabilities | Bob_cap | Schema_name_1 | Credential_data_1 | Schema_name_2 | Credential_data_2 | Proof_request | | Acme1 | --revocation --public-did --wallet-type askar-anoncreds | Acme2 | --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | health_id | Data_DL_MaxValues | DL_age_over_19_v2_with_health_id_no_revoc | + + @T003-RFC0454.4 + Scenario Outline: Present Proof for a credential where multiple credentials are issued and all but one are revoked + Given we have "3" agents + | name | role | capabilities | + | Acme1 | issuer1 | | + | Faber | verifier | | + | Bob | prover | | + And "" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "" revokes the credential + And "Bob" has another issued credential from "" + And "Faber" and "Bob" have an existing connection + When "Faber" sends a request with explicit revocation status for proof presentation to "Bob" + Then "Faber" has the proof verified + + @WalletType_Askar + Examples: + | issuer1 | Acme1_capabilities | Bob_cap | Schema_name_1 | Credential_data_1 | Proof_request | + | Acme1 | --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @WalletType_Askar_AnonCreds + Examples: + | issuer1 | Acme1_capabilities | Bob_cap | Schema_name_1 | Credential_data_1 | Proof_request | + | Acme1 | --revocation --public-did --wallet-type askar-anoncreds | --wallet-type askar-anoncreds | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index 977a72e209..6dc33f428d 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -51,6 +51,15 @@ def step_impl(context, issuer, schema_name): context.cred_def_id = cred_def_id +@when('"{issuer}" sets the credential type to {credential_type}') +def step_impl(context, issuer, credential_type): + agent = context.active_agents[issuer] + + agent["agent"].set_cred_type(credential_type) + + assert agent["agent"].cred_type == credential_type + + @given('"{issuer}" offers a credential with data {credential_data}') @when('"{issuer}" offers a credential with data {credential_data}') def step_impl(context, issuer, credential_data): @@ -686,3 +695,22 @@ def step_impl(context, holder, schema_name, credential_data, issuer): + """" has the credential issued """ ) + + +@given( + '"{holder}" has another issued {schema_name} credential {credential_data} from "{issuer}"' +) +def step_impl(context, holder, schema_name, credential_data, issuer): + context.execute_steps( + # TODO possibly check that the requested schema is "active" (if there are multiple schemas) + ''' + When "''' + + issuer + + """" offers a credential with data """ + + credential_data + + ''' + Then "''' + + holder + + """" has the credential issued + """ + ) diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index da112e14d5..406db972a0 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -761,7 +761,6 @@ def step_impl(context, holder_name, issuer_name): "/credentials", params={}, ) - assert len(cred_list["results"]) == 1 cred_id = cred_list["results"][0]["referent"] revoc_status_bool = False diff --git a/demo/features/steps/upgrade.py b/demo/features/steps/upgrade.py new file mode 100644 index 0000000000..fe23f2570e --- /dev/null +++ b/demo/features/steps/upgrade.py @@ -0,0 +1,24 @@ +"""Steps for upgrading the wallet to support anoncreds.""" + +from bdd_support.agent_backchannel_client import ( + agent_container_POST, + async_sleep, +) +from behave import given, then + + +@given('"{issuer}" upgrades the wallet to anoncreds') +@then('"{issuer}" upgrades the wallet to anoncreds') +def step_impl(context, issuer): + """Upgrade the wallet to support anoncreds.""" + agent = context.active_agents[issuer] + agent_container_POST( + agent["agent"], + "/anoncreds/wallet/upgrade", + data={}, + params={ + "wallet_name": agent["agent"].agent.wallet_name, + }, + ) + + async_sleep(2.0) diff --git a/demo/features/upgrade.feature b/demo/features/upgrade.feature new file mode 100644 index 0000000000..d837efbab9 --- /dev/null +++ b/demo/features/upgrade.feature @@ -0,0 +1,29 @@ +Feature: ACA-Py Anoncreds Upgrade + + @GHA + Scenario Outline: Using revocation api, issue, revoke credentials and publish + Given we have "3" agents + | name | role | capabilities | + | Acme | issuer | | + | Faber | verifier | | + | Bob | prover | | + And "" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "" has written the credential definition for to the ledger + And "" has written the revocation registry definition to the ledger + And "" has written the revocation registry entry transaction to the ledger + And "" revokes the credential without publishing the entry + And "" authors a revocation registry entry publishing transaction + And "Faber" and "Bob" have an existing connection + When "Faber" sends a request for proof presentation to "Bob" + Then "Faber" has the proof verification fail + Then "Bob" can verify the credential from "" was revoked + And "" upgrades the wallet to anoncreds + And "Bob" has an issued credential from "" + And "Bob" upgrades the wallet to anoncreds + And "Bob" has an issued credential from "" + When "Faber" sends a request for proof presentation to "Bob" + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Acme | --revocation --public-did --multitenant | --multitenant | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | \ No newline at end of file diff --git a/demo/runners/agent_container.py b/demo/runners/agent_container.py index 6395de5e84..9a84f54aef 100644 --- a/demo/runners/agent_container.py +++ b/demo/runners/agent_container.py @@ -17,6 +17,7 @@ from runners.support.agent import ( # noqa:E402 CRED_FORMAT_INDY, CRED_FORMAT_JSON_LD, + CRED_FORMAT_VC_DI, DID_METHOD_KEY, KEY_TYPE_BLS, WALLET_TYPE_INDY, @@ -293,6 +294,10 @@ async def handle_issue_credential_v2_0(self, message): await self.admin_POST( f"/issue-credential-2.0/records/{cred_ex_id}/send-request", data ) + elif message["by_format"]["cred_offer"].get("vc_di"): + await self.admin_POST( + f"/issue-credential-2.0/records/{cred_ex_id}/send-request" + ) elif state == "done": pass @@ -322,6 +327,9 @@ async def handle_issue_credential_v2_0_indy(self, message): self.log(f"Revocation registry ID: {rev_reg_id}") self.log(f"Credential revocation ID: {cred_rev_id}") + async def handle_issue_credential_v2_0_vc_di(self, message): + self.log(f"Handle VC_DI Credential: message = {message}") + async def handle_issue_credential_v2_0_ld_proof(self, message): self.log(f"LD Credential: message = {message}") @@ -704,6 +712,9 @@ def check_input_descriptor_record_id( return result + def set_cred_type(self, new_cred_type: str): + self.cred_type = new_cred_type + class AgentContainer: def __init__( @@ -925,6 +936,10 @@ async def initialize( schema_name, schema_attrs ) + def set_cred_type(self, new_cred_type: str): + self.cred_type = new_cred_type + self.agent.set_cred_type(new_cred_type) + async def create_schema_and_cred_def( self, schema_name: str, @@ -933,9 +948,7 @@ async def create_schema_and_cred_def( ): if not self.public_did: raise Exception("Can't create a schema/cred def without a public DID :-(") - if self.cred_type in [ - CRED_FORMAT_INDY, - ]: + if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: # need to redister schema and cred def on the ledger self.cred_def_id = await self.agent.create_schema_and_cred_def( schema_name, @@ -981,9 +994,7 @@ async def issue_credential( ): log_status("#13 Issue credential offer to X") - if self.cred_type in [ - CRED_FORMAT_INDY, - ]: + if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: cred_preview = { "@type": CRED_PREVIEW_TYPE, "attributes": cred_attrs, @@ -1043,9 +1054,7 @@ async def receive_credential( async def request_proof(self, proof_request, explicit_revoc_required: bool = False): log_status("#20 Request proof of degree from alice") - if self.cred_type in [ - CRED_FORMAT_INDY, - ]: + if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: indy_proof_request = { "name": ( proof_request["name"] @@ -1128,9 +1137,7 @@ async def verify_proof(self, proof_request): # log_status(f">>> last proof received: {self.agent.last_proof_received}") - if self.cred_type in [ - CRED_FORMAT_INDY, - ]: + if self.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: # return verified status return self.agent.last_proof_received["verified"] @@ -1522,11 +1529,13 @@ async def create_agent_with_args(args, ident: str = None, extra_args: list = Non if "cred_type" in args and args.cred_type not in [ CRED_FORMAT_INDY, + CRED_FORMAT_VC_DI, ]: public_did = None aip = 20 elif "cred_type" in args and args.cred_type in [ CRED_FORMAT_INDY, + CRED_FORMAT_VC_DI, ]: public_did = True else: diff --git a/demo/runners/faber.py b/demo/runners/faber.py index 8a8d83e687..6de8018c80 100644 --- a/demo/runners/faber.py +++ b/demo/runners/faber.py @@ -19,6 +19,7 @@ from runners.support.agent import ( # noqa:E402 CRED_FORMAT_INDY, CRED_FORMAT_JSON_LD, + CRED_FORMAT_VC_DI, SIG_TYPE_BLS, ) from runners.support.utils import ( # noqa:E402 @@ -141,6 +142,32 @@ def generate_credential_offer(self, aip, cred_type, cred_def_id, exchange_tracin } return offer_request + elif cred_type == CRED_FORMAT_VC_DI: + self.cred_attrs[cred_def_id] = { + "name": "Alice Smith", + "date": "2018-05-28", + "degree": "Maths", + "birthdate_dateint": birth_date.strftime(birth_date_format), + "timestamp": str(int(time.time())), + } + + cred_preview = { + "@type": CRED_PREVIEW_TYPE, + "attributes": [ + {"name": n, "value": v} + for (n, v) in self.cred_attrs[cred_def_id].items() + ], + } + offer_request = { + "connection_id": self.connection_id, + "comment": f"Offer on cred def id {cred_def_id}", + "auto_remove": False, + "credential_preview": cred_preview, + "filter": {"vc_di": {"cred_def_id": cred_def_id}}, + "trace": exchange_tracing, + } + return offer_request + elif cred_type == CRED_FORMAT_JSON_LD: offer_request = { "connection_id": self.connection_id, @@ -313,6 +340,72 @@ def generate_proof_request_web_request( proof_request_web_request["connection_id"] = self.connection_id return proof_request_web_request + elif cred_type == CRED_FORMAT_VC_DI: + proof_request_web_request = { + "comment": "Test proof request for VC-DI format", + "presentation_request": { + "dif": { + "options": { + "challenge": "3fa85f64-5717-4562-b3fc-2c963f66afa7", + "domain": "4jt78h47fh47", + }, + "presentation_definition": { + "id": "32f54163-7166-48f1-93d8-ff217bdb0654", + "submission_requirements": [ + { + "name": "Degree Verification", + "rule": "pick", + "min": 1, + "from": "A", + } + ], + "input_descriptors": [ + { + "id": "degree_input_1", + "name": "Degree Certificate", + "group": ["A"], + "schema": [ + { + "uri": "https://www.w3.org/2018/credentials#VerifiableCredential" + }, + { + "uri": "https://w3id.org/citizenship#PermanentResidentCard" + }, + ], + "constraints": { + "limit_disclosure": "required", + "fields": [ + { + "path": [ + "$.credentialSubject.degree.name" + ], + "purpose": "We need to verify that you have the required degree.", + "filter": {"type": "string"}, + }, + { + "path": [ + "$.credentialSubject.birthDate" + ], + "purpose": "To ensure you meet the age requirement.", + "filter": { + "type": "string", + "pattern": birth_date.strftime( + birth_date_format + ), + }, + }, + ], + }, + } + ], + }, + }, + }, + } + if not connectionless: + proof_request_web_request["connection_id"] = self.connection_id + return proof_request_web_request + elif cred_type == CRED_FORMAT_JSON_LD: proof_request_web_request = { "comment": "test proof request for json-ld", @@ -435,7 +528,7 @@ async def main(args): "birthdate_dateint", "timestamp", ] - if faber_agent.cred_type == CRED_FORMAT_INDY: + if faber_agent.cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: faber_agent.public_did = True await faber_agent.initialize( the_agent=agent, @@ -447,7 +540,9 @@ async def main(args): else False ), ) - elif faber_agent.cred_type == CRED_FORMAT_JSON_LD: + elif faber_agent.cred_type in [ + CRED_FORMAT_JSON_LD, + ]: faber_agent.public_did = True await faber_agent.initialize(the_agent=agent) else: @@ -463,8 +558,13 @@ async def main(args): ) exchange_tracing = False - options = ( - " (1) Issue Credential\n" + options = " (1) Issue Credential\n" + if faber_agent.cred_type in [ + CRED_FORMAT_INDY, + CRED_FORMAT_VC_DI, + ]: + options += " (1a) Set Credential Type (%CRED_TYPE%)\n" + options += ( " (2) Send Proof Request\n" " (2a) Send *Connectionless* Proof Request (requires a Mobile client)\n" " (3) Send Message\n" @@ -481,15 +581,28 @@ async def main(args): options += " (D) Set Endorser's DID\n" if faber_agent.multitenant: options += " (W) Create and/or Enable Wallet\n" + options += " (U) Upgrade wallet to anoncreds \n" options += " (T) Toggle tracing on credential/proof exchange\n" options += " (X) Exit?\n[1/2/3/4/{}{}T/X] ".format( "5/6/7/8/" if faber_agent.revocation else "", "W/" if faber_agent.multitenant else "", ) - async for option in prompt_loop(options): + + upgraded_to_anoncreds = False + async for option in prompt_loop( + options.replace("%CRED_TYPE%", faber_agent.cred_type) + ): if option is not None: option = option.strip() + # Anoncreds has different endpoints for revocation + is_anoncreds = False + if ( + faber_agent.agent.__dict__["wallet_type"] == "askar-anoncreds" + or upgraded_to_anoncreds + ): + is_anoncreds = True + if option is None or option in "xX": break @@ -540,6 +653,21 @@ async def main(args): ) ) + elif option == "1a": + new_cred_type = await prompt( + "Enter credential type ({}, {}): ".format( + CRED_FORMAT_INDY, + CRED_FORMAT_VC_DI, + ) + ) + if new_cred_type in [ + CRED_FORMAT_INDY, + CRED_FORMAT_VC_DI, + ]: + faber_agent.set_cred_type(new_cred_type) + else: + log_msg("Not a valid credential type.") + elif option == "1": log_status("#13 Issue credential offer to X") @@ -568,6 +696,14 @@ async def main(args): exchange_tracing, ) + elif faber_agent.cred_type == CRED_FORMAT_VC_DI: + offer_request = faber_agent.agent.generate_credential_offer( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.cred_def_id, + exchange_tracing, + ) + else: raise Exception( f"Error invalid credential type: {faber_agent.cred_type}" @@ -617,6 +753,16 @@ async def main(args): ) ) + elif faber_agent.cred_type == CRED_FORMAT_VC_DI: + proof_request_web_request = ( + faber_agent.agent.generate_proof_request_web_request( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.revocation, + exchange_tracing, + ) + ) + else: raise Exception( "Error invalid credential type:" + faber_agent.cred_type @@ -684,6 +830,17 @@ async def main(args): connectionless=True, ) ) + + elif faber_agent.cred_type == CRED_FORMAT_VC_DI: + proof_request_web_request = ( + faber_agent.agent.generate_proof_request_web_request( + faber_agent.aip, + faber_agent.cred_type, + faber_agent.revocation, + exchange_tracing, + connectionless=True, + ) + ) else: raise Exception( "Error invalid credential type:" + faber_agent.cred_type @@ -739,11 +896,6 @@ async def main(args): await prompt("Publish now? [Y/N]: ", default="N") ).strip() in "yY" - # Anoncreds has different endpoints for revocation - is_anoncreds = False - if faber_agent.agent.__dict__["wallet_type"] == "askar-anoncreds": - is_anoncreds = True - try: endpoint = ( "/anoncreds/revocation/revoke" @@ -845,6 +997,14 @@ async def main(args): ) except ClientError: pass + elif option in "uU" and faber_agent.multitenant: + log_status("Upgrading wallet to anoncreds. Wait a couple seconds...") + await faber_agent.agent.admin_POST( + "/anoncreds/wallet/upgrade", + params={"wallet_name": faber_agent.agent.wallet_name}, + ) + upgraded_to_anoncreds = True + await asyncio.sleep(2.0) if faber_agent.show_timing: timing = await faber_agent.agent.fetch_timing() diff --git a/demo/runners/performance.py b/demo/runners/performance.py index 4a0794caa8..194284b910 100644 --- a/demo/runners/performance.py +++ b/demo/runners/performance.py @@ -179,6 +179,7 @@ def __init__( log_file: str = None, log_config: str = None, log_level: str = None, + cred_type: str = None, **kwargs, ): super().__init__( @@ -188,6 +189,7 @@ def __init__( log_file=log_file, log_config=log_config, log_level=log_level, + cred_type=cred_type, **kwargs, ) self.extra_args = [ @@ -330,6 +332,7 @@ async def main( log_file: str = None, log_config: str = None, log_level: str = None, + cred_type: str = None, ): if multi_ledger: genesis = None @@ -377,6 +380,7 @@ async def main( log_file=log_file, log_config=log_config, log_level=log_level, + cred_type=cred_type, ) await faber.listen_webhooks(start_port + 5) await faber.register_did() @@ -760,6 +764,13 @@ async def check_received_pings(agent, issue_count, pb): "('debug', 'info', 'warning', 'error', 'critical')" ), ) + parser.add_argument( + "--cred-type", + type=str, + metavar="", + default=None, + help=("Specifyng the credential type"), + ) args = parser.parse_args() if args.did_exchange and args.mediation: diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index e71c0fcaf0..153be98abb 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -71,6 +71,7 @@ CRED_FORMAT_INDY = "indy" CRED_FORMAT_JSON_LD = "json-ld" +CRED_FORMAT_VC_DI = "vc_di" DID_METHOD_SOV = "sov" DID_METHOD_KEY = "key" KEY_TYPE_ED255 = "ed25519" @@ -676,9 +677,7 @@ async def register_did( role: str = "TRUST_ANCHOR", cred_type: str = CRED_FORMAT_INDY, ): - if cred_type in [ - CRED_FORMAT_INDY, - ]: + if cred_type in [CRED_FORMAT_INDY, CRED_FORMAT_VC_DI]: # if registering a did for issuing indy credentials, publish the did on the ledger self.log(f"Registering {self.ident} ...") if not ledger_url: @@ -825,6 +824,22 @@ async def register_or_switch_wallet( else: await self.admin_POST("/wallet/did/public?did=" + self.did) await asyncio.sleep(3.0) + elif cred_type == CRED_FORMAT_VC_DI: + # assign public did + new_did = await self.admin_POST("/wallet/did/create") + self.did = new_did["result"]["did"] + await self.register_did( + did=new_did["result"]["did"], + verkey=new_did["result"]["verkey"], + cred_type=CRED_FORMAT_VC_DI, + ) + if self.endorser_role and self.endorser_role == "author": + if endorser_agent: + await self.admin_POST("/wallet/did/public?did=" + self.did) + await asyncio.sleep(3.0) + else: + await self.admin_POST("/wallet/did/public?did=" + self.did) + await asyncio.sleep(3.0) elif cred_type == CRED_FORMAT_JSON_LD: # create did of appropriate type data = {"method": DID_METHOD_KEY, "options": {"key_type": KEY_TYPE_BLS}} diff --git a/demo/runners/support/utils.py b/demo/runners/support/utils.py index 77e5d7792f..b69d24b42d 100644 --- a/demo/runners/support/utils.py +++ b/demo/runners/support/utils.py @@ -115,7 +115,7 @@ def output_reader(handle, callback, *args, **kwargs): break try: run_in_terminal(functools.partial(callback, line, *args)) - except AssertionError as e: + except AssertionError: # see comment in DemoAgent.handle_output # trace log and prompt_toolkit do not get along... pass @@ -245,19 +245,7 @@ def progress(*args, **kwargs): def check_requires(args): wtype = args.wallet_type or "askar" - if wtype == "indy": - try: - from indy.libindy import _cdll - - _cdll() - except ImportError: - print("python3-indy module not installed") - sys.exit(1) - except OSError: - print("libindy shared library could not be loaded") - sys.exit(1) - - elif wtype == "askar": + if wtype == "askar": try: from aries_askar.bindings import get_library diff --git a/docs/deploying/ContainerImagesAndGithubActions.md b/docs/deploying/ContainerImagesAndGithubActions.md index bdc28e19d8..3dbdf275fc 100644 --- a/docs/deploying/ContainerImagesAndGithubActions.md +++ b/docs/deploying/ContainerImagesAndGithubActions.md @@ -22,22 +22,18 @@ Multiple variants are available; see [Tags](#tags). ACA-Py is a foundation for building decentralized identity applications; to this end, there are multiple variants of ACA-Py built to suit the needs of a variety -of environments and workflows. There are currently two main variants: +of environments and workflows. The following variants exist: - "Standard" - The default configuration of ACA-Py, including: - Aries Askar for secure storage - Indy VDR for Indy ledger communication - Indy Shared Libraries for AnonCreds -- "Indy" - The legacy configuration of ACA-Py, including: - - Indy SDK Wallet for secure storage - - Indy SDK Ledger for Indy ledger communication - - Indy SDK for AnonCreds -These two image variants are largely distinguished by providers for Indy Network -and AnonCreds support. The Standard variant is recommended for new projects. -Migration from an Indy based image (whether the new Indy image variant or the -original BC Gov images) to the Standard image is outside of the scope of this -document. +In the past, two image variants were published. These two variants are largely +distinguished by providers for Indy Network and AnonCreds support. The Standard +variant is recommended for new projects. Migration from an Indy based image +(whether the new Indy image variant or the original BC Gov images) to the +Standard image is outside of the scope of this document. The ACA-Py images built by this project are tagged to indicate which of the above variants it is. Other tags may also be generated for use by developers. @@ -48,8 +44,6 @@ Tag | Variant | Example | Description ------------------------|----------|--------------------------|-------------------------------------------------------------------------------------------------| py3.9-X.Y.Z | Standard | py3.9-0.7.4 | Standard image variant built on Python 3.9 for ACA-Py version X.Y.Z | py3.10-X.Y.Z | Standard | py3.10-0.7.4 | Standard image variant built on Python 3.10 for ACA-Py version X.Y.Z | -py3.9-indy-A.B.C-X.Y.Z | Indy | py3.9-indy-1.16.0-0.7.4 | Standard image variant built on Python 3.9 for ACA-Py version X.Y.Z and Indy SDK Version A.B.C | -py3.10-indy-A.B.C-X.Y.Z | Indy | py3.10-indy-1.16.0-0.7.4 | Standard image variant built on Python 3.10 for ACA-Py version X.Y.Z and Indy SDK Version A.B.C | ### Image Comparison @@ -63,7 +57,7 @@ variants and between the BC Gov ACA-Py images. - Uses container's system python environment rather than `pyenv` - Askar and Indy Shared libraries are installed as dependencies of ACA-Py through pip from pre-compiled binaries included in the python wrappers - Built from repo contents -- Indy Image +- Indy Image (no longer produced but included here for clarity) - Based on slim variant of Debian - Built from multi-stage build step (`indy-base` in the Dockerfile) which includes Indy dependencies; this could be replaced with an explicit `indy-python` image from the Indy SDK repo - Includes `libindy` but does **NOT** include the Indy CLI @@ -86,21 +80,16 @@ variants and between the BC Gov ACA-Py images. - Tests (`.github/workflows/tests.yml`) - A reusable workflow that runs tests for the Standard ACA-Py variant for a given python version. -- Tests (Indy) (`.github/workflows/tests-indy.yml`) - A reusable workflow that - runs tests for the Indy ACA-Py variant for a given python and indy version. - PR Tests (`.github/workflows/pr-tests.yml`) - Run on pull requests; runs tests - for the Standard and Indy ACA-Py variants for a "default" python version. - Check this workflow for the current default python and Indy versions in use. + for the Standard ACA-Py variant for a "default" python version. + Check this workflow for the current default python version in use. - Nightly Tests (`.github/workflows/nightly-tests.yml`) - Run nightly; runs - tests for the Standard and Indy ACA-Py variants for all currently supported + tests for the Standard ACA-Py variant for all currently supported python versions. Check this workflow for the set of currently supported - versions and Indy version(s) in use. + versions in use. - Publish (`.github/workflows/publish.yml`) - Run on new release published or when manually triggered; builds and pushes the Standard ACA-Py variant to the Github Container Registry. -- Publish (Indy) (`.github/workflows/publish-indy.yml`) - Run on new release - published or when manually triggered; builds and pushes the Indy ACA-Py - variant to the Github Container Registry. - Integration Tests (`.github/workflows/integrationtests.yml`) - Run on pull requests (to the hyperledger fork only); runs BDD integration tests. - Black Format (`.github/workflows/blackformat.yml`) - Run on pull requests; diff --git a/docs/design/UpgradeViaApi.md b/docs/design/UpgradeViaApi.md new file mode 100644 index 0000000000..0ddb890449 --- /dev/null +++ b/docs/design/UpgradeViaApi.md @@ -0,0 +1,103 @@ +# Upgrade via API Design + +#### To isolate an upgrade process and trigger it via API the following pattern was designed to handle multitenant scenarios. It includes an is_upgrading record in the wallet(DB) and a middleware to prevent requests during the upgrade process. + +#### The diagam below descripes the sequence of events for the anoncreds upgrade process which it was designed for, but the architecture can be used for any upgrade process. + +```mermaid +sequenceDiagram + participant A1 as Agent 1 + participant M1 as Middleware + participant IAS1 as IsAnoncredsSingleton Set + participant UIPS1 as UpgradeInProgressSingleton Set + participant W as Wallet (DB) + participant UIPS2 as UpgradeInProgressSingleton Set + participant IAS2 as IsAnoncredsSingleton Set + participant M2 as Middleware + participant A2 as Agent 2 + + Note over A1,A2: Start upgrade for non-anoncreds wallet + A1->>M1: POST /anoncreds/wallet/upgrade + M1-->>IAS1: check if wallet is in set + IAS1-->>M1: wallet is not in set + M1-->>UIPS1: check if wallet is in set + UIPS1-->>M1: wallet is not in set + M1->>A1: OK + A1-->>W: Add is_upgrading = anoncreds_in_progress record + A1->>A1: Upgrade wallet + A1-->>UIPS1: Add wallet to set + + Note over A1,A2: Attempted Requests During Upgrade + + Note over A1: Attempted Request + A1->>M1: GET /any-endpoint + M1-->>IAS1: check if wallet is in set + IAS1-->>M1: wallet is not in set + M1-->>UIPS1: check if wallet is in set + UIPS1-->>M1: wallet is in set + M1->>A1: 503 Service Unavailable + + Note over A2: Attempted Request + A2->>M2: GET /any-endpoint + M2-->>IAS2: check if wallet is in set + IAS2->>M2: wallet is not in set + M2-->>UIPS2: check if wallet is in set + UIPS2-->>M2: wallet is not in set + A2-->>W: Query is_upgrading = anoncreds_in_progress record + W-->>A2: record = anoncreds_in_progress + A2->>A2: Loop until upgrade is finished in seperate process + A2-->>UIPS2: Add wallet to set + M2->>A2: 503 Service Unavailable + + Note over A1,A2: Agent Restart During Upgrade + A1-->>W: Get is_upgrading record for wallet or all subwallets + W-->>A1: + A1->>A1: Resume upgrade if in progress + A1-->>UIPS1: Add wallet to set + + Note over A2: Same as Agent 1 + + Note over A1,A2: Upgrade Completes + + Note over A1: Finish Upgrade + A1-->>W: set is_upgrading = anoncreds_finished + A1-->>UIPS1: Remove wallet from set + A1-->>IAS1: Add wallet to set + A1->>A1: update subwallet or restart + + Note over A2: Detect Upgrade Complete + A2-->>W: Check is_upgrading = anoncreds_finished + W-->>A2: record = anoncreds_in_progress + A2->>A2: Wait 1 second + A2-->>W: Check is_upgrading = anoncreds_finished + W-->>A2: record = anoncreds_finished + A2-->>UIPS2: Remove wallet from set + A2-->>IAS2: Add wallet to set + A2->>A2: update subwallet or restart + + Note over A1,A2: Restarted Agents After Upgrade + + A1-->W: Get is_upgrading record for wallet or all subwallets + W-->>A1: + A1->>IAS1: Add wallet to set if record = anoncreds_finished + + Note over A2: Same as Agent 1 + + Note over A1,A2: Attempted Requests After Upgrade + + Note over A1: Attempted Request + A1->>M1: GET /any-endpoint + M1-->>IAS1: check if wallet is in set + IAS1-->>M1: wallet is in set + M1-->>A1: OK + + Note over A2: Same as Agent 1 +``` + + +##### An example of the implementation can be found via the anoncreds upgrade components. + - `aries_cloudagent/wallet/routes.py` in the `upgrade_anoncreds` controller + - the upgrade code in `wallet/anoncreds_upgrade.py` + - the middleware in `admin/server.py` in the `upgrade_middleware` function + - the singleton sets in `wallet/singletons.py` + - the startup process in `core/conductor.py` in the `check_for_wallet_upgrades_in_progress` function \ No newline at end of file diff --git a/docs/features/DIDResolution.md b/docs/features/DIDResolution.md index bdd9911a51..23144a894a 100644 --- a/docs/features/DIDResolution.md +++ b/docs/features/DIDResolution.md @@ -176,7 +176,7 @@ plugin: The following is a fully functional Dockerfile encapsulating this setup: ```dockerfile= -FROM ghcr.io/hyperledger/aries-cloudagent-python:py3.9-0.12.1rc1 +FROM ghcr.io/hyperledger/aries-cloudagent-python:py3.9-0.12.1 RUN pip3 install git+https://github.com/dbluhm/acapy-resolver-github CMD ["aca-py", "start", "-it", "http", "0.0.0.0", "3000", "-ot", "http", "-e", "http://localhost:3000", "--admin", "0.0.0.0", "3001", "--admin-insecure-mode", "--no-ledger", "--plugin", "acapy_resolver_github"] diff --git a/docs/features/SupportedRFCs.md b/docs/features/SupportedRFCs.md index 956245db9f..2e6048858e 100644 --- a/docs/features/SupportedRFCs.md +++ b/docs/features/SupportedRFCs.md @@ -8,7 +8,7 @@ ACA-Py or the repository `main` branch. Reminders (and PRs!) to update this page welcome! If you have any questions, please contact us on the #aries channel on [Hyperledger Discord](https://discord.gg/hyperledger) or through an issue in this repo. -**Last Update**: 2024-04-26, Release 0.12.1rc1 +**Last Update**: 2024-05-01, Release 0.12.1 > The checklist version of this document was created as a joint effort > between [Northern Block](https://northernblock.io/), [Animo Solutions](https://animo.id/) and the Ontario government, on behalf of the Ontario government. diff --git a/docs/gettingStarted/AriesDeveloperDemos.md b/docs/gettingStarted/AriesDeveloperDemos.md index 69fde702c9..338498f69a 100644 --- a/docs/gettingStarted/AriesDeveloperDemos.md +++ b/docs/gettingStarted/AriesDeveloperDemos.md @@ -21,3 +21,12 @@ and then use your new wallet to get and present credentials in some sample scena verifiable credential experience in 30 minutes or less. [BC Gov Showcase]: https://digital.gov.bc.ca/digital-trust/showcase/ + +## Indicio Developer Demo + +Minimal Aca-Py demo that can be used by developers to isolat and test features: + +- Minimal Setup (everything runs in containers) +- Quickly reproduce an issue or demonstrate a feature by writing one simple script or pytest tests. + +[Indicio Aca-Py Minimal Example](https://github.com/Indicio-tech/acapy-minimal-example) diff --git a/docs/testing/UnitTests.md b/docs/testing/UnitTests.md index 786517f132..a167128a13 100644 --- a/docs/testing/UnitTests.md +++ b/docs/testing/UnitTests.md @@ -251,7 +251,7 @@ async def receive_invitation( function.`assert_called_once()` - pytest.mark setup in `setup.cfg` - can be attributed at function or class level. Example, `@pytest.mark.indy` + can be attributed at function or class level. Example, `@pytest.mark.askar` - Code coverage ![Code coverage screenshot](https://i.imgur.com/VhNYcje.png) diff --git a/open-api/openapi.json b/open-api/openapi.json index b2e0ca950e..cb0e2fefe3 100644 --- a/open-api/openapi.json +++ b/open-api/openapi.json @@ -2,7 +2,7 @@ "openapi" : "3.0.1", "info" : { "title" : "Aries Cloud Agent", - "version" : "v0.12.1rc1" + "version" : "v0.12.1" }, "servers" : [ { "url" : "/" diff --git a/open-api/swagger.json b/open-api/swagger.json index 4ee83754fc..a6741ced04 100644 --- a/open-api/swagger.json +++ b/open-api/swagger.json @@ -1,7 +1,7 @@ { "swagger" : "2.0", "info" : { - "version" : "v0.12.1rc1", + "version" : "v0.12.1", "title" : "Aries Cloud Agent" }, "tags" : [ { diff --git a/poetry.lock b/poetry.lock index 6659de5cf9..3dffa73ff2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiohttp" @@ -151,6 +151,17 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + [[package]] name = "anoncreds" version = "0.2.0" @@ -259,33 +270,33 @@ tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", " [[package]] name = "black" -version = "24.3.0" +version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, - {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, - {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, - {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, - {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, - {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, - {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, - {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, - {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, - {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, - {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, - {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, - {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, - {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, - {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, - {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, - {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, - {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, - {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, - {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, - {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, - {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] @@ -853,13 +864,13 @@ files = [ [[package]] name = "ecdsa" -version = "0.16.1" +version = "0.19.0" description = "ECDSA cryptographic signature library (pure python)" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.6" files = [ - {file = "ecdsa-0.16.1-py2.py3-none-any.whl", hash = "sha256:881fa5e12bb992972d3d1b3d4dfbe149ab76a89f13da02daa5ea1ec7dea6e747"}, - {file = "ecdsa-0.16.1.tar.gz", hash = "sha256:cfc046a2ddd425adbd1a78b3c46f0d1325c657811c0f45ecc3a0a6236c1e50ff"}, + {file = "ecdsa-0.19.0-py2.py3-none-any.whl", hash = "sha256:2cea9b88407fdac7bbeca0833b189e4c9c53f2ef1e1eaa29f6224dbc809b707a"}, + {file = "ecdsa-0.19.0.tar.gz", hash = "sha256:60eaad1199659900dd0af521ed462b793bbdf867432b3948e87416ae4caf6bf8"}, ] [package.dependencies] @@ -1444,22 +1455,6 @@ docs = ["alabaster (==0.7.15)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "s lint = ["pre-commit (>=2.4,<4.0)"] tests = ["pytest", "pytz", "simplejson"] -[[package]] -name = "mock" -version = "4.0.3" -description = "Rolling backport of unittest.mock for all Pythons" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"}, - {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"}, -] - -[package.extras] -build = ["blurb", "twine", "wheel"] -docs = ["sphinx"] -test = ["pytest (<5.4)", "pytest-cov"] - [[package]] name = "multidict" version = "6.0.5" @@ -1720,13 +1715,13 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest- [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -1765,13 +1760,13 @@ tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "p [[package]] name = "pre-commit" -version = "3.3.3" +version = "3.7.1" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pre_commit-3.3.3-py2.py3-none-any.whl", hash = "sha256:10badb65d6a38caff29703362271d7dca483d01da88f9d7e05d0b97171c136cb"}, - {file = "pre_commit-3.3.3.tar.gz", hash = "sha256:a2256f489cd913d575c145132ae196fe335da32d91a8294b7afe6622335dd023"}, + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, ] [package.dependencies] @@ -1844,55 +1839,113 @@ files = [ [[package]] name = "pydantic" -version = "1.10.14" -description = "Data validation and settings management using python type hints" +version = "2.7.1" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, - {file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, - {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, - {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, - {file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, - {file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, - {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, - {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, - {file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, - {file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, - {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, - {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, - {file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, - {file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, - {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, - {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, - {file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, - {file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, - {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, - {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, - {file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, - {file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, - {file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, + {file = "pydantic-2.7.1-py3-none-any.whl", hash = "sha256:e029badca45266732a9a79898a15ae2e8b14840b1eabbb25844be28f0b33f3d5"}, + {file = "pydantic-2.7.1.tar.gz", hash = "sha256:e9dbb5eada8abe4d9ae5f46b9939aead650cd2b68f249bb3a8139dbe125803cc"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.18.2" +typing-extensions = ">=4.6.1" [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9e08e867b306f525802df7cd16c44ff5ebbe747ff0ca6cf3fde7f36c05a59a81"}, + {file = "pydantic_core-2.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f0a21cbaa69900cbe1a2e7cad2aa74ac3cf21b10c3efb0fa0b80305274c0e8a2"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0680b1f1f11fda801397de52c36ce38ef1c1dc841a0927a94f226dea29c3ae3d"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95b9d5e72481d3780ba3442eac863eae92ae43a5f3adb5b4d0a1de89d42bb250"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fcf5cd9c4b655ad666ca332b9a081112cd7a58a8b5a6ca7a3104bc950f2038"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b5155ff768083cb1d62f3e143b49a8a3432e6789a3abee8acd005c3c7af1c74"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:553ef617b6836fc7e4df130bb851e32fe357ce36336d897fd6646d6058d980af"}, + {file = "pydantic_core-2.18.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89ed9eb7d616ef5714e5590e6cf7f23b02d0d539767d33561e3675d6f9e3857"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:75f7e9488238e920ab6204399ded280dc4c307d034f3924cd7f90a38b1829563"}, + {file = "pydantic_core-2.18.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ef26c9e94a8c04a1b2924149a9cb081836913818e55681722d7f29af88fe7b38"}, + {file = "pydantic_core-2.18.2-cp310-none-win32.whl", hash = "sha256:182245ff6b0039e82b6bb585ed55a64d7c81c560715d1bad0cbad6dfa07b4027"}, + {file = "pydantic_core-2.18.2-cp310-none-win_amd64.whl", hash = "sha256:e23ec367a948b6d812301afc1b13f8094ab7b2c280af66ef450efc357d2ae543"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:219da3f096d50a157f33645a1cf31c0ad1fe829a92181dd1311022f986e5fbe3"}, + {file = "pydantic_core-2.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc1cfd88a64e012b74e94cd00bbe0f9c6df57049c97f02bb07d39e9c852e19a4"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05b7133a6e6aeb8df37d6f413f7705a37ab4031597f64ab56384c94d98fa0e90"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:224c421235f6102e8737032483f43c1a8cfb1d2f45740c44166219599358c2cd"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b14d82cdb934e99dda6d9d60dc84a24379820176cc4a0d123f88df319ae9c150"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2728b01246a3bba6de144f9e3115b532ee44bd6cf39795194fb75491824a1413"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:470b94480bb5ee929f5acba6995251ada5e059a5ef3e0dfc63cca287283ebfa6"}, + {file = "pydantic_core-2.18.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:997abc4df705d1295a42f95b4eec4950a37ad8ae46d913caeee117b6b198811c"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75250dbc5290e3f1a0f4618db35e51a165186f9034eff158f3d490b3fed9f8a0"}, + {file = "pydantic_core-2.18.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4456f2dca97c425231d7315737d45239b2b51a50dc2b6f0c2bb181fce6207664"}, + {file = "pydantic_core-2.18.2-cp311-none-win32.whl", hash = "sha256:269322dcc3d8bdb69f054681edff86276b2ff972447863cf34c8b860f5188e2e"}, + {file = "pydantic_core-2.18.2-cp311-none-win_amd64.whl", hash = "sha256:800d60565aec896f25bc3cfa56d2277d52d5182af08162f7954f938c06dc4ee3"}, + {file = "pydantic_core-2.18.2-cp311-none-win_arm64.whl", hash = "sha256:1404c69d6a676245199767ba4f633cce5f4ad4181f9d0ccb0577e1f66cf4c46d"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fb2bd7be70c0fe4dfd32c951bc813d9fe6ebcbfdd15a07527796c8204bd36242"}, + {file = "pydantic_core-2.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6132dd3bd52838acddca05a72aafb6eab6536aa145e923bb50f45e78b7251043"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d904828195733c183d20a54230c0df0eb46ec746ea1a666730787353e87182"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9bd70772c720142be1020eac55f8143a34ec9f82d75a8e7a07852023e46617f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8ed04b3582771764538f7ee7001b02e1170223cf9b75dff0bc698fadb00cf3"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6dac87ddb34aaec85f873d737e9d06a3555a1cc1a8e0c44b7f8d5daeb89d86f"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ca4ae5a27ad7a4ee5170aebce1574b375de390bc01284f87b18d43a3984df72"}, + {file = "pydantic_core-2.18.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:886eec03591b7cf058467a70a87733b35f44707bd86cf64a615584fd72488b7c"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ca7b0c1f1c983e064caa85f3792dd2fe3526b3505378874afa84baf662e12241"}, + {file = "pydantic_core-2.18.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b4356d3538c3649337df4074e81b85f0616b79731fe22dd11b99499b2ebbdf3"}, + {file = "pydantic_core-2.18.2-cp312-none-win32.whl", hash = "sha256:8b172601454f2d7701121bbec3425dd71efcb787a027edf49724c9cefc14c038"}, + {file = "pydantic_core-2.18.2-cp312-none-win_amd64.whl", hash = "sha256:b1bd7e47b1558ea872bd16c8502c414f9e90dcf12f1395129d7bb42a09a95438"}, + {file = "pydantic_core-2.18.2-cp312-none-win_arm64.whl", hash = "sha256:98758d627ff397e752bc339272c14c98199c613f922d4a384ddc07526c86a2ec"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:9fdad8e35f278b2c3eb77cbdc5c0a49dada440657bf738d6905ce106dc1de439"}, + {file = "pydantic_core-2.18.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1d90c3265ae107f91a4f279f4d6f6f1d4907ac76c6868b27dc7fb33688cfb347"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390193c770399861d8df9670fb0d1874f330c79caaca4642332df7c682bf6b91"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:82d5d4d78e4448683cb467897fe24e2b74bb7b973a541ea1dcfec1d3cbce39fb"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4774f3184d2ef3e14e8693194f661dea5a4d6ca4e3dc8e39786d33a94865cefd"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d938ec0adf5167cb335acb25a4ee69a8107e4984f8fbd2e897021d9e4ca21b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0e8b1be28239fc64a88a8189d1df7fad8be8c1ae47fcc33e43d4be15f99cc70"}, + {file = "pydantic_core-2.18.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:868649da93e5a3d5eacc2b5b3b9235c98ccdbfd443832f31e075f54419e1b96b"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:78363590ef93d5d226ba21a90a03ea89a20738ee5b7da83d771d283fd8a56761"}, + {file = "pydantic_core-2.18.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:852e966fbd035a6468fc0a3496589b45e2208ec7ca95c26470a54daed82a0788"}, + {file = "pydantic_core-2.18.2-cp38-none-win32.whl", hash = "sha256:6a46e22a707e7ad4484ac9ee9f290f9d501df45954184e23fc29408dfad61350"}, + {file = "pydantic_core-2.18.2-cp38-none-win_amd64.whl", hash = "sha256:d91cb5ea8b11607cc757675051f61b3d93f15eca3cefb3e6c704a5d6e8440f4e"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:ae0a8a797a5e56c053610fa7be147993fe50960fa43609ff2a9552b0e07013e8"}, + {file = "pydantic_core-2.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:042473b6280246b1dbf530559246f6842b56119c2926d1e52b631bdc46075f2a"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a388a77e629b9ec814c1b1e6b3b595fe521d2cdc625fcca26fbc2d44c816804"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25add29b8f3b233ae90ccef2d902d0ae0432eb0d45370fe315d1a5cf231004b"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f459a5ce8434614dfd39bbebf1041952ae01da6bed9855008cb33b875cb024c0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eff2de745698eb46eeb51193a9f41d67d834d50e424aef27df2fcdee1b153845"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8309f67285bdfe65c372ea3722b7a5642680f3dba538566340a9d36e920b5f0"}, + {file = "pydantic_core-2.18.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f93a8a2e3938ff656a7c1bc57193b1319960ac015b6e87d76c76bf14fe0244b4"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:22057013c8c1e272eb8d0eebc796701167d8377441ec894a8fed1af64a0bf399"}, + {file = "pydantic_core-2.18.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cfeecd1ac6cc1fb2692c3d5110781c965aabd4ec5d32799773ca7b1456ac636b"}, + {file = "pydantic_core-2.18.2-cp39-none-win32.whl", hash = "sha256:0d69b4c2f6bb3e130dba60d34c0845ba31b69babdd3f78f7c0c8fae5021a253e"}, + {file = "pydantic_core-2.18.2-cp39-none-win_amd64.whl", hash = "sha256:d9319e499827271b09b4e411905b24a426b8fb69464dfa1696258f53a3334641"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a1874c6dd4113308bd0eb568418e6114b252afe44319ead2b4081e9b9521fe75"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ccdd111c03bfd3666bd2472b674c6899550e09e9f298954cfc896ab92b5b0e6d"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e18609ceaa6eed63753037fc06ebb16041d17d28199ae5aba0052c51449650a9"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e5c584d357c4e2baf0ff7baf44f4994be121e16a2c88918a5817331fc7599d7"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43f0f463cf89ace478de71a318b1b4f05ebc456a9b9300d027b4b57c1a2064fb"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e1b395e58b10b73b07b7cf740d728dd4ff9365ac46c18751bf8b3d8cca8f625a"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0098300eebb1c837271d3d1a2cd2911e7c11b396eac9661655ee524a7f10587b"}, + {file = "pydantic_core-2.18.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:36789b70d613fbac0a25bb07ab3d9dba4d2e38af609c020cf4d888d165ee0bf3"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3f9a801e7c8f1ef8718da265bba008fa121243dfe37c1cea17840b0944dfd72c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3a6515ebc6e69d85502b4951d89131ca4e036078ea35533bb76327f8424531ce"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20aca1e2298c56ececfd8ed159ae4dde2df0781988c97ef77d5c16ff4bd5b400"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:223ee893d77a310a0391dca6df00f70bbc2f36a71a895cecd9a0e762dc37b349"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2334ce8c673ee93a1d6a65bd90327588387ba073c17e61bf19b4fd97d688d63c"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cbca948f2d14b09d20268cda7b0367723d79063f26c4ffc523af9042cad95592"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b3ef08e20ec49e02d5c6717a91bb5af9b20f1805583cb0adfe9ba2c6b505b5ae"}, + {file = "pydantic_core-2.18.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6fdc8627910eed0c01aed6a390a252fe3ea6d472ee70fdde56273f198938374"}, + {file = "pydantic_core-2.18.2.tar.gz", hash = "sha256:2e29d20810dfc3043ee13ac7d9e25105799817683348823f305ab3f349b9386e"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydevd" @@ -1926,19 +1979,19 @@ files = [ [[package]] name = "pydid" -version = "0.4.3" +version = "0.5.0" description = "Python library for validating, constructing, and representing DIDs and DID Documents" optional = false -python-versions = ">=3.8.0,<4.0.0" +python-versions = "<4.0.0,>=3.9.0" files = [ - {file = "pydid-0.4.3-py3-none-any.whl", hash = "sha256:39a586b4f26c41277b93db2aaf0a2db298f48ccc413bdfc71b7dd010045f31f4"}, - {file = "pydid-0.4.3.tar.gz", hash = "sha256:1a48a6940bae8279083ebb7c5ab5fe0249e9ba3ea638de9cf8c127487b96b2ef"}, + {file = "pydid-0.5.0-py3-none-any.whl", hash = "sha256:2562852d2af98ce1a404d64b0826344d811ad78142927da3a84116f1103eac43"}, + {file = "pydid-0.5.0.tar.gz", hash = "sha256:c97c543e019c469fae0939bab454bedf8e010668e746935e3094e13bdfad28d0"}, ] [package.dependencies] inflection = ">=0.5.1,<0.6.0" -pydantic = ">=1.10.0,<2.0.0" -typing-extensions = ">=4.5.0,<5.0.0" +pydantic = ">=2.7.0,<3.0.0" +typing-extensions = ">=4.7.0,<5.0.0" [[package]] name = "pygments" @@ -2022,13 +2075,13 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] [[package]] name = "pytest" -version = "8.0.2" +version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, - {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, + {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, + {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] @@ -2036,21 +2089,21 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.3.0,<2.0" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.23.5" +version = "0.23.7" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.5.tar.gz", hash = "sha256:3a048872a9c4ba14c3e90cc1aa20cbc2def7d01c7c8db3777ec281ba9c057675"}, - {file = "pytest_asyncio-0.23.5-py3-none-any.whl", hash = "sha256:4e7093259ba018d58ede7d5315131d21923a60f8a6e9ee266ce1589685c89eac"}, + {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, + {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, ] [package.dependencies] @@ -2062,13 +2115,13 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "4.1.0" +version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] @@ -2076,20 +2129,21 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-ruff" -version = "0.1.1" +version = "0.3.2" description = "pytest plugin to check ruff requirements." optional = false -python-versions = ">=3.7,<4.0" +python-versions = "<4.0,>=3.8" files = [ - {file = "pytest_ruff-0.1.1-py3-none-any.whl", hash = "sha256:db33c8d32d730d61d372c1ac4615b1036c47a14c781cbc0ae71811c4cadadc47"}, - {file = "pytest_ruff-0.1.1.tar.gz", hash = "sha256:f599768ff3834d6b1d6d26b25a030a5b1dcc9cf187239bd9621a7f25f7d8fe46"}, + {file = "pytest_ruff-0.3.2-py3-none-any.whl", hash = "sha256:5096578df2240b2a99f7376747bc433ce25e590c7d570d5c2b47f725497f2c10"}, + {file = "pytest_ruff-0.3.2.tar.gz", hash = "sha256:8d82882969e52b664a7cef4465cba63e45173f38d907dffeca41d9672f59b6c6"}, ] [package.dependencies] +pytest = ">=5" ruff = ">=0.0.242" [[package]] @@ -2117,22 +2171,6 @@ files = [ {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, ] -[[package]] -name = "python3-indy" -version = "1.16.0.post286" -description = "This is the official SDK for Hyperledger Indy (https://www.hyperledger.org/projects), which provides a distributed-ledger-based foundation for self-sovereign identity (https://sovrin.org). The major artifact of the SDK is a c-callable library." -optional = true -python-versions = "*" -files = [ - {file = "python3-indy-1.16.0-post-286.tar.gz", hash = "sha256:80e6a4241134ea3ef8b2554cffb11e504978f87edb004a1c965ec6eb063449a4"}, -] - -[package.dependencies] -base58 = "*" - -[package.extras] -test = ["base58", "pytest (<3.7)", "pytest-asyncio (==0.10.0)"] - [[package]] name = "pytz" version = "2021.1" @@ -2292,28 +2330,28 @@ test = ["hypothesis (==5.19.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] name = "ruff" -version = "0.1.2" -description = "An extremely fast Python linter, written in Rust." +version = "0.4.4" +description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.2-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:0d3ee66b825b713611f89aa35d16de984f76f26c50982a25d52cd0910dff3923"}, - {file = "ruff-0.1.2-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:f85f850a320ff532b8f93e8d1da6a36ef03698c446357c8c43b46ef90bb321eb"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:809c6d4e45683696d19ca79e4c6bd3b2e9204fe9546923f2eb3b126ec314b0dc"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46005e4abb268e93cad065244e17e2ea16b6fcb55a5c473f34fbc1fd01ae34cb"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10cdb302f519664d5e2cf954562ac86c9d20ca05855e5b5c2f9d542228f45da4"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f89ebcbe57a1eab7d7b4ceb57ddf0af9ed13eae24e443a7c1dc078000bd8cc6b"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7344eaca057d4c32373c9c3a7afb7274f56040c225b6193dd495fcf69453b436"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dffa25f6e03c4950b6ac6f216bc0f98a4be9719cb0c5260c8e88d1bac36f1683"}, - {file = "ruff-0.1.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42ddaea52cb7ba7c785e8593a7532866c193bc774fe570f0e4b1ccedd95b83c5"}, - {file = "ruff-0.1.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8533efda625bbec0bf27da2886bd641dae0c209104f6c39abc4be5b7b22de2a"}, - {file = "ruff-0.1.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b0b1b82221ba7c50e03b7a86b983157b5d3f4d8d4f16728132bdf02c6d651f77"}, - {file = "ruff-0.1.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c1362eb9288f8cc95535294cb03bd4665c8cef86ec32745476a4e5c6817034c"}, - {file = "ruff-0.1.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ffa7ef5ded0563329a35bd5a1cfdae40f05a75c0cc2dd30f00b1320b1fb461fc"}, - {file = "ruff-0.1.2-py3-none-win32.whl", hash = "sha256:6e8073f85e47072256e2e1909f1ae515cf61ff5a4d24730a63b8b4ac24b6704a"}, - {file = "ruff-0.1.2-py3-none-win_amd64.whl", hash = "sha256:b836ddff662a45385948ee0878b0a04c3a260949905ad861a37b931d6ee1c210"}, - {file = "ruff-0.1.2-py3-none-win_arm64.whl", hash = "sha256:b0c42d00db5639dbd5f7f9923c63648682dd197bf5de1151b595160c96172691"}, - {file = "ruff-0.1.2.tar.gz", hash = "sha256:afd4785ae060ce6edcd52436d0c197628a918d6d09e3107a892a1bad6a4c6608"}, + {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"}, + {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"}, + {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"}, + {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"}, + {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"}, + {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"}, + {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"}, + {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"}, ] [[package]] @@ -2805,9 +2843,8 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [extras] askar = ["anoncreds", "aries-askar", "indy-credx", "indy-vdr"] bbs = ["ursa-bbs-signatures"] -indy = ["python3-indy"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "6aef813877ebf9ed3259ae2dbe0efbe2a0b7f0192a2f3881f52336c4c05c8c8b" +content-hash = "6cf57f6ea8c6d733f9a834939795b57a633008f32291f7c05ac2f32f4e8192af" diff --git a/pyproject.toml b/pyproject.toml index 5d5c414031..9533b3f205 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "aries_cloudagent" -version = "0.12.1rc1" +version = "0.12.1" description = "Hyperledger Aries Cloud Agent Python (ACA-Py) is a foundation for building decentralized identity applications and services running in non-mobile environments. " authors = ["Hyperledger Aries "] license = "Apache-2.0" @@ -24,7 +24,7 @@ async-timeout="~4.0.2" base58="~2.1.0" ConfigArgParse="~1.5.3" deepmerge="~0.3.0" -ecdsa="~0.16.1" +ecdsa="~0.19.0" jsonpath-ng="1.6.1" Markdown="~3.5.2" markupsafe="2.0.1" @@ -33,7 +33,7 @@ nest_asyncio="~1.5.5" packaging="~23.1" portalocker="~2.7.0" prompt_toolkit=">=2.0.9,<2.1.0" -pydid="^0.4.3" +pydid="^0.5.0" pyjwt="~2.8.0" pyld="^2.0.4" pynacl="~1.5.0" @@ -58,16 +58,13 @@ anoncreds= { version = "0.2.0", optional = true } # bbs ursa-bbs-signatures= { version = "~1.0.1", optional = true } -# indy -python3-indy= { version = "^1.11.1", optional = true } - [tool.poetry.group.dev.dependencies] -pre-commit="~3.3.3" +pre-commit = "~3.7.0" # Sync with version in .pre-commit-config.yaml -ruff = "0.1.2" +ruff = "0.4.4" # Sync with version in .github/workflows/blackformat.yml # Sync with version in .pre-commit-config.yaml -black="24.3.0" +black = "24.4.2" sphinx="1.8.4" sphinx-rtd-theme=">=0.4.3" @@ -78,11 +75,10 @@ pydevd="1.5.1" pydevd-pycharm="~193.6015.39" # testing -pytest= "^8.0.0" -pytest-asyncio= "^0.23.5" -pytest-cov= "4.1.0" -pytest-ruff="^0.1.1" -mock= "~4.0" +pytest = "^8.2.0" +pytest-asyncio = "^0.23.6" +pytest-cov = "^5.0.0" +pytest-ruff = "^0.3.2" [tool.poetry.extras] askar = [ @@ -94,17 +90,14 @@ askar = [ bbs = [ "ursa-bbs-signatures" ] -indy = [ - "python3-indy" -] [tool.poetry.scripts] aca-py = "aries_cloudagent.__main__:script_main" [tool.ruff] -select = ["B006", "C", "D", "E", "F"] +lint.select = ["B006", "C", "D", "E", "F"] -ignore = [ +lint.ignore = [ # Google Python Doc Style "D203", "D204", "D213", "D215", "D400", "D401", "D404", "D406", "D407", "D408", "D409", "D413", @@ -118,7 +111,7 @@ include = ["aries_cloudagent/**/*.py"] line-length = 90 -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "**/{tests}/*" = ["B006", "D", "E501", "F841"] [tool.pytest.ini_options] diff --git a/scripts/run_tests_indy b/scripts/run_tests_indy deleted file mode 100755 index 37fab8d5e7..0000000000 --- a/scripts/run_tests_indy +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -cd "$(dirname "$0")" || exit -CONTAINER_RUNTIME="${CONTAINER_RUNTIME:-docker}" - -DOCKER_BUILDKIT=1 $CONTAINER_RUNTIME build \ - -t aries-cloudagent-test \ - -f ../docker/Dockerfile.indy \ - --target acapy-test .. \ - --build-arg indy_version=1.16.0 \ - || exit 1 - -if [ ! -d ../test-reports ]; then mkdir ../test-reports; fi - -# on Windows, docker run needs to be prefixed by winpty -if [ "$OSTYPE" == "msys" ]; then - CONTAINER_RUNTIME="winpty docker" -fi -if [ -z "$DOCKER_NET" ]; then - DOCKER_NET="bridge" -fi - -if [ -z "$POSTGRES_URL" ]; then - if [ -n "$($CONTAINER_RUNTIME ps --filter name=indy-demo-postgres --quiet)" ]; then - DOCKER_ARGS="$DOCKER_ARGS --link indy-demo-postgres" - POSTGRES_URL="indy-demo-postgres" - fi -fi -if [ -n "$POSTGRES_URL" ]; then - DOCKER_ARGS="$DOCKER_ARGS -e POSTGRES_URL=$POSTGRES_URL" -fi - -$CONTAINER_RUNTIME run --rm -ti --name aries-cloudagent-runner \ - --network=${DOCKER_NET} \ - -v "$(pwd)/../test-reports:/home/indy/src/app/test-reports" \ - $DOCKER_ARGS \ - aries-cloudagent-test "$@"