diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9689f48af33..988e061080d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,4 +1 @@ -# This file controls which users are required reviewers for given segments of the repository - -* @inrupt/enterprise-solid-server - +* @inrupt/sdk-developers diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7d6895c31a0..80a015c8e8a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,126 +3,86 @@ updates: - package-ecosystem: "maven" directory: "/" schedule: - interval: "weekly" - ignore: - - dependency-name: "org.eclipse.rdf4j:rdf4j-*" - - dependency-name: "org.springframework.security:spring-*" - - - package-ecosystem: "maven" - directory: "/rdf4j/" - schedule: - interval: "weekly" - allow: - - dependency-name: "org.eclipse.rdf4j:rdf4j-*" - - - package-ecosystem: "maven" - directory: "/spring/" - schedule: - interval: "weekly" - allow: - - dependency-name: "org.springframework.security:spring-*" - - - package-ecosystem: "gradle" - directory: "/gradle/" - schedule: - interval: "weekly" + interval: "cron" + cronjob: "30 4 1,15 * *" + groups: + plugins: + patterns: + - "org.apache.maven.plugins:*" + - "com.mycila:license-maven-plugin" + - "org.jacoco:jacoco-maven-plugin" + - "org.sonarsource.scanner.maven:sonar-maven-plugin" + - "org.sonatype.plugins:nexus-staging-maven-plugin" + - "org.owasp:dependency-check-maven" + - "com.puppycrawl.tools:checkstyle" - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "monthly" -# 1.0 Maintenance +# 1.3 Maintenance - package-ecosystem: "maven" directory: "/" schedule: - interval: "weekly" - target-branch: "1.0" - ignore: - - dependency-name: "org.eclipse.rdf4j:rdf4j-*" - labels: - - "backport" - - "1.0" - - - package-ecosystem: "maven" - directory: "/rdf4j/" - schedule: - interval: "weekly" - target-branch: "1.0" - allow: - - dependency-name: "org.eclipse.rdf4j:rdf4j-*" - labels: - - "backport" - - "1.0" - - - package-ecosystem: "gradle" - directory: "/gradle/" - schedule: - interval: "weekly" - target-branch: "1.0" - labels: - - "backport" - - "1.0" - - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - target-branch: "1.0" - labels: - - "backport" - - "1.0" - -# 1.1 Maintenance - - package-ecosystem: "maven" - directory: "/" - schedule: - interval: "weekly" - target-branch: "1.1" + interval: "cron" + cronjob: "30 4 1,15 * *" + target-branch: "1.3" ignore: - dependency-name: "org.eclipse.rdf4j:rdf4j-*" - dependency-name: "org.springframework.security:spring-*" + - dependency-name: "com.google.protobuf:protobuf-java" + update-types: + - "version-update:semver-major" + - dependency-name: "org.junit.jupiter:junit-*" + update-types: + - "version-update:semver-major" + - dependency-name: "org.junit:junit-*" + update-types: + - "version-update:semver-major" labels: - "backport" - - "1.1" + - "1.3" + groups: + plugins: + patterns: + - "org.apache.maven.plugins:*" + - "com.mycila:license-maven-plugin" + - "org.jacoco:jacoco-maven-plugin" + - "org.sonarsource.scanner.maven:sonar-maven-plugin" + - "org.sonatype.plugins:nexus-staging-maven-plugin" + - "org.owasp:dependency-check-maven" - package-ecosystem: "maven" directory: "/rdf4j/" schedule: - interval: "weekly" - target-branch: "1.1" + interval: "cron" + cronjob: "30 4 1,15 * *" + target-branch: "1.3" allow: - dependency-name: "org.eclipse.rdf4j:rdf4j-*" labels: - "backport" - - "1.1" + - "1.3" - package-ecosystem: "maven" directory: "/spring/" schedule: - interval: "weekly" - target-branch: "1.1" + interval: "cron" + cronjob: "30 4 1,15 * *" + target-branch: "1.3" allow: - dependency-name: "org.springframework.security:spring-*" labels: - "backport" - - "1.1" - - - package-ecosystem: "gradle" - directory: "/gradle/" - schedule: - interval: "weekly" - target-branch: "1.1" - labels: - - "backport" - - "1.1" + - "1.3" - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" - target-branch: "1.1" + interval: "monthly" + target-branch: "1.3" labels: - "backport" - - "1.1" + - "1.3" diff --git a/.github/workflows/cd-config.yml b/.github/workflows/cd-config.yml index 245988229e3..0e5c10abf9a 100644 --- a/.github/workflows/cd-config.yml +++ b/.github/workflows/cd-config.yml @@ -9,9 +9,13 @@ on: - inrupt-client-[0-9]+.[0-9]+.[0-9]+.Alpha[0-9]+ - inrupt-client-[0-9]+.[0-9]+.[0-9]+.Beta[0-9]+ +permissions: {} + jobs: deployment: name: Deploy artifacts + permissions: + contents: read runs-on: ubuntu-latest environment: name: ${{ matrix.envName }} @@ -27,15 +31,15 @@ jobs: envName: "Development" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up JDK 17 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 17 cache: 'maven' - server-id: 'ossrh' + server-id: 'central' server-username: MAVEN_REPO_USERNAME server-password: MAVEN_REPO_TOKEN gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }} @@ -54,49 +58,5 @@ jobs: - name: Sonar Analysis if: ${{ github.actor != 'dependabot[bot]' }} - run: mvn sonar:sonar -Dsonar.login=${{ secrets.SONARQUBE_TOKEN }} - - site: - name: Publish version-specific site - runs-on: ubuntu-latest - if: ${{ github.actor != 'dependabot[bot]' }} - environment: - name: "Documentation" - - steps: - - uses: actions/checkout@v4 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: 17 - cache: 'maven' - - - name: Build the code with Maven - run: mvn -B -ntp install -Pwebsite - - - name: Build the site with Maven - run: mvn -B -ntp site site:stage -Pwebsite - - - name: Set site version - run: | - VERSION=$(mvn -q help:evaluate -Dexpression=project.version -DforceStdout) - echo "VERSION_DIR=./${VERSION}/" >> $GITHUB_ENV - - - name: Publish tagged site to GitHub pages - uses: peaceiris/actions-gh-pages@v3 - if: ${{ startsWith(github.event.ref, 'refs/tags/inrupt-client') }} - with: - keep_files: true - publish_dir: ./target/staging/ - personal_token: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish versioned site to GitHub pages - uses: peaceiris/actions-gh-pages@v3 - with: - keep_files: true - publish_dir: ./target/staging/ - destination_dir: ${{ env.VERSION_DIR }} - personal_token: ${{ secrets.GITHUB_TOKEN }} + run: mvn sonar:sonar -Dsonar.token=${{ secrets.SONARQUBE_TOKEN }} diff --git a/.github/workflows/ci-config.yml b/.github/workflows/ci-config.yml index 5b48356e2cd..f2144b28bf8 100644 --- a/.github/workflows/ci-config.yml +++ b/.github/workflows/ci-config.yml @@ -3,21 +3,26 @@ name: Solid Java Client CI on: # Build pull requests for any branch pull_request: { } + merge_group: { } + +permissions: {} jobs: build: name: Java environment + permissions: + contents: read runs-on: ubuntu-latest strategy: matrix: - java: [ 11, 17, 21 ] + java: [ 17, 21 ] fail-fast: false steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -40,33 +45,20 @@ jobs: INRUPT_TEST_REQUEST_METADATA_FEATURE: false INRUPT_TEST_REQUEST_METADATA_HEADERS_THAT_PROPAGATE: ${{ secrets.INRUPT_PROD_HEADERS_THAT_PROPAGATE }} - - name: Dev Integration tests - if: ${{ github.actor != 'dependabot[bot]' && matrix.java == 11 }} - continue-on-error: true - run: mvn -B -ntp verify -Pci -pl integration/uma,integration/openid - env: - INRUPT_TEST_WEBID: ${{ secrets.INRUPT_DEV_WEBID }} - INRUPT_TEST_ACCESS_GRANT_PROVIDER: ${{ secrets.INRUPT_DEV_ACCESS_GRANT_PROVIDER }} - INRUPT_TEST_CLIENT_ID: ${{ secrets.INRUPT_DEV_CLIENT_ID }} - INRUPT_TEST_CLIENT_SECRET: ${{ secrets.INRUPT_DEV_CLIENT_SECRET }} - INRUPT_TEST_REQUESTER_WEBID: ${{ secrets.INRUPT_DEV_REQUESTER_WEBID }} - INRUPT_TEST_REQUESTER_CLIENT_ID: ${{ secrets.INRUPT_DEV_REQUESTER_CLIENT_ID }} - INRUPT_TEST_REQUESTER_CLIENT_SECRET: ${{ secrets.INRUPT_DEV_REQUESTER_CLIENT_SECRET }} - INRUPT_TEST_REQUEST_METADATA_FEATURE: true - INRUPT_TEST_REQUEST_METADATA_HEADERS_THAT_PROPAGATE: ${{ secrets.INRUPT_DEV_HEADERS_THAT_PROPAGATE }} - performance: name: Performance Tests + permissions: + contents: read runs-on: ubuntu-latest strategy: matrix: java: [ 17 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -78,16 +70,18 @@ jobs: documentation: name: Documentation Check + permissions: + contents: read runs-on: ubuntu-latest strategy: matrix: java: [ 17 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} @@ -96,33 +90,41 @@ jobs: - name: Build the code with Maven run: mvn -B -ntp verify -Pwebsite javadoc:javadoc - dependencies: - name: Dependency Check + sonar: + name: Sonar Scan + permissions: + contents: read runs-on: ubuntu-latest - strategy: - matrix: - java: [ 17 ] + if: ${{ github.actor != 'dependabot[bot]' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v5 with: distribution: 'temurin' - java-version: ${{ matrix.java }} + java-version: 17 cache: 'maven' - - name: Generate Cache Name - shell: bash - run: echo "CACHE_NAME=$(date '+%y.%j')" >> $GITHUB_ENV + - name: Build the code with Maven + run: mvn -B -ntp install -Pci + + - name: Sonar Analysis + run: mvn sonar:sonar -Dsonar.token=${{ secrets.SONARQUBE_TOKEN }} - - name: Restore NVD data cache - uses: actions/cache@v3 - with: - key: nvd-data-${{ env.CACHE_NAME }} - restore-keys: nvd-data- - path: ./data/cache - - name: Verify dependencies - run: mvn -B -ntp verify -Pdependencies -Dnvd.api.datafeed="file:${GITHUB_WORKSPACE}/data/cache/nvdcve-{0}.json.gz" + check: + if: always() + needs: + - build + - performance + - documentation + - sonar + runs-on: ubuntu-latest + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # release/v1 + with: + allowed-skips: sonar, performance + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/nvd-cache.yml b/.github/workflows/nvd-cache.yml deleted file mode 100644 index 98e4cc05c95..00000000000 --- a/.github/workflows/nvd-cache.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: NVD Data Workflow Schedule - -on: - schedule: - - cron: '0 5 * * 1,2,3,4,5' - workflow_dispatch: { } - -jobs: - build: - name: Build and collect data - runs-on: ubuntu-latest - if: ${{ github.actor != 'dependabot[bot]' }} - - steps: - - name: Checkout OVP repository - uses: actions/checkout@v4 - with: - repository: jeremylong/Open-Vulnerability-Project - path: ovp - ref: v5.1.1 - - - name: Set up JDK 17 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: 17 - cache: 'gradle' - - - name: Build the OVP code with Gradle - working-directory: ./ovp - run: ./gradlew build -x test - - - uses: actions/checkout@v4 - with: - path: data - - - name: Generate Cache Name - shell: bash - run: echo "CACHE_NAME=$(date '+%y.%j')" >> $GITHUB_ENV - - - name: Rename artifact - run: find ./ovp/vulnz/build/libs -type f -regex './ovp/vulnz/build/libs/vulnz-[0-9].[0-9].[0-9].jar' -exec mv {} ./data/vulnz.jar ';' - - - name: Generate data - working-directory: ./data - run: ./vulnz.jar cve --cache --directory ./cache - env: - NVD_API_KEY: ${{ secrets.NVD_API_KEY }} - - - name: Cache NVD data - uses: actions/cache@v3 - with: - key: nvd-data-${{ env.CACHE_NAME }} - path: ./data/cache diff --git a/.github/workflows/site-ci-config.yml b/.github/workflows/site-ci-config.yml index ba7e9ff08a3..9a3e7af2d1b 100644 --- a/.github/workflows/site-ci-config.yml +++ b/.github/workflows/site-ci-config.yml @@ -6,19 +6,23 @@ on: paths: - '**/site/**' +permissions: {} + jobs: site: name: Project site + permissions: + contents: read runs-on: ubuntu-latest strategy: matrix: - java: [ 11 ] + java: [ 17 ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: ${{ matrix.java }} diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index 84d1e60d8d2..00000000000 --- a/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import java.io.IOException; -import java.io.InputStream; -import java.net.Authenticator; -import java.net.PasswordAuthentication; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; - -public final class MavenWrapperDownloader -{ - private static final String WRAPPER_VERSION = "3.2.0"; - - private static final boolean VERBOSE = Boolean.parseBoolean( System.getenv( "MVNW_VERBOSE" ) ); - - public static void main( String[] args ) - { - log( "Apache Maven Wrapper Downloader " + WRAPPER_VERSION ); - - if ( args.length != 2 ) - { - System.err.println( " - ERROR wrapperUrl or wrapperJarPath parameter missing" ); - System.exit( 1 ); - } - - try - { - log( " - Downloader started" ); - final URL wrapperUrl = new URL( args[0] ); - final String jarPath = args[1].replace( "..", "" ); // Sanitize path - final Path wrapperJarPath = Paths.get( jarPath ).toAbsolutePath().normalize(); - downloadFileFromURL( wrapperUrl, wrapperJarPath ); - log( "Done" ); - } - catch ( IOException e ) - { - System.err.println( "- Error downloading: " + e.getMessage() ); - if ( VERBOSE ) - { - e.printStackTrace(); - } - System.exit( 1 ); - } - } - - private static void downloadFileFromURL( URL wrapperUrl, Path wrapperJarPath ) - throws IOException - { - log( " - Downloading to: " + wrapperJarPath ); - if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null ) - { - final String username = System.getenv( "MVNW_USERNAME" ); - final char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray(); - Authenticator.setDefault( new Authenticator() - { - @Override - protected PasswordAuthentication getPasswordAuthentication() - { - return new PasswordAuthentication( username, password ); - } - } ); - } - try ( InputStream inStream = wrapperUrl.openStream() ) - { - Files.copy( inStream, wrapperJarPath, StandardCopyOption.REPLACE_EXISTING ); - } - log( " - Downloader complete" ); - } - - private static void log( String msg ) - { - if ( VERBOSE ) - { - System.out.println( msg ); - } - } - -} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 6d3a56651da..0e9982859c1 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,18 +1,3 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. +wrapperVersion=3.3.4 +distributionType=only-script distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/README.md b/README.md index e4400febcd8..a3a423aa9fa 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The Inrupt Java Client Libraries provide highly flexible, simple components that ## Using the libraries as a dependency in your own projects -To use the Inrupt Java Client Libraries in your projects make sure to visit the [Getting Started page](https://docs.inrupt.com/developer-tools/java/client-libraries/getting-started/) of the documentation. +To use the Inrupt Java Client Libraries in your projects make sure to visit the [Getting Started page](https://docs.inrupt.com/sdk/java-sdk/tutorial) of the documentation. Next we will document how to use this repository locally. @@ -18,7 +18,7 @@ After cloning the repository locally you can work with the code as follows: ### Code build -The project can be built with Maven and a Java 11+ build environment. +The project can be built with Maven and a Java 17+ build environment. ```bash ./mvnw install @@ -63,8 +63,8 @@ forum is a good place to meet the rest of the community. ## Documentation -- [Inrupt Java Client Libraries getting started](https://docs.inrupt.com/developer-tools/java/client-libraries/getting-started/) -- [Inrupt Java Client Libraries javadocs](https://docs.inrupt.com/developer-tools/api/java/inrupt-client/latest/) +- [Inrupt Java Client Libraries getting started](https://docs.inrupt.com/sdk/java-sdk/tutorial) +- [Inrupt Java Client Libraries javadocs](https://api.docs.inrupt.com/docs/developer-tools/api/java/inrupt-client/latest/) ## Changelog diff --git a/access-grant/pom.xml b/access-grant/pom.xml index e08c5f14434..b55865f6a99 100644 --- a/access-grant/pom.xml +++ b/access-grant/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-accessgrant @@ -13,11 +13,6 @@ Access Grant support for the Inrupt Client Libraries. - - 1.8 - 1.8 - - com.inrupt.client diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java index d376ce5e040..32f05594db6 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredential.java @@ -37,6 +37,7 @@ /** A base class for access credentials. **/ public class AccessCredential { + static final int SINGLETON = 1; private static final Logger LOGGER = LoggerFactory.getLogger(AccessCredential.class); protected static final String TYPE = "type"; @@ -49,6 +50,7 @@ public class AccessCredential { private final Set modes; private final Set purposes; private final Set resources; + private final Set templates; private final URI recipient; private final URI creator; private final Instant expiration; @@ -71,6 +73,7 @@ protected AccessCredential(final URI identifier, final String credential, this.purposes = data.getPurposes(); this.resources = data.getResources(); + this.templates = data.getTemplates(); this.modes = data.getModes(); this.recipient = data.getRecipient(); @@ -165,6 +168,15 @@ public Set getResources() { return resources; } + /** + * Get the templates associated with the access credential. + * + * @return the associated templates + */ + public Set getTemplates() { + return templates; + } + /** * Get the creator of this access credential. * @@ -281,7 +293,9 @@ public static class CredentialData { private final Set modes; private final Set purposes; private final Set resources; + private final Set templates; private final URI recipient; + private final URI accessRequest; /** * Create a collection of user-managed credential data. @@ -293,10 +307,41 @@ public static class CredentialData { */ public CredentialData(final Set resources, final Set modes, final Set purposes, final URI recipient) { + this(resources, Collections.emptySet(), modes, purposes, recipient, null); + } + + /** + * Create a collection of user-managed credential data. + * + * @param resources the resources referenced by the credential + * @param modes the access modes defined by this credential + * @param purposes the purposes associated with this credential + * @param recipient the recipient for this credential, may be {@code null} + * @param accessRequest the access request identifier, may be {@code null} + */ + public CredentialData(final Set resources, final Set modes, + final Set purposes, final URI recipient, final URI accessRequest) { + this(resources, Collections.emptySet(), modes, purposes, recipient, accessRequest); + } + + /** + * Create a collection of user-managed credential data. + * + * @param resources the resources referenced by the credential + * @param templates the resource templates referenced by the credential + * @param modes the access modes defined by this credential + * @param purposes the purposes associated with this credential + * @param recipient the recipient for this credential, may be {@code null} + * @param accessRequest the access request identifier, may be {@code null} + */ + public CredentialData(final Set resources, final Set templates, final Set modes, + final Set purposes, final URI recipient, final URI accessRequest) { + this.templates = Objects.requireNonNull(templates, "templates may not be null!"); this.modes = Objects.requireNonNull(modes, "modes may not be null!"); this.purposes = Objects.requireNonNull(purposes, "purposes may not be null!"); this.resources = Objects.requireNonNull(resources, "resources may not be null!"); this.recipient = recipient; + this.accessRequest = accessRequest; } /** @@ -326,6 +371,15 @@ public Set getResources() { return resources; } + /** + * Get the URL templates associated with this credential. + * + * @return the URL templates + */ + public Set getTemplates() { + return templates; + } + /** * Get the recipient associated with this credential. * @@ -334,6 +388,15 @@ public Set getResources() { public URI getRecipient() { return recipient; } + + /** + * Get the access request identifier associated with this credential. + * + * @return the access request identifier, may be {@code null} + */ + public URI getAccessRequest() { + return accessRequest; + } } static CredentialMetadata extractMetadata(final Map data) { @@ -368,7 +431,7 @@ static Stream filterUris(final String uri) { try { return Stream.of(URI.create(uri)); } catch (final IllegalArgumentException ex) { - LOGGER.debug("Ignoring non-URI purpose: {}", ex.getMessage()); + LOGGER.atDebug().setMessage("Ignoring non-URI purpose: {}").addArgument(ex::getMessage).log(); } return Stream.empty(); } diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredentialQuery.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredentialQuery.java deleted file mode 100644 index fa2c087a5a5..00000000000 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessCredentialQuery.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright Inrupt Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.inrupt.client.accessgrant; - -import java.net.URI; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -/** - * An object to represent an access credential query. - * - * @param The access credential type - */ -public class AccessCredentialQuery { - - private final Set purposes; - private final Set modes; - private final URI resource; - private final URI creator; - private final URI recipient; - private final Class clazz; - - /** - * Create an access credential query. - * - * @param resource the resource, may be {@code null} - * @param creator the creator, may be {@code null} - * @param recipient the recipient, may be {@code null} - * @param purposes the purposes, never {@code null} - * @param modes the access modes, never {@code null} - * @param clazz the credential type, never {@code null} - */ - AccessCredentialQuery(final URI resource, final URI creator, final URI recipient, - final Set purposes, final Set modes, final Class clazz) { - this.clazz = Objects.requireNonNull(clazz, "The clazz parameter must not be null!"); - this.resource = resource; - this.creator = creator; - this.recipient = recipient; - this.purposes = purposes; - this.modes = modes; - } - - /** - * Get the requested resource. - * - * @return the resource, may be {@code null} - */ - public URI getResource() { - return resource; - } - - /** - * Get the requested creator. - * - * @return the creator, may be {@code null} - */ - public URI getCreator() { - return creator; - } - - /** - * Get the requested recipient. - * - * @return the recipient, may be {@code null} - */ - public URI getRecipient() { - return recipient; - } - - /** - * Get the requested purposes. - * - * @return the purpose identifiers, never {@code null} - */ - public Set getPurposes() { - return purposes; - } - - /** - * Get the requested access modes. - * - * @return the access modes, never {@code null} - */ - public Set getModes() { - return modes; - } - - /* package private */ - Class getAccessCredentialType() { - return clazz; - } - - /** - * Create a new access credential query builder. - * - * @return the builder - */ - public static Builder newBuilder() { - return new Builder(); - } - - /** - * A builder class for access credential queries. - */ - public static class Builder { - - private final Set purposes = new HashSet<>(); - private final Set modes = new HashSet<>(); - private URI builderResource; - private URI builderCreator; - private URI builderRecipient; - - /** - * Set the resource identifier. - * - * @param resource the resource identifier, may be {@code null} - * @return this builder - */ - public Builder resource(final URI resource) { - builderResource = resource; - return this; - } - - /** - * Add a purpose identifier. - * - * @param purpose a purpose identifier; {@code null} values have no effect. - * @return this builder - */ - public Builder purpose(final URI purpose) { - if (purpose != null) { - purposes.add(purpose); - } - return this; - } - - /** - * Add an access mode value. - * - * @param mode a mode value; {@code null} values have no effect. - * @return this builder - */ - public Builder mode(final String mode) { - if (mode != null) { - modes.add(mode); - } - return this; - } - - /** - * Set the creator identifier. - * - * @param creator the creator identifier, may be {@code null} - * @return this builder - */ - public Builder creator(final URI creator) { - builderCreator = creator; - return this; - } - - /** - * Set the recipient identifier. - * - * @param recipient the recipient identifier, may be {@code null} - * @return this builder - */ - public Builder recipient(final URI recipient) { - builderRecipient = recipient; - return this; - } - - /** - * Build the access credential query. - * - * @param the credential type - * @param clazz the credential type - * @return the query object - */ - public AccessCredentialQuery build(final Class clazz) { - return new AccessCredentialQuery<>(builderResource, builderCreator, builderRecipient, purposes, modes, - clazz); - } - } -} diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessDenial.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessDenial.java index ffb9c4d7b83..6ad694f9bb3 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessDenial.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessDenial.java @@ -49,6 +49,8 @@ public class AccessDenial extends AccessCredential { private static final Set supportedTypes = getSupportedTypes(); private static final JsonService jsonService = ServiceProvider.getJsonService(); + private final URI accessRequest; + /** * Read a verifiable presentation as an AccessDenial. * @@ -60,6 +62,16 @@ public class AccessDenial extends AccessCredential { protected AccessDenial(final URI identifier, final String credential, final CredentialData data, final CredentialMetadata metadata) { super(identifier, credential, data, metadata); + this.accessRequest = data.getAccessRequest(); + } + + /** + * Get the corresponding access request identifier. + * + * @return the access request identifier, may be {@code null} + */ + public URI getAccessRequest() { + return accessRequest; } /** @@ -93,18 +105,19 @@ public static AccessDenial of(final InputStream serialization) { static Set getSupportedTypes() { final Set types = new HashSet<>(); types.add("SolidAccessDenial"); + types.add("vc:SolidAccessDenial"); types.add("http://www.w3.org/ns/solid/vc#SolidAccessDenial"); return Collections.unmodifiableSet(types); } static AccessDenial parse(final String serialization) throws IOException { - try (final InputStream in = new ByteArrayInputStream(serialization.getBytes())) { + try (final InputStream in = new ByteArrayInputStream(serialization.getBytes(UTF_8))) { // TODO process as JSON-LD final Map data = jsonService.fromJson(in, new HashMap(){}.getClass().getGenericSuperclass()); final List> vcs = getCredentialsFromPresentation(data, supportedTypes); - if (vcs.size() != 1) { + if (vcs.size() != SINGLETON) { throw new IllegalArgumentException( "Invalid Access Denial: ambiguous number of verifiable credentials"); } @@ -126,12 +139,14 @@ static AccessDenial parse(final String serialization) throws IOException { final Optional other = asUri(consent.get("isProvidedTo")); final URI recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null))); + final URI accessRequest = asUri(consent.get("request")).orElse(null); final Set modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet); final Set resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet) .stream().map(URI::create).collect(Collectors.toSet()); final Set purposes = asSet(consent.get("forPurpose")).orElseGet(Collections::emptySet) .stream().flatMap(AccessCredential::filterUris).collect(Collectors.toSet()); - final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient); + final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient, + accessRequest); return new AccessDenial(identifier, serialization, credentialData, credentialMetadata); } else { diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrant.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrant.java index ab5ab7e7e75..c9fec2814b5 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrant.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrant.java @@ -49,6 +49,8 @@ public class AccessGrant extends AccessCredential { private static final Set supportedTypes = getSupportedTypes(); private static final JsonService jsonService = ServiceProvider.getJsonService(); + private final URI accessRequest; + /** * Read a verifiable presentation as an AccessGrant. * @@ -60,6 +62,16 @@ public class AccessGrant extends AccessCredential { protected AccessGrant(final URI identifier, final String credential, final CredentialData data, final CredentialMetadata metadata) { super(identifier, credential, data, metadata); + this.accessRequest = data.getAccessRequest(); + } + + /** + * Get the corresponding access request identifier. + * + * @return the access request identifier, may be {@code null} + */ + public URI getAccessRequest() { + return accessRequest; } /** @@ -93,18 +105,19 @@ public static AccessGrant of(final InputStream serialization) { static Set getSupportedTypes() { final Set types = new HashSet<>(); types.add("SolidAccessGrant"); + types.add("vc:SolidAccessGrant"); types.add("http://www.w3.org/ns/solid/vc#SolidAccessGrant"); return Collections.unmodifiableSet(types); } static AccessGrant parse(final String serialization) throws IOException { - try (final InputStream in = new ByteArrayInputStream(serialization.getBytes())) { + try (final InputStream in = new ByteArrayInputStream(serialization.getBytes(UTF_8))) { // TODO process as JSON-LD final Map data = jsonService.fromJson(in, new HashMap(){}.getClass().getGenericSuperclass()); final List> vcs = getCredentialsFromPresentation(data, supportedTypes); - if (vcs.size() != 1) { + if (vcs.size() != SINGLETON) { throw new IllegalArgumentException( "Invalid Access Grant: ambiguous number of verifiable credentials"); } @@ -125,12 +138,16 @@ static AccessGrant parse(final String serialization) throws IOException { final Optional other = asUri(consent.get("isProvidedTo")); final URI recipient = person.orElseGet(() -> controller.orElseGet(() -> other.orElse(null))); + final URI accessRequest = asUri(consent.get("verifiedRequest")).orElse( + asUri(consent.get("request")).orElse(null) + ); final Set modes = asSet(consent.get("mode")).orElseGet(Collections::emptySet); final Set resources = asSet(consent.get("forPersonalData")).orElseGet(Collections::emptySet) .stream().map(URI::create).collect(Collectors.toSet()); final Set purposes = asSet(consent.get("forPurpose")).orElseGet(Collections::emptySet) .stream().flatMap(AccessCredential::filterUris).collect(Collectors.toSet()); - final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient); + final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient, + accessRequest); return new AccessGrant(identifier, serialization, credentialData, credentialMetadata); } else { diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java index 6d74bde4b37..998dc84209b 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessGrantClient.java @@ -25,6 +25,7 @@ import com.inrupt.client.Client; import com.inrupt.client.ClientCache; import com.inrupt.client.ClientProvider; +import com.inrupt.client.Headers; import com.inrupt.client.Request; import com.inrupt.client.Response; import com.inrupt.client.auth.Session; @@ -37,6 +38,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.lang.reflect.Type; import java.net.URI; import java.time.Duration; import java.time.Instant; @@ -53,7 +55,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.stream.Collectors; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,14 +74,15 @@ URI resource = URI.create("https://storage.example/data/resource"); URI purpose = URI.create("https://purpose.example/1"); - AccessCredentialQuery query = AccessCredentialQuery.newBuilder() + CredentialFilter filter = CredentialFilter.newBuilder() + .status(CredentialStatus.ACTIVE) .resource(resource) - .mode("Read") .purpose(purpose) .build(AccessGrant.class); - client.query(query) - .thenApply(grants -> AccessGrantSession.ofAccessGrant(openid, grants.toArray(new AccessGrant[0]))) + client.query(filter) + .thenApply(results -> AccessGrantSession.ofAccessGrant(openid, + results.getItems().toArray(new AccessGrant[0]))) .thenApply(session -> SolidClient.getClient().session(session)) .thenAccept(cl -> { // Do something with the Access Grant-scoped client @@ -92,9 +95,10 @@ public class AccessGrantClient { private static final String CONTEXT = "@context"; private static final String VC_CONTEXT_URI = "https://www.w3.org/2018/credentials/v1"; - private static final String INRUPT_CONTEXT_URI = "https://schema.inrupt.com/credentials/v1.jsonld"; + private static final String INRUPT_CONTEXT_URI = "https://schema.inrupt.com/credentials/v2.jsonld"; private static final String VERIFIABLE_CREDENTIAL = "verifiableCredential"; private static final String SOLID_VC_NAMESPACE = "http://www.w3.org/ns/solid/vc#"; + private static final String SOLID_VC_QN = "vc:"; private static final String TYPE = "type"; private static final String APPLICATION_JSON = "application/json"; private static final String CONTENT_TYPE = "Content-Type"; @@ -102,7 +106,10 @@ public class AccessGrantClient { private static final String IS_PROVIDED_TO = "isProvidedTo"; private static final String IS_CONSENT_FOR_DATA_SUBJECT = "isConsentForDataSubject"; private static final String FOR_PERSONAL_DATA = "forPersonalData"; + private static final String TEMPLATE = "template"; private static final String HAS_STATUS = "hasStatus"; + private static final String REQUEST = "request"; + private static final String VERIFIED_REQUEST = "verifiedRequest"; private static final String MODE = "mode"; private static final String PROVIDED_CONSENT = "providedConsent"; private static final String FOR_PURPOSE = "forPurpose"; @@ -112,12 +119,22 @@ public class AccessGrantClient { private static final String SOLID_ACCESS_GRANT = "SolidAccessGrant"; private static final String SOLID_ACCESS_REQUEST = "SolidAccessRequest"; private static final String SOLID_ACCESS_DENIAL = "SolidAccessDenial"; + private static final String VERIFIABLE_PRESENTATION = "VerifiablePresentation"; + private static final String FIRST = "first"; + private static final String LAST = "last"; + private static final String PREV = "prev"; + private static final String NEXT = "next"; private static final URI FQ_ACCESS_GRANT = URI.create(SOLID_VC_NAMESPACE + SOLID_ACCESS_GRANT); private static final URI FQ_ACCESS_REQUEST = URI.create(SOLID_VC_NAMESPACE + SOLID_ACCESS_REQUEST); private static final URI FQ_ACCESS_DENIAL = URI.create(SOLID_VC_NAMESPACE + SOLID_ACCESS_DENIAL); + private static final URI QN_ACCESS_GRANT = URI.create(SOLID_VC_QN + SOLID_ACCESS_GRANT); + private static final URI QN_ACCESS_REQUEST = URI.create(SOLID_VC_QN + SOLID_ACCESS_REQUEST); + private static final URI QN_ACCESS_DENIAL = URI.create(SOLID_VC_QN + SOLID_ACCESS_DENIAL); private static final Set ACCESS_GRANT_TYPES = getAccessGrantTypes(); private static final Set ACCESS_REQUEST_TYPES = getAccessRequestTypes(); private static final Set ACCESS_DENIAL_TYPES = getAccessDenialTypes(); + private static final Type JSON_TYPE_REF = new HashMap(){}.getClass().getGenericSuperclass(); + private static final Set LINK_REL_VALUES = getLinkPagingRelValues(); private final Client client; private final ClientCache metadataCache; @@ -168,7 +185,10 @@ private AccessGrantClient(final Client client, final ClientCache this.config = Objects.requireNonNull(config, "config may not be null!"); this.metadataCache = Objects.requireNonNull(metadataCache, "metadataCache may not be null!"); this.jsonService = ServiceProvider.getJsonService(); - LOGGER.debug("Initializing Access Grant client with issuer: {}", config.getIssuer()); + LOGGER.atDebug() + .setMessage("Initializing Access Grant client with issuer: {}") + .addArgument(config::getIssuer) + .log(); } /** @@ -189,7 +209,7 @@ public AccessGrantClient session(final Session session) { * @return the next stage of completion containing the resulting access request */ public CompletionStage requestAccess(final AccessRequest.RequestParameters request) { - return requestAccess(request.getRecipient(), request.getResources(), + return requestAccess(request.getRecipient(), request.getResources(), request.getTemplates(), request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt()); } @@ -205,16 +225,23 @@ public CompletionStage requestAccess(final AccessRequest.RequestP */ public CompletionStage requestAccess(final URI recipient, final Set resources, final Set modes, final Set purposes, final Instant expiration) { - return requestAccess(recipient, resources, modes, purposes, expiration, null); + return requestAccess(recipient, resources, Collections.emptySet(), modes, purposes, expiration, null); } private CompletionStage requestAccess(final URI recipient, final Set resources, - final Set modes, final Set purposes, final Instant expiration, final Instant issuance) { + final Set templates, final Set modes, final Set purposes, final Instant expiration, + final Instant issuance) { Objects.requireNonNull(resources, "Resources may not be null!"); + Objects.requireNonNull(templates, "Templates may not be null!"); Objects.requireNonNull(modes, "Access modes may not be null!"); + if (templates.isEmpty() && resources.isEmpty()) { + LOGGER.warn("Both resources and templates are empty in access request"); + } else if (!templates.isEmpty() && !resources.isEmpty()) { + LOGGER.warn("Both resources and templates are non-empty in access request"); + } return v1Metadata().thenCompose(metadata -> { - final Map data = buildAccessRequestv1(recipient, resources, modes, purposes, expiration, - issuance); + final Map data = buildAccessRequestv1(recipient, resources, templates, modes, purposes, + expiration, issuance); final Request req = Request.newBuilder(metadata.issueEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) @@ -240,14 +267,79 @@ private CompletionStage requestAccess(final URI recipient, final /** * Issue an access grant based on an access request. * + *

+ * The access request is not verified. + * Any templated URLs are ignored. + * * @param request the access request * @return the next stage of completion containing the issued access grant */ public CompletionStage grantAccess(final AccessRequest request) { + return grantAccess(request, templates -> Collections.emptySet(), false); + } + + /** + * Issue an access grant based on an access request. + * + *

+ * The access request is verified. + * Any templated URLs are processed according to the provided mapping function. + * + * @param request the access request + * @param mapping a mapping function for template URLs + * @return the next stage of completion containing the issued access grant + */ + public CompletionStage grantAccess(final AccessRequest request, + final Function, Set> mapping) { + return grantAccess(request, mapping, true); + } + + /** + * Issue an access grant based on an access request. + * + *

+ * Any templated URLs are ignored. + * + * @param request the access request + * @param verifyRequest whether the request should be verified before issuing the access grant + * @return the next stage of completion containing the issued access grant + */ + public CompletionStage grantAccess(final AccessRequest request, final boolean verifyRequest) { + return grantAccess(request, templates -> Collections.emptySet(), verifyRequest); + } + + /** + * Issue an access grant based on an access request. + * + * @param request the access request + * @param mapping a mapping function for template URLs + * @param verifyRequest whether the request should be verified before issuing the access grant + * @return the next stage of completion containing the issued access grant + */ + public CompletionStage grantAccess(final AccessRequest request, + final Function, Set> mapping, final boolean verifyRequest) { Objects.requireNonNull(request, "Request may not be null!"); + final var templated = mapping.apply(request.getTemplates()); + if (templated.size() != request.getTemplates().size()) { + LOGGER.atDebug() + .setMessage("Unexpected number of mapped template values, found ({}) expected ({})") + .addArgument(templated::size) + .addArgument(() -> request.getTemplates().size()) + .log(); + } + final var resources = new HashSet(request.getResources()); + resources.addAll(templated); + if (resources.isEmpty()) { + LOGGER.atWarn() + .setMessage("No data URLs supplied: {} resource URLs and {} mapped templates") + .addArgument(() -> request.getResources().size()) + .addArgument(templated::size) + .log(); + } return v1Metadata().thenCompose(metadata -> { - final Map data = buildAccessGrantv1(request.getCreator(), request.getResources(), - request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt()); + final Map data = buildAccessGrantv1(request.getCreator(), resources, + request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(), + request.getIdentifier(), verifyRequest); final Request req = Request.newBuilder(metadata.issueEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build(); @@ -270,16 +362,28 @@ public CompletionStage grantAccess(final AccessRequest request) { } /** - * Issue an access denial receipt based on an access request. + * Issue an access denial receipt based on an access request. The access request is not verified. * * @param request the access request * @return the next stage of completion containing the issued access denial */ public CompletionStage denyAccess(final AccessRequest request) { + return denyAccess(request, false); + } + + /** + * Issue an access denial receipt based on an access request. + * + * @param request the access request + * @param verifyRequest whether the request should be verified before issuing the access denial + * @return the next stage of completion containing the issued access denial + */ + public CompletionStage denyAccess(final AccessRequest request, final boolean verifyRequest) { Objects.requireNonNull(request, "Request may not be null!"); return v1Metadata().thenCompose(metadata -> { final Map data = buildAccessDenialv1(request.getCreator(), request.getResources(), - request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt()); + request.getModes(), request.getPurposes(), request.getExpiration(), request.getIssuedAt(), + request.getIdentifier(), verifyRequest); final Request req = Request.newBuilder(metadata.issueEndpoint) .header(CONTENT_TYPE, APPLICATION_JSON) .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build(); @@ -314,8 +418,7 @@ public CompletionStage verify(final AccessCredenti final Map presentation = new HashMap<>(); try (final InputStream is = new ByteArrayInputStream(credential.serialize().getBytes(UTF_8))) { - final Map data = jsonService.fromJson(is, - new HashMap(){}.getClass().getGenericSuperclass()); + final Map data = jsonService.fromJson(is, JSON_TYPE_REF); Utils.getCredentialsFromPresentation(data, credential.getTypes()).stream().findFirst() .ifPresent(c -> presentation.put(VERIFIABLE_CREDENTIAL, c)); } catch (final IOException ex) { @@ -345,85 +448,107 @@ public CompletionStage verify(final AccessCredenti } /** - * Perform an Access Credentials query and returns 0 to N matching access credentials. + * Perform an Access Credentials query and return a page of access credentials. * - * @param the AccessCredential type - * @param resource the resource identifier, may be {@code null} - * @param creator the identifier for the agent who created the credential, may be {@code null} - * @param recipient the identifier for the agent who is the recipient for the credential, may be {@code null} - * @param purpose the access purpose, may be {@code null} - * @param mode the access mode, may be {@code null} - * @param clazz the AccessCredential type, either {@link AccessGrant} or {@link AccessRequest} - * @return the next stage of completion, including the matched Access Credentials - */ - public CompletionStage> query(final URI resource, final URI creator, - final URI recipient, final URI purpose, final String mode, final Class clazz) { - - final Set modes = mode != null ? Collections.singleton(mode) : Collections.emptySet(); - final Set purposes = purpose != null ? Collections.singleton(purpose) : Collections.emptySet(); - - return query(resource, creator, recipient, purposes, modes, clazz); - } - - /** - * Perform an Access Credentials query and returns 0 to N matching access credentials. - * - * @param the AccessCredential type - * @param query the access credential query, never {@code null} - * @return the next stage of completion, including the matched Access Credentials + * @param the credential type + * @param filter the query filter + * @return the page of query results */ - public CompletionStage> query(final AccessCredentialQuery query) { - Objects.requireNonNull(query, "The query may not be null!"); - return query(query.getResource(), query.getCreator(), query.getRecipient(), query.getPurposes(), - query.getModes(), query.getAccessCredentialType()); - } - - private CompletionStage> query(final URI resource, final URI creator, - final URI recipient, final Set purposes, final Set modes, final Class clazz) { - Objects.requireNonNull(clazz, "The clazz parameter must not be null!"); - - final URI type; + public CompletionStage> query(final CredentialFilter filter) { + final Class clazz = Objects.requireNonNull(filter, "filter may not be null!").getCredentialType(); final Set supportedTypes; if (AccessGrant.class.isAssignableFrom(clazz)) { - type = URI.create(SOLID_ACCESS_GRANT); supportedTypes = ACCESS_GRANT_TYPES; } else if (AccessRequest.class.isAssignableFrom(clazz)) { - type = URI.create(SOLID_ACCESS_REQUEST); supportedTypes = ACCESS_REQUEST_TYPES; } else if (AccessDenial.class.isAssignableFrom(clazz)) { - type = URI.create(SOLID_ACCESS_DENIAL); supportedTypes = ACCESS_DENIAL_TYPES; } else { throw new AccessGrantException("Unsupported type " + clazz + " in query request"); } return v1Metadata().thenCompose(metadata -> { - final List>> responses = buildQuery(config.getIssuer(), type, - resource, creator, recipient, purposes, modes).stream() - .map(data -> Request.newBuilder(metadata.queryEndpoint) - .header(CONTENT_TYPE, APPLICATION_JSON) - .POST(Request.BodyPublishers.ofByteArray(serialize(data))).build()) - .map(req -> client.send(req, Response.BodyHandlers.ofInputStream()) - .toCompletableFuture()) - .collect(Collectors.toList()); - - return CompletableFuture.allOf(responses.toArray(new CompletableFuture[0])) - .thenApply(x -> responses.stream().map(CompletableFuture::join).map(response -> { - try (final InputStream input = response.body()) { - final int status = response.statusCode(); - if (isSuccess(status)) { - return processQueryResponse(input, supportedTypes, clazz); - } - throw new AccessGrantException("Unable to perform Access Grant query: HTTP error " + - status, status); - } catch (final IOException ex) { - throw new AccessGrantException( - "Unexpected I/O exception while processing Access Grant query", ex); + if (metadata.queryEndpoint == null) { + throw new AccessGrantException("Server does not support CredentialFilter-based queries"); + } + final Request req = Request.newBuilder(filter.asURI(metadata.queryEndpoint)).GET().build(); + return client.send(req, Response.BodyHandlers.ofInputStream()).thenApply(response -> { + try (final InputStream input = response.body()) { + if (isSuccess(response.statusCode())) { + final Map> links = processFilterResponseHeaders(response.headers(), + filter); + final List items = processFilterResponseBody(input, supportedTypes, clazz); + return new CredentialResult<>(items, links.get(FIRST), links.get(PREV), + links.get(NEXT), links.get(LAST)); + } else { + throw new AccessGrantException("Error querying access grant: HTTP response " + + response.statusCode()); } - }).flatMap(List::stream).collect(Collectors.toList())); + } catch (final IOException ex) { + throw new AccessGrantException( + "Unexpected I/O exception while processing Access Grant query", ex); + } + }); }); } + Map> processFilterResponseHeaders(final Headers headers, + final CredentialFilter filter) { + final Map> links = new HashMap<>(); + final List linkHeaders = headers.allValues("Link"); + if (!linkHeaders.isEmpty()) { + Headers.Link.parse(linkHeaders.toArray(new String[0])) + .forEach(link -> { + final String rel = link.getParameter("rel"); + final URI uri = link.getUri(); + if (LINK_REL_VALUES.contains(rel) && uri != null) { + final String page = Utils.getQueryParam(uri, "page"); + links.put(rel, CredentialFilter.newBuilder(filter).page(page) + .build(filter.getCredentialType())); + } + }); + } + return links; + } + + List processFilterResponseBody(final InputStream input, + final Set validTypes, final Class clazz) throws IOException { + + final List page = new ArrayList<>(); + final Map data = jsonService.fromJson(input, JSON_TYPE_REF); + Utils.asList(data.get("items")).ifPresent(items -> { + for (final Object item : items) { + Utils.asMap(item).ifPresent(credential -> + Utils.asSet(credential.get(TYPE)).ifPresent(types -> { + types.retainAll(validTypes); + if (!types.isEmpty()) { + final Map presentation = new HashMap<>(); + presentation.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI)); + presentation.put(TYPE, Arrays.asList(VERIFIABLE_PRESENTATION)); + presentation.put(VERIFIABLE_CREDENTIAL, Arrays.asList(credential)); + final T c = cast(presentation, clazz); + if (c != null) { + page.add(c); + } + } + })); + } + }); + return page; + } + + @SuppressWarnings("unchecked") + T cast(final Map data, final Class clazz) { + if (AccessGrant.class.isAssignableFrom(clazz)) { + return (T) AccessGrant.of(new String(serialize(data), UTF_8)); + } else if (AccessRequest.class.isAssignableFrom(clazz)) { + return (T) AccessRequest.of(new String(serialize(data), UTF_8)); + } else if (AccessDenial.class.isAssignableFrom(clazz)) { + return (T) AccessDenial.of(new String(serialize(data), UTF_8)); + } + return null; + } + /** * Revoke an access credential. * @@ -475,11 +600,11 @@ public CompletionStage fetch(final URI identifie try (final InputStream input = res.body()) { final int httpStatus = res.statusCode(); if (isSuccess(httpStatus)) { - if (AccessGrant.class.equals(clazz)) { + if (AccessGrant.class.isAssignableFrom(clazz)) { return (T) processVerifiableCredential(input, ACCESS_GRANT_TYPES, clazz); - } else if (AccessRequest.class.equals(clazz)) { + } else if (AccessRequest.class.isAssignableFrom(clazz)) { return (T) processVerifiableCredential(input, ACCESS_REQUEST_TYPES, clazz); - } else if (AccessDenial.class.equals(clazz)) { + } else if (AccessDenial.class.isAssignableFrom(clazz)) { return (T) processVerifiableCredential(input, ACCESS_DENIAL_TYPES, clazz); } throw new AccessGrantException("Unable to fetch credential as " + clazz); @@ -493,35 +618,28 @@ public CompletionStage fetch(final URI identifie }); } - @SuppressWarnings("unchecked") T processVerifiableCredential(final InputStream input, final Set validTypes, final Class clazz) throws IOException { - final Map data = jsonService.fromJson(input, - new HashMap(){}.getClass().getGenericSuperclass()); + final Map data = jsonService.fromJson(input, JSON_TYPE_REF); final Set types = Utils.asSet(data.get(TYPE)).orElseThrow(() -> new AccessGrantException("Invalid Access Grant: no 'type' field")); types.retainAll(validTypes); if (!types.isEmpty()) { final Map presentation = new HashMap<>(); presentation.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI)); - presentation.put(TYPE, Arrays.asList("VerifiablePresentation")); + presentation.put(TYPE, Arrays.asList(VERIFIABLE_PRESENTATION)); presentation.put(VERIFIABLE_CREDENTIAL, Arrays.asList(data)); - if (AccessGrant.class.isAssignableFrom(clazz)) { - return (T) AccessGrant.of(new String(serialize(presentation), UTF_8)); - } else if (AccessRequest.class.isAssignableFrom(clazz)) { - return (T) AccessRequest.of(new String(serialize(presentation), UTF_8)); - } else if (AccessDenial.class.isAssignableFrom(clazz)) { - return (T) AccessDenial.of(new String(serialize(presentation), UTF_8)); + final T credential = cast(presentation, clazz); + if (credential != null) { + return credential; } } throw new AccessGrantException("Invalid Access Grant: missing supported type"); } - @SuppressWarnings("unchecked") List processQueryResponse(final InputStream input, final Set validTypes, final Class clazz) throws IOException { - final Map data = jsonService.fromJson(input, - new HashMap(){}.getClass().getGenericSuperclass()); + final Map data = jsonService.fromJson(input, JSON_TYPE_REF); final List grants = new ArrayList<>(); for (final Object item : getCredentials(data)) { Utils.asMap(item).ifPresent(credential -> @@ -530,14 +648,11 @@ List processQueryResponse(final InputStream inpu if (!types.isEmpty()) { final Map presentation = new HashMap<>(); presentation.put(CONTEXT, Arrays.asList(VC_CONTEXT_URI)); - presentation.put(TYPE, Arrays.asList("VerifiablePresentation")); + presentation.put(TYPE, Arrays.asList(VERIFIABLE_PRESENTATION)); presentation.put(VERIFIABLE_CREDENTIAL, Arrays.asList(credential)); - if (AccessGrant.class.equals(clazz)) { - grants.add((T) AccessGrant.of(new String(serialize(presentation), UTF_8))); - } else if (AccessRequest.class.equals(clazz)) { - grants.add((T) AccessRequest.of(new String(serialize(presentation), UTF_8))); - } else if (AccessDenial.class.equals(clazz)) { - grants.add((T) AccessDenial.of(new String(serialize(presentation), UTF_8))); + final T c = cast(presentation, clazz); + if (c != null) { + grants.add(c); } } })); @@ -559,8 +674,7 @@ CompletionStage v1Metadata() { try (final InputStream input = res.body()) { final int httpStatus = res.statusCode(); if (isSuccess(httpStatus)) { - final Map data = jsonService.fromJson(input, - new HashMap(){}.getClass().getGenericSuperclass()); + final Map data = jsonService.fromJson(input, JSON_TYPE_REF); return data; } throw new AccessGrantException( @@ -572,7 +686,7 @@ CompletionStage v1Metadata() { }) .thenApply(metadata -> { final Metadata m = new Metadata(); - m.queryEndpoint = asUri(metadata.get("derivationService")); + m.queryEndpoint = asUri(metadata.get("queryService")); m.issueEndpoint = asUri(metadata.get("issuerService")); m.verifyEndpoint = asUri(metadata.get("verifierService")); m.statusEndpoint = asUri(metadata.get("statusService")); @@ -690,14 +804,22 @@ static URI asUri(final Object value) { return null; } - static Map buildAccessDenialv1(final URI agent, final Set resources, final Set modes, - final Set purposes, final Instant expiration, final Instant issuance) { + static Map buildAccessDenialv1( + final URI agent, + final Set resources, + final Set modes, + final Set purposes, + final Instant expiration, + final Instant issuance, + final URI accessRequest, + final boolean verifiedRequest) { Objects.requireNonNull(agent, "Access denial agent may not be null!"); final Map consent = new HashMap<>(); consent.put(MODE, modes); consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusRefused"); consent.put(FOR_PERSONAL_DATA, resources); consent.put(IS_PROVIDED_TO, agent); + linkRequest(consent, accessRequest, verifiedRequest); if (!purposes.isEmpty()) { consent.put(FOR_PURPOSE, purposes); } @@ -720,14 +842,22 @@ static Map buildAccessDenialv1(final URI agent, final Set r return data; } - static Map buildAccessGrantv1(final URI agent, final Set resources, final Set modes, - final Set purposes, final Instant expiration, final Instant issuance) { + static Map buildAccessGrantv1( + final URI agent, + final Set resources, + final Set modes, + final Set purposes, + final Instant expiration, + final Instant issuance, + final URI accessRequest, + final boolean verifiedRequest) { Objects.requireNonNull(agent, "Access grant agent may not be null!"); final Map consent = new HashMap<>(); consent.put(MODE, modes); consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusExplicitlyGiven"); consent.put(FOR_PERSONAL_DATA, resources); consent.put(IS_PROVIDED_TO, agent); + linkRequest(consent, accessRequest, verifiedRequest); if (!purposes.isEmpty()) { consent.put(FOR_PURPOSE, purposes); } @@ -750,12 +880,17 @@ static Map buildAccessGrantv1(final URI agent, final Set re return data; } - static Map buildAccessRequestv1(final URI agent, final Set resources, final Set modes, - final Set purposes, final Instant expiration, final Instant issuance) { + static Map buildAccessRequestv1(final URI agent, final Set resources, + final Set templates, final Set modes, final Set purposes, + final Instant expiration, final Instant issuance) { final Map consent = new HashMap<>(); consent.put(HAS_STATUS, "https://w3id.org/GConsent#ConsentStatusRequested"); consent.put(MODE, modes); - consent.put(FOR_PERSONAL_DATA, resources); + if (!resources.isEmpty()) { + consent.put(FOR_PERSONAL_DATA, resources); + } else { + consent.put(TEMPLATE, templates); + } if (agent != null) { consent.put(IS_CONSENT_FOR_DATA_SUBJECT, agent); } @@ -789,6 +924,7 @@ static boolean isSuccess(final int statusCode) { static Set getAccessRequestTypes() { final Set types = new HashSet<>(); types.add(SOLID_ACCESS_REQUEST); + types.add(QN_ACCESS_REQUEST.toString()); types.add(FQ_ACCESS_REQUEST.toString()); return Collections.unmodifiableSet(types); } @@ -796,6 +932,7 @@ static Set getAccessRequestTypes() { static Set getAccessGrantTypes() { final Set types = new HashSet<>(); types.add(SOLID_ACCESS_GRANT); + types.add(QN_ACCESS_GRANT.toString()); types.add(FQ_ACCESS_GRANT.toString()); return Collections.unmodifiableSet(types); } @@ -803,19 +940,40 @@ static Set getAccessGrantTypes() { static Set getAccessDenialTypes() { final Set types = new HashSet<>(); types.add(SOLID_ACCESS_DENIAL); + types.add(QN_ACCESS_DENIAL.toString()); types.add(FQ_ACCESS_DENIAL.toString()); return Collections.unmodifiableSet(types); } + static Set getLinkPagingRelValues() { + final Set values = new HashSet<>(); + values.add(FIRST); + values.add(LAST); + values.add(NEXT); + values.add(PREV); + return Collections.unmodifiableSet(values); + } + static boolean isAccessGrant(final URI type) { - return SOLID_ACCESS_GRANT.equals(type.toString()) || FQ_ACCESS_GRANT.equals(type); + return SOLID_ACCESS_GRANT.equals(type.toString()) || QN_ACCESS_GRANT.equals(type) + || FQ_ACCESS_GRANT.equals(type); } static boolean isAccessRequest(final URI type) { - return SOLID_ACCESS_REQUEST.equals(type.toString()) || FQ_ACCESS_REQUEST.equals(type); + return SOLID_ACCESS_REQUEST.equals(type.toString()) || QN_ACCESS_REQUEST.equals(type) + || FQ_ACCESS_REQUEST.equals(type); } static boolean isAccessDenial(final URI type) { - return SOLID_ACCESS_DENIAL.equals(type.toString()) || FQ_ACCESS_DENIAL.equals(type); + return SOLID_ACCESS_DENIAL.equals(type.toString()) || QN_ACCESS_DENIAL.equals(type) + || FQ_ACCESS_DENIAL.equals(type); + } + + private static void linkRequest(final Map consent, final URI request, final boolean verifiedLink) { + if (verifiedLink) { + consent.put(VERIFIED_REQUEST, request); + } else { + consent.put(REQUEST, request); + } } } diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java index 75c4f090b69..bb3db7dcd79 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/AccessRequest.java @@ -37,6 +37,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -94,18 +95,19 @@ public static AccessRequest of(final InputStream serialization) { static Set getSupportedTypes() { final Set types = new HashSet<>(); types.add("SolidAccessRequest"); + types.add("vc:SolidAccessRequest"); types.add("http://www.w3.org/ns/solid/vc#SolidAccessRequest"); return Collections.unmodifiableSet(types); } static AccessRequest parse(final String serialization) throws IOException { - try (final InputStream in = new ByteArrayInputStream(serialization.getBytes())) { + try (final InputStream in = new ByteArrayInputStream(serialization.getBytes(UTF_8))) { // TODO process as JSON-LD final Map data = jsonService.fromJson(in, new HashMap(){}.getClass().getGenericSuperclass()); final List> vcs = getCredentialsFromPresentation(data, supportedTypes); - if (vcs.size() != 1) { + if (vcs.size() != SINGLETON) { throw new IllegalArgumentException( "Invalid Access Request: ambiguous number of verifiable credentials"); } @@ -127,7 +129,15 @@ static AccessRequest parse(final String serialization) throws IOException { .stream().map(URI::create).collect(Collectors.toSet()); final Set purposes = asSet(consent.get("forPurpose")).orElseGet(Collections::emptySet) .stream().flatMap(AccessCredential::filterUris).collect(Collectors.toSet()); - final CredentialData credentialData = new CredentialData(resources, modes, purposes, recipient); + final Set templates = asSet(consent.get("template")).orElseGet(Collections::emptySet); + + final CredentialData credentialData; + + if (!templates.isEmpty()) { + credentialData = new CredentialData(resources, templates, modes, purposes, recipient, null); + } else { + credentialData = new CredentialData(resources, modes, purposes, recipient); + } return new AccessRequest(identifier, serialization, credentialData, credentialMetadata); } else { @@ -136,6 +146,33 @@ static AccessRequest parse(final String serialization) throws IOException { } } + /** + * Produce a URI template that can be used consistently with the JCL. + * + * @param dataPath the data path relative to a storage root + * @return a URI template + */ + public static String template(final String dataPath) { + Objects.requireNonNull(dataPath, "dataPath may not be null!"); + if (!dataPath.startsWith("/") || dataPath.isBlank()) { + return template("/" + dataPath); + } + return "https://{domain}/{+path}" + dataPath; + } + + /** + * Produce a Map of template values that can be used with a URI template resolver. + * + * @param domain the domain of the user's storage + * @param path the base path of the user's storage + * @return a map of template values + */ + public static Map templateValues(final String domain, final String path) { + Objects.requireNonNull(domain, "domain may not be null!"); + Objects.requireNonNull(path, "path may not be null!"); + return Map.of("domain", domain, "path", path); + } + /** * A collection of parameters used for creating access requests. * @@ -144,6 +181,7 @@ static AccessRequest parse(final String serialization) throws IOException { public static class RequestParameters { private final URI recipient; + private final Set templates; private final Set resources; private final Set modes; private final Set purposes; @@ -151,10 +189,11 @@ public static class RequestParameters { private final Instant issuedAt; /* package private */ - RequestParameters(final URI recipient, final Set resources, + RequestParameters(final URI recipient, final Set resources, final Set templates, final Set modes, final Set purposes, final Instant expiration, final Instant issuedAt) { this.recipient = recipient; this.resources = resources; + this.templates = templates; this.modes = modes; this.purposes = purposes; this.expiration = expiration; @@ -175,12 +214,21 @@ public URI getRecipient() { /** * Get the resources used with an access request operation. * - * @return the resource idnetifiers + * @return the resource identifiers */ public Set getResources() { return resources; } + /** + * Get the resource templates used with an access request operation. + * + * @return the URI templates for an access request + */ + public Set getTemplates() { + return templates; + } + /** * Get the access modes used with an access request operation. * @@ -238,6 +286,7 @@ public static class Builder { private final Set builderResources = new HashSet<>(); private final Set builderModes = new HashSet<>(); private final Set builderPurposes = new HashSet<>(); + private final Set builderTemplates = new HashSet<>(); private URI builderRecipient; private Instant builderExpiration; private Instant builderIssuedAt; @@ -288,6 +337,34 @@ public Builder resources(final Collection resources) { return this; } + /** + * Set a single resource template for the access request operation. + * + * @param template a URL template for the requested resource, not {@code null} + * @return this builder + */ + public Builder template(final String template) { + builderTemplates.add(template); + return this; + } + + /** + * Set multiple resource templates for the access request operation. + * + *

Note: A null value will clear all existing template values + * + * @param templates URL templates for the requested resources, may be {@code null} + * @return this builder + */ + public Builder templates(final Collection templates) { + if (templates != null) { + builderTemplates.addAll(templates); + } else { + builderTemplates.clear(); + } + return this; + } + /** * Set a single access mode for the access request operation. * @@ -376,8 +453,8 @@ public Builder issuedAt(final Instant issuedAt) { * @return the access request parameters */ public RequestParameters build() { - return new RequestParameters(builderRecipient, builderResources, builderModes, builderPurposes, - builderExpiration, builderIssuedAt); + return new RequestParameters(builderRecipient, builderResources, builderTemplates, builderModes, + builderPurposes, builderExpiration, builderIssuedAt); } } } diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/CredentialFilter.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/CredentialFilter.java new file mode 100644 index 00000000000..3b5868e6e94 --- /dev/null +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/CredentialFilter.java @@ -0,0 +1,420 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.accessgrant; + +import com.inrupt.client.util.URIBuilder; + +import java.net.URI; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * A class for representing Access Grant query filters. + * + * @param the credential type + */ +public class CredentialFilter { + + /** The default size of result pages. */ + public static final int DEFAULT_PAGE_SIZE = 20; + /** The maximum result page size. */ + public static final int MAX_PAGE_SIZE = 100; + + private static final Map TYPE_VALUES = buildTypeValues(); + + private final URI fromAgent; + private final URI toAgent; + private final CredentialStatus status; + private final URI resource; + private final URI purpose; + private final CredentialDuration revokedWithin; + private final CredentialDuration issuedWithin; + private final String page; + private final int pageSize; + private final Class clazz; + + CredentialFilter(final URI fromAgent, final URI toAgent, final CredentialStatus status, final URI resource, + final URI purpose, final CredentialDuration issuedWithin, final CredentialDuration revokedWithin, + final int pageSize, final String page, final Class clazz) { + + this.clazz = Objects.requireNonNull(clazz, "The clazz parameter must not be null!"); + this.fromAgent = fromAgent; + this.toAgent = toAgent; + this.status = status; + this.resource = resource; + this.purpose = purpose; + this.revokedWithin = revokedWithin; + this.issuedWithin = issuedWithin; + this.page = page; + this.pageSize = pageSize; + } + + /** + * A filter for an agent that issued credentials. + * + * @return the agent issuer, if present + */ + public Optional getFromAgent() { + return Optional.ofNullable(fromAgent); + } + + /** + * A filter for an agent that is the target of credentials. + * + * @return the target agent, if present + */ + public Optional getToAgent() { + return Optional.ofNullable(toAgent); + } + + /** + * A filter for credential status. + * + * @return the credential status, if present + */ + public Optional getStatus() { + return Optional.ofNullable(status); + } + + /** + * A filter for a target resource. + * + * @return the credential target resource, if present + */ + public Optional getResource() { + return Optional.ofNullable(resource); + } + + /** + * A filter for a target purpose. + * + * @return the credential target purpose, if present + */ + public Optional getPurpose() { + return Optional.ofNullable(purpose); + } + + /** + * A filter for credential issuance. + * + * @return the credential issuance filter, if present + */ + public Optional getIssuedWithin() { + return Optional.ofNullable(issuedWithin); + } + + /** + * A filter for credential revocation. + * + * @return the credential revocation filter, if present + */ + public Optional getRevokedWithin() { + return Optional.ofNullable(revokedWithin); + } + + /** + * A filter for a result page. + * + * @return the page indicator, if present + */ + public Optional getPage() { + return Optional.ofNullable(page); + } + + /** + * The page size for result sets. + * + * @return the result set size + */ + public int getPageSize() { + return pageSize; + } + + /** + * Convert the filter to a URI. + * + * @param baseUrl the base URL for the filter + * @return the formatted URI + */ + public URI asURI(final URI baseUrl) { + final URIBuilder builder = URIBuilder.newBuilder(baseUrl) + .queryParam("type", TYPE_VALUES.get(getCredentialType().getSimpleName())) + .queryParam("pageSize", Integer.toString(getPageSize())); + + getPurpose().map(URI::toString).ifPresent(p -> builder.queryParam("purpose", p)); + getResource().map(URI::toString).ifPresent(r -> builder.queryParam("resource", r)); + getFromAgent().map(URI::toString).ifPresent(a -> builder.queryParam("fromAgent", a)); + getToAgent().map(URI::toString).ifPresent(a -> builder.queryParam("toAgent", a)); + + getStatus().map(CredentialStatus::getValue).ifPresent(s -> builder.queryParam("status", s)); + getIssuedWithin().map(CredentialDuration::name).ifPresent(d -> builder.queryParam("issuedWithin", d)); + getRevokedWithin().map(CredentialDuration::name).ifPresent(d -> builder.queryParam("revokedWithin", d)); + + getPage().ifPresent(p -> builder.queryParam("page", p)); + + return builder.build(); + } + + /* package private */ + Class getCredentialType() { + return clazz; + } + + /** + * Create a new CredentialFilter builder. + * + * @return the builder + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Create a new CredentialFilter builder from an existing filter. + * + * @param the credential type + * @param filter an existing credential filter + * @return the builder + */ + public static Builder newBuilder(final CredentialFilter filter) { + final Builder builder = new Builder().pageSize(filter.getPageSize()); + filter.getPurpose().ifPresent(builder::purpose); + filter.getResource().ifPresent(builder::resource); + filter.getFromAgent().ifPresent(builder::fromAgent); + filter.getToAgent().ifPresent(builder::toAgent); + filter.getStatus().ifPresent(builder::status); + filter.getIssuedWithin().ifPresent(builder::issuedWithin); + filter.getRevokedWithin().ifPresent(builder::revokedWithin); + filter.getPage().ifPresent(builder::page); + return builder; + } + + /** + * The CredentialFilter builder. + */ + public static class Builder { + private URI builderPurpose; + private URI builderResource; + private URI builderFromAgent; + private URI builderToAgent; + private CredentialStatus builderStatus; + private CredentialDuration builderIssuedWithin; + private CredentialDuration builderRevokedWithin; + private String builderPage; + private int builderPageSize = DEFAULT_PAGE_SIZE; + + /* Package private */ + Builder() { + } + + /** + * Add a purpose filter. + * + * @param purpose the purpose identifier + * @return this builder + */ + public Builder purpose(final URI purpose) { + builderPurpose = purpose; + return this; + } + + /** + * Add a resource filter. + * + * @param resource the resource identifier + * @return this builder + */ + public Builder resource(final URI resource) { + builderResource = resource; + return this; + } + + /** + * Add a fromAgent filter. + * + * @param fromAgent the agent identifier + * @return this builder + */ + public Builder fromAgent(final URI fromAgent) { + builderFromAgent = fromAgent; + return this; + } + + /** + * Add a toAgent filter. + * + * @param toAgent the agent identifier + * @return this builder + */ + public Builder toAgent(final URI toAgent) { + builderToAgent = toAgent; + return this; + } + + /** + * Add a status filter. + * + * @param status the status value + * @return this builder + */ + public Builder status(final CredentialStatus status) { + builderStatus = status; + return this; + } + + /** + * Add an issuance date filter. + * + * @param issuedWithin the issuance date filter + * @return this builder + */ + public Builder issuedWithin(final CredentialDuration issuedWithin) { + builderIssuedWithin = issuedWithin; + return this; + } + + /** + * Add a revocation date filter. + * + * @param revokedWithin the revocation date filter + * @return this builder + */ + public Builder revokedWithin(final CredentialDuration revokedWithin) { + builderRevokedWithin = revokedWithin; + return this; + } + + /** + * Add a page indicator. + * + * @param page the page indicator + * @return this builder + */ + public Builder page(final String page) { + builderPage = page; + return this; + } + + /** + * Indicate the size of a page. + * + * @param pageSize the size of a page + * @return this builder + */ + public Builder pageSize(final int pageSize) { + if (pageSize > 0 && pageSize < MAX_PAGE_SIZE) { + builderPageSize = pageSize; + } else { + builderPageSize = DEFAULT_PAGE_SIZE; + } + return this; + } + + /** + * Build a credential filter. + * + * @param the credential type + * @param clazz the credential type + * @return the credential filter + */ + public CredentialFilter build(final Class clazz) { + return new CredentialFilter<>(builderFromAgent, builderToAgent, builderStatus, builderResource, + builderPurpose, builderIssuedWithin, builderRevokedWithin, builderPageSize, builderPage, clazz); + } + } + + /** + * The duration values for filtering access credentials. + */ + public enum CredentialDuration { + + /** 1 Day. */ + P1D(Duration.ofDays(1)), + /** 1 Week. */ + P7D(Duration.ofDays(7)), + /** 1 Month. */ + P1M(Duration.ofDays(30)), + /** 3 Months. */ + P3M(Duration.ofDays(90)); + + private final Duration value; + + CredentialDuration(final Duration value) { + this.value = value; + } + + /** + * Get the value of the duration. + * + * @return the duration filter value + */ + public Duration asDuration() { + return this.value; + } + } + + /** + * The status values for filtering access credentials. + */ + public enum CredentialStatus { + + /** Pending credentials. */ + PENDING("Pending"), + /** Deined credentials. */ + DENIED("Denied"), + /** Granted credentials. */ + GRANTED("Granted"), + /** Canceled credentials. */ + CANCELED("Canceled"), + /** Expired credentials. */ + EXPIRED("Expired"), + /** Active credentials. */ + ACTIVE("Active"), + /** Revoked credentials. */ + REVOKED("Revoked"); + + private final String value; + + CredentialStatus(final String value) { + this.value = value; + } + + /** + * Get the value of status filter. + * + * @return the value + */ + public String getValue() { + return value; + } + } + + private static Map buildTypeValues() { + final Map values = new HashMap<>(); + values.put("AccessGrant", "SolidAccessGrant"); + values.put("AccessDenial", "SolidAccessDenial"); + values.put("AccessRequest", "SolidAccessRequest"); + return values; + } +} diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/CredentialResult.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/CredentialResult.java new file mode 100644 index 00000000000..11782b73fb0 --- /dev/null +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/CredentialResult.java @@ -0,0 +1,101 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.accessgrant; + +import java.util.List; +import java.util.Optional; + +/** + * A page-able result from a credential query. + * + * @param the access credential type + */ +public class CredentialResult { + + private final List items; + private final CredentialFilter first; + private final CredentialFilter last; + private final CredentialFilter prev; + private final CredentialFilter next; + + /** + * A page of access credential results. + * + * @param items the result items + * @param first a filter for the first page of results, may be {@code null} + * @param prev a filter for the previous page of results, may be {@code null} + * @param next a filter for the next page of results, may be {@code null} + * @param last a filter for the last page of results, may be {@code null} + */ + public CredentialResult(final List items, final CredentialFilter first, final CredentialFilter prev, + final CredentialFilter next, final CredentialFilter last) { + this.items = items; + this.first = first; + this.prev = prev; + this.next = next; + this.last = last; + } + + /** + * Get a filter for the first page of results. + * + * @return the first page filter, if present + */ + public Optional> firstPage() { + return Optional.ofNullable(first); + } + + /** + * Get a filter for the previous page of results. + * + * @return the previous page filter, if present + */ + public Optional> prevPage() { + return Optional.ofNullable(prev); + } + + /** + * Get a filter for the next page of results. + * + * @return the next page filter, if present + */ + public Optional> nextPage() { + return Optional.ofNullable(next); + } + + /** + * Get a filter for the last page of results. + * + * @return the last page filter, if present + */ + public Optional> lastPage() { + return Optional.ofNullable(last); + } + + /** + * Get the credential items in the result page. + * + * @return the credential items + */ + public List getItems() { + return items; + } +} diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/Metadata.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/Metadata.java index 913147d98b6..185772e63f4 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/Metadata.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/Metadata.java @@ -23,10 +23,8 @@ import java.net.URI; class Metadata { - public URI queryEndpoint; public URI issueEndpoint; public URI statusEndpoint; public URI verifyEndpoint; - } diff --git a/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java b/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java index 9bbd9936753..7b8c47146a2 100644 --- a/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java +++ b/access-grant/src/main/java/com/inrupt/client/accessgrant/Utils.java @@ -28,6 +28,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -84,6 +85,15 @@ public static Optional> asMap(final Object value) { return Optional.empty(); } + public static Optional> asList(final Object value) { + if (value instanceof List) { + @SuppressWarnings("unchecked") + final List v = (List) value; + return Optional.of(v); + } + return Optional.empty(); + } + public static Optional> asSet(final Object value) { if (value != null) { final Set data = new HashSet<>(); @@ -119,6 +129,21 @@ public static List> getCredentialsFromPresentation(final Map return credentials; } + public static String getQueryParam(final URI uri, final String name) { + Objects.requireNonNull(uri, "uri may not be null!"); + Objects.requireNonNull(name, "name may not be null!"); + final String params = uri.getQuery(); + if (params != null) { + for (final String param : params.split("&")) { + final String[] parts = param.split("=", 2); + if (parts.length == 2 && name.equals(parts[0])) { + return parts[1]; + } + } + } + return null; + } + private Utils() { // prevent instantiation } diff --git a/access-grant/src/site/site.xml b/access-grant/src/site/site.xml index cd7c727aa68..4e08bdd2434 100644 --- a/access-grant/src/site/site.xml +++ b/access-grant/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessDenialTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessDenialTest.java new file mode 100644 index 00000000000..ec2c85a8e27 --- /dev/null +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessDenialTest.java @@ -0,0 +1,106 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.accessgrant; + +import static org.junit.jupiter.api.Assertions.*; + +import com.inrupt.client.spi.JsonService; +import com.inrupt.client.spi.ServiceProvider; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.time.Instant; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +class AccessDenialTest { + + private static final JsonService jsonService = ServiceProvider.getJsonService(); + + @Test + void testReadAccessDenial() throws IOException { + try (final InputStream resource = AccessDenialTest.class.getResourceAsStream("/access_denial1.json")) { + final AccessDenial denial = AccessDenial.of(resource); + assertEquals(Collections.singleton("Read"), denial.getModes()); + assertEquals(URI.create("https://accessgrant.test"), denial.getIssuer()); + final Set expectedTypes = new HashSet<>(); + expectedTypes.add("VerifiableCredential"); + expectedTypes.add("SolidAccessDenial"); + assertEquals(expectedTypes, denial.getTypes()); + assertEquals(URI.create("https://accessrequest.test/5678"), denial.getAccessRequest()); + assertEquals(Instant.parse("2022-08-27T12:00:00Z"), denial.getExpiration()); + assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), denial.getIssuedAt()); + assertEquals(URI.create("https://accessgrant.test/credential/fc2dbcd9-81d4-4fa4-8fd4-239e16dd83ab"), + denial.getIdentifier()); + assertEquals(Collections.singleton(URI.create("https://purpose.test/Purpose1")), denial.getPurposes()); + assertEquals(Collections.singleton( + URI.create("https://storage.test/e973cc3d-5c28-4a10-98c5-e40079289358/")), + denial.getResources()); + assertEquals(URI.create("https://id.test/grantor"), denial.getCreator()); + assertEquals(Optional.of(URI.create("https://id.test/grantee")), denial.getRecipient()); + final Optional status = denial.getStatus(); + assertTrue(status.isPresent()); + status.ifPresent(s -> { + assertEquals(URI.create("https://accessgrant.test/status/CVAM#2832"), s.getIdentifier()); + assertEquals(URI.create("https://accessgrant.test/status/CVAM"), s.getCredential()); + assertEquals(2832, s.getIndex()); + assertEquals("RevocationList2020Status", s.getType()); + }); + } + } + + @Test + void testReadAccessDenialQualifiedName() throws IOException { + try (final InputStream resource = AccessDenialTest.class.getResourceAsStream("/access_denial2.json")) { + final AccessDenial denial = AccessDenial.of(resource); + assertEquals(Collections.singleton("Read"), denial.getModes()); + assertEquals(URI.create("https://accessgrant.test"), denial.getIssuer()); + final Set expectedTypes = new HashSet<>(); + expectedTypes.add("VerifiableCredential"); + expectedTypes.add("vc:SolidAccessDenial"); + assertNull(denial.getAccessRequest()); + assertEquals(expectedTypes, denial.getTypes()); + assertEquals(Instant.parse("2022-08-27T12:00:00Z"), denial.getExpiration()); + assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), denial.getIssuedAt()); + assertEquals(URI.create("https://accessgrant.test/credential/39a4fdd4-44b0-48a5-a9b5-7a9b648e9a67"), + denial.getIdentifier()); + assertEquals(Collections.singleton(URI.create("https://purpose.test/Purpose1")), denial.getPurposes()); + assertEquals(Collections.singleton( + URI.create("https://storage.test/d5ef4173-4f12-40b2-9a0e-18fa7cc0dd38/")), + denial.getResources()); + assertEquals(URI.create("https://id.test/grantor"), denial.getCreator()); + assertEquals(Optional.of(URI.create("https://id.test/grantee")), denial.getRecipient()); + final Optional status = denial.getStatus(); + assertTrue(status.isPresent()); + status.ifPresent(s -> { + assertEquals(URI.create("https://accessgrant.test/status/CVAM#2832"), s.getIdentifier()); + assertEquals(URI.create("https://accessgrant.test/status/CVAM"), s.getCredential()); + assertEquals(2832, s.getIndex()); + assertEquals("RevocationList2020Status", s.getType()); + }); + } + } +} diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java index c4e8a8c86e5..4192c3e6a02 100644 --- a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantClientTest.java @@ -33,10 +33,8 @@ import java.io.UncheckedIOException; import java.net.URI; import java.time.Instant; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -44,6 +42,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.stream.Collectors; import org.apache.commons.io.IOUtils; import org.jose4j.jwk.PublicJsonWebKey; @@ -55,6 +54,8 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class AccessGrantClientTest { @@ -174,7 +175,6 @@ void testFetch5() { assertInstanceOf(AccessGrantException.class, err1.getCause()); } - @Test void testFetch6() { final Map claims = new HashMap<>(); @@ -215,6 +215,27 @@ void testNotAccessGrant() { client.fetch(uri, AccessGrant.class).toCompletableFuture()::join); } + @Test + void testFetchTemplate() { + final Map claims = new HashMap<>(); + claims.put("webid", WEBID); + claims.put("sub", SUB); + claims.put("iss", ISS); + claims.put("azp", AZP); + final String token = generateIdToken(claims); + final URI uri = URIBuilder.newBuilder(baseUri).path("access-request-6").build(); + final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + final AccessRequest request = client.fetch(uri, AccessRequest.class).toCompletableFuture().join(); + + assertEquals(uri, request.getIdentifier()); + assertEquals(baseUri, request.getIssuer()); + + // Revoke + final CompletableFuture future = client.revoke(request).toCompletableFuture(); + final CompletionException err1 = assertThrows(CompletionException.class, future::join); + assertInstanceOf(AccessGrantException.class, err1.getCause()); + } + @Test void testFetchInvalid() { final URI uri = URIBuilder.newBuilder(baseUri).path(".well-known/vc-configuration").build(); @@ -236,7 +257,7 @@ void testIssueRequest() { final URI recipient = URI.create("https://id.test/agent"); final Instant expiration = Instant.parse("2022-08-27T12:00:00Z"); - final Set modes = new HashSet<>(Arrays.asList("Read", "Append")); + final Set modes = Set.of("Read", "Append"); final Set purposes = Collections.singleton(URI.create("https://purpose.test/Purpose1")); final Set resources = Collections.singleton(URI.create("https://storage.test/data/")); @@ -264,7 +285,7 @@ void testIssueRequestBuilder() { final URI recipient = URI.create("https://id.test/agent"); final Instant expiration = Instant.parse("2022-08-27T12:00:00Z"); - final Set modes = new HashSet<>(Arrays.asList("Read", "Append")); + final Set modes = Set.of("Read", "Append"); final Set purposes = Collections.singleton(URI.create("https://purpose.test/Purpose1")); final Set resources = Collections.singleton(URI.create("https://storage.test/data/")); @@ -286,11 +307,39 @@ void testIssueRequestBuilder() { assertEquals(resources, request.getResources()); } + @Test + void testGrantFromTemplate() { + final Map claims = new HashMap<>(); + claims.put("webid", WEBID); + claims.put("sub", SUB); + claims.put("iss", ISS); + claims.put("azp", AZP); + final String token = generateIdToken(claims); + final URI uri = URIBuilder.newBuilder(baseUri).path("access-request-6").build(); + final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + final AccessRequest request = client.fetch(uri, AccessRequest.class).toCompletableFuture().join(); + + final AccessGrant grant = client.grantAccess(request, + templates -> Set.of(URI.create("https://storage.test/data/"))).toCompletableFuture().join(); + + final Instant expiration = Instant.parse("2022-08-27T12:00:00Z"); + final Set modes = Set.of("Read", "Append"); + final Set purposes = Collections.singleton(URI.create("https://purpose.test/Purpose1")); + + assertTrue(grant.getTypes().contains("SolidAccessGrant")); + assertEquals(Optional.of(URI.create("https://id.test/agent")), grant.getRecipient()); + assertEquals(modes, grant.getModes()); + assertEquals(expiration, grant.getExpiration()); + assertEquals(baseUri, grant.getIssuer()); + assertEquals(purposes, grant.getPurposes()); + assertNotNull(grant.getAccessRequest()); + } + @Test void testRequestAccessNoAuth() { final URI recipient = URI.create("https://id.test/agent"); final Instant expiration = Instant.parse("2022-08-27T12:00:00Z"); - final Set modes = new HashSet<>(Arrays.asList("Read", "Append")); + final Set modes = Set.of("Read", "Append"); final Set purposes = Collections.singleton(URI.create("https://purpose.test/Purpose1")); final Set resources = Collections.singleton(URI.create("https://storage.test/data/")); @@ -301,8 +350,9 @@ void testRequestAccessNoAuth() { assertInstanceOf(AccessGrantException.class, err.getCause()); } - @Test - void testGrantAccess() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testGrantAccess(final boolean verifyRequest) { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -313,14 +363,14 @@ void testGrantAccess() { final URI recipient = URI.create("https://id.test/agent"); final Instant expiration = Instant.parse("2022-08-27T12:00:00Z"); - final Set modes = new HashSet<>(Arrays.asList("Read", "Append")); + final Set modes = Set.of("Read", "Append"); final Set purposes = Collections.singleton(URI.create("https://purpose.test/Purpose1")); final Set resources = Collections.singleton(URI.create("https://storage.test/data/")); final AccessRequest request = client.requestAccess(recipient, resources, modes, purposes, expiration) .toCompletableFuture().join(); - final AccessGrant grant = client.grantAccess(request).toCompletableFuture().join(); + final AccessGrant grant = client.grantAccess(request, verifyRequest).toCompletableFuture().join(); assertTrue(grant.getTypes().contains("SolidAccessGrant")); assertEquals(Optional.of(recipient), grant.getRecipient()); @@ -329,10 +379,61 @@ void testGrantAccess() { assertEquals(baseUri, grant.getIssuer()); assertEquals(purposes, grant.getPurposes()); assertEquals(resources, grant.getResources()); + // The request URL is static in the mock response, but it is dynamic in the request, so they will mismatch + // if compared. + assertNotNull(grant.getAccessRequest()); } - @Test - void testDenyAccess() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testGrantTemplatedAccess(final boolean verifyRequest) { + final Map claims = new HashMap<>(); + claims.put("webid", WEBID); + claims.put("sub", SUB); + claims.put("iss", ISS); + claims.put("azp", AZP); + final String token = generateIdToken(claims); + final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + + final URI recipient = URI.create("https://id.test/agent"); + final Instant expiration = Instant.parse("2022-08-27T12:00:00Z"); + final Set modes = Set.of("Read", "Append"); + final Set purposes = Collections.singleton(URI.create("https://purpose.test/Purpose1")); + final Set resources = Collections.singleton(URI.create("https://storage.test/data/")); + + final var req = AccessRequest.RequestParameters.newBuilder() + .recipient(recipient) + .templates(Collections.singleton("http://{storage}/path")) + .templates(null) + .template(AccessRequest.template("data")) + .modes(modes) + .purposes(purposes) + .expiration(expiration) + .build(); + + final AccessRequest request = client.requestAccess(req) + .toCompletableFuture().join(); + + final AccessGrant grant = client.grantAccess(request, templates -> + templates.stream() + .map(t -> t.replace("{domain}", "storage.test").replace("{+path}/", "")) + .map(URI::create) + .collect(Collectors.toSet()), verifyRequest) + .toCompletableFuture().join(); + + assertTrue(grant.getTypes().contains("SolidAccessGrant")); + assertEquals(Optional.of(recipient), grant.getRecipient()); + assertEquals(modes, grant.getModes()); + assertEquals(expiration, grant.getExpiration()); + assertEquals(baseUri, grant.getIssuer()); + assertEquals(purposes, grant.getPurposes()); + assertEquals(resources, grant.getResources()); + assertNotNull(grant.getAccessRequest()); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testDenyAccess(final boolean verifyRequest) { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -343,14 +444,14 @@ void testDenyAccess() { final URI recipient = URI.create("https://id.test/agent"); final Instant expiration = Instant.parse("2022-09-12T12:00:00Z"); - final Set modes = new HashSet<>(Arrays.asList("Read", "Append")); + final Set modes = Set.of("Read", "Append"); final Set purposes = Collections.singleton(URI.create("https://purpose.test/Purpose1")); final Set resources = Collections.singleton(URI.create("https://storage.test/data/")); final AccessRequest request = client.requestAccess(recipient, resources, modes, purposes, expiration) .toCompletableFuture().join(); - final AccessDenial denial = client.denyAccess(request).toCompletableFuture().join(); + final AccessDenial denial = client.denyAccess(request, verifyRequest).toCompletableFuture().join(); assertTrue(denial.getTypes().contains("SolidAccessDenial")); assertEquals(Optional.of(recipient), denial.getRecipient()); @@ -367,7 +468,7 @@ void testDenyAccess() { } @Test - void testGrantAccessNoAuth() { + void testNoTemplatesResources() { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -378,20 +479,21 @@ void testGrantAccessNoAuth() { final URI recipient = URI.create("https://id.test/agent"); final Instant expiration = Instant.parse("2022-08-27T12:00:00Z"); - final Set modes = new HashSet<>(Arrays.asList("Read", "Append")); + final Set modes = Set.of("Read", "Append"); final Set purposes = Collections.singleton(URI.create("https://purpose.test/Purpose1")); - final Set resources = Collections.singleton(URI.create("https://storage.test/data/")); - final AccessRequest request = client.requestAccess(recipient, resources, modes, purposes, expiration) - .toCompletableFuture().join(); + final var req = AccessRequest.RequestParameters.newBuilder() + .recipient(recipient) + .modes(modes) + .purposes(purposes) + .expiration(expiration) + .build(); - final CompletableFuture future = agClient.grantAccess(request).toCompletableFuture(); - final CompletionException err = assertThrows(CompletionException.class, future::join); - assertInstanceOf(AccessGrantException.class, err.getCause()); + assertDoesNotThrow(client.requestAccess(req).toCompletableFuture()::join); } @Test - void testQueryGrant() { + void testBothTemplatesResources() { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -400,31 +502,25 @@ void testQueryGrant() { final String token = generateIdToken(claims); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - final URI resource = URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/a/b/c"); - final List grants = client.query(resource, null, null, null, "Read", AccessGrant.class) - .toCompletableFuture().join(); - assertEquals(1, grants.size()); - } + final URI recipient = URI.create("https://id.test/agent"); + final Instant expiration = Instant.parse("2022-08-27T12:00:00Z"); + final Set modes = Set.of("Read", "Append"); + final Set purposes = Collections.singleton(URI.create("https://purpose.test/Purpose1")); - @Test - void testQueryGrantBuilder() { - final Map claims = new HashMap<>(); - claims.put("webid", WEBID); - claims.put("sub", SUB); - claims.put("iss", ISS); - claims.put("azp", AZP); - final String token = generateIdToken(claims); - final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + final var req = AccessRequest.RequestParameters.newBuilder() + .recipient(recipient) + .modes(modes) + .purposes(purposes) + .expiration(expiration) + .template(AccessRequest.template("data")) + .resource(URI.create("https://storage.test/path/data/")) + .build(); - final URI resource = URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/a/b/c"); - final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() - .resource(resource).mode("Read").build(AccessGrant.class); - final List grants = client.query(query).toCompletableFuture().join(); - assertEquals(1, grants.size()); + assertDoesNotThrow(client.requestAccess(req).toCompletableFuture()::join); } @Test - void testQueryGrantRecipient() { + void testGrantAccessNoAuth() { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -433,29 +529,22 @@ void testQueryGrantRecipient() { final String token = generateIdToken(claims); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - final List grants = client.query(null, null, URI.create("https://id.test/user"), - null, "Read", AccessGrant.class).toCompletableFuture().join(); - assertEquals(1, grants.size()); - } + final URI recipient = URI.create("https://id.test/agent"); + final Instant expiration = Instant.parse("2022-08-27T12:00:00Z"); + final Set modes = Set.of("Read", "Append"); + final Set purposes = Collections.singleton(URI.create("https://purpose.test/Purpose1")); - @Test - void testQueryGrantCreator() { - final Map claims = new HashMap<>(); - claims.put("webid", WEBID); - claims.put("sub", SUB); - claims.put("iss", ISS); - claims.put("azp", AZP); - final String token = generateIdToken(claims); - final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - // A query is always done with the agent making the query as the creator. - final List grants = client.query( - null, URI.create("https://id.test/user"), null, null, "Read", AccessGrant.class - ).toCompletableFuture().join(); - assertEquals(1, grants.size()); + final Set resources = Collections.singleton(URI.create("https://storage.test/data/")); + final AccessRequest request = client.requestAccess(recipient, resources, modes, purposes, expiration) + .toCompletableFuture().join(); + + final CompletableFuture future = agClient.grantAccess(request).toCompletableFuture(); + final CompletionException err = assertThrows(CompletionException.class, future::join); + assertInstanceOf(AccessGrantException.class, err.getCause()); } @Test - void testQueryGrantModesPurposesBuilder() { + void testQueryGrantFilter() { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -464,16 +553,16 @@ void testQueryGrantModesPurposesBuilder() { final String token = generateIdToken(claims); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() - .resource(URI.create("https://storage.example/")) - .mode("Read").mode("Write").purpose(URI.create("https://id.example/Purpose8")) - .purpose(URI.create("https://id.example/Purpose9")).build(AccessGrant.class); - final List grants = client.query(query).toCompletableFuture().join(); - assertEquals(1, grants.size()); + final CredentialFilter filter = CredentialFilter.newBuilder().build(AccessGrant.class); + + final CredentialResult results = client.query(filter).toCompletableFuture().join(); + + assertEquals(1, results.getItems().size()); + assertEquals("/access-grant-1", results.getItems().get(0).getIdentifier().getPath()); } @Test - void testQueryGrantModesPurposesNoMatchBuilder() { + void testQueryRequestFilter() { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -482,22 +571,17 @@ void testQueryGrantModesPurposesNoMatchBuilder() { final String token = generateIdToken(claims); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - final AccessCredentialQuery query1 = AccessCredentialQuery.newBuilder() - .resource(URI.create("https://storage.example/")) - .mode("Read").mode(null).purpose(URI.create("https://id.example/Purpose8")) - .purpose(URI.create("https://id.example/Purpose9")).purpose(null).build(AccessGrant.class); - final List grants1 = client.query(query1).toCompletableFuture().join(); - assertEquals(0, grants1.size()); + final CredentialFilter filter = CredentialFilter.newBuilder().build(AccessRequest.class); - final AccessCredentialQuery query2 = AccessCredentialQuery.newBuilder() - .resource(URI.create("https://storage.example/")).mode("Read").mode("Write") - .purpose(URI.create("https://id.example/Purpose9")).build(AccessGrant.class); - final List grants2 = client.query(query2).toCompletableFuture().join(); - assertEquals(0, grants2.size()); + final CredentialResult results = client.query(filter).toCompletableFuture().join(); + + assertEquals(1, results.getItems().size()); + assertEquals("https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", + results.getItems().get(0).getIdentifier().toString()); } @Test - void testQueryRequestRecipient() { + void testQueryDenialFilter() { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -506,29 +590,17 @@ void testQueryRequestRecipient() { final String token = generateIdToken(claims); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - final List requests = client.query(null, null, URI.create("https://id.test/user"), - null, "Read", AccessRequest.class).toCompletableFuture().join(); - assertEquals(1, requests.size()); - } + final CredentialFilter filter = CredentialFilter.newBuilder().build(AccessDenial.class); - @Test - void testQueryRequestRecipientBuilder() { - final Map claims = new HashMap<>(); - claims.put("webid", WEBID); - claims.put("sub", SUB); - claims.put("iss", ISS); - claims.put("azp", AZP); - final String token = generateIdToken(claims); - final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + final CredentialResult results = client.query(filter).toCompletableFuture().join(); - final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() - .recipient(URI.create("https://id.test/user")).mode("Read").build(AccessRequest.class); - final List requests = client.query(query).toCompletableFuture().join(); - assertEquals(1, requests.size()); + assertEquals(1, results.getItems().size()); + assertEquals("https://accessgrant.test/credential/fc2dbcd9-81d4-4fa4-8fd4-239e16dd83ab", + results.getItems().get(0).getIdentifier().toString()); } @Test - void testQueryRequest() { + void testQueryFilterInvalidType() { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -536,15 +608,12 @@ void testQueryRequest() { claims.put("azp", AZP); final String token = generateIdToken(claims); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - - final URI resource = URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"); - final List requests = client.query(resource, null, null, null, "Read", AccessRequest.class) - .toCompletableFuture().join(); - assertEquals(1, requests.size()); + final CredentialFilter filter = CredentialFilter.newBuilder().build(AccessCredential.class); + assertThrows(AccessGrantException.class, () -> client.query(filter)); } @Test - void testQueryRequestBuilder() { + void testQueryPageFilter() { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); @@ -553,59 +622,48 @@ void testQueryRequestBuilder() { final String token = generateIdToken(claims); final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); - final URI resource = URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"); - final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() - .resource(resource).mode("Read").build(AccessRequest.class); - final List requests = client.query(query).toCompletableFuture().join(); - assertEquals(1, requests.size()); - } + final CredentialFilter filter = CredentialFilter.newBuilder().page("1").pageSize(5) + .build(AccessRequest.class); - @Test - void testQueryDenial() { - final Map claims = new HashMap<>(); - claims.put("webid", WEBID); - claims.put("sub", SUB); - claims.put("iss", ISS); - claims.put("azp", AZP); - final String token = generateIdToken(claims); - final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + final CredentialResult results = client.query(filter).toCompletableFuture().join(); - final URI resource = URI.create("https://storage.example/ef9c4b90-0459-408d-bfa9-1c61d46e1eaf/e/f/g"); - final List grants = client.query(resource, null, null, null, "Read", AccessDenial.class) - .toCompletableFuture().join(); - assertEquals(1, grants.size()); + assertEquals(5, results.getItems().size()); + assertEquals("https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", + results.getItems().get(0).getIdentifier().toString()); + assertEquals("https://accessgrant.test/credential/52049498-fc2e-45be-be79-703a39086574", + results.getItems().get(1).getIdentifier().toString()); + assertEquals("https://accessgrant.test/credential/a0a73677-d139-48e4-8e62-0ae63925bd2b", + results.getItems().get(2).getIdentifier().toString()); + assertEquals("https://accessgrant.test/credential/3869d6d9-3b3e-4c77-b842-6c938367e3b5", + results.getItems().get(3).getIdentifier().toString()); + assertEquals("https://accessgrant.test/credential/3f61667a-4569-43f3-854e-1832f898049e", + results.getItems().get(4).getIdentifier().toString()); + assertEquals(Optional.empty(), results.prevPage()); + assertEquals(Optional.of("1"), results.firstPage().flatMap(CredentialFilter::getPage)); + assertEquals(Optional.of("2"), results.nextPage().flatMap(CredentialFilter::getPage)); + assertEquals(Optional.of("3"), results.lastPage().flatMap(CredentialFilter::getPage)); } @Test - void testQueryDenialBuilder() { + void testServerUnsupportedFilterQuery() { final Map claims = new HashMap<>(); claims.put("webid", WEBID); claims.put("sub", SUB); claims.put("iss", ISS); claims.put("azp", AZP); final String token = generateIdToken(claims); - final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + final AccessGrantClient altAgClient = new AccessGrantClient(URIBuilder.newBuilder(baseUri).path("alternative") + .build()); - final URI resource = URI.create("https://storage.example/ef9c4b90-0459-408d-bfa9-1c61d46e1eaf/e/f/g"); - final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() - .resource(resource).mode("Read").build(AccessDenial.class); - final List grants = client.query(query).toCompletableFuture().join(); - assertEquals(1, grants.size()); - } + final AccessGrantClient client = altAgClient.session(OpenIdSession.ofIdToken(token)); - @Test - void testQueryInvalidType() { - final Map claims = new HashMap<>(); - claims.put("webid", WEBID); - claims.put("sub", SUB); - claims.put("iss", ISS); - claims.put("azp", AZP); - final String token = generateIdToken(claims); - final AccessGrantClient client = agClient.session(OpenIdSession.ofIdToken(token)); + final URI resource = URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/"); + final CredentialFilter filter = CredentialFilter.newBuilder().page("1").pageSize(2) + .resource(resource).build(AccessGrant.class); - final URI uri = URI.create("https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/a/b/c"); - assertThrows(AccessGrantException.class, () -> - client.query(uri, null, null, null, "Read", AccessCredential.class)); + final CompletionException err = assertThrows(CompletionException.class, + client.query(filter).toCompletableFuture()::join); + assertInstanceOf(AccessGrantException.class, err.getCause()); } @Test @@ -654,7 +712,7 @@ void checkAsUri() { final String uri = "https://example.com/"; assertNull(AccessGrantClient.asUri(null)); assertNull(AccessGrantClient.asUri(5)); - assertNull(AccessGrantClient.asUri(Arrays.asList(uri))); + assertNull(AccessGrantClient.asUri(List.of(uri))); assertEquals(URI.create(uri), AccessGrantClient.asUri(uri)); } @@ -683,4 +741,10 @@ static String generateIdToken(final Map claims) { throw new UncheckedJoseException("Unable to generate DPoP token", ex); } } + + @Test + void testCastInvalidType() { + final Map data = new HashMap<>(); + assertNull(agClient.cast(data, AccessCredential.class)); + } } diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java index 3e9f67c3ec0..966eaa1c0bb 100644 --- a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessGrantTest.java @@ -52,6 +52,39 @@ void testReadAccessGrant() throws IOException { expectedTypes.add("VerifiableCredential"); expectedTypes.add("SolidAccessGrant"); assertEquals(expectedTypes, grant.getTypes()); + assertNull(grant.getAccessRequest()); + assertEquals(Instant.parse("2022-08-27T12:00:00Z"), grant.getExpiration()); + assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), grant.getIssuedAt()); + assertEquals(URI.create("https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626"), + grant.getIdentifier()); + assertEquals(Collections.singleton(URI.create("https://purpose.example/Purpose1")), grant.getPurposes()); + assertEquals(Collections.singleton( + URI.create("https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/")), + grant.getResources()); + assertEquals(URI.create("https://id.example/grantor"), grant.getCreator()); + assertEquals(Optional.of(URI.create("https://id.example/grantee")), grant.getRecipient()); + final Optional status = grant.getStatus(); + assertTrue(status.isPresent()); + status.ifPresent(s -> { + assertEquals(URI.create("https://accessgrant.example/status/CVAM#2832"), s.getIdentifier()); + assertEquals(URI.create("https://accessgrant.example/status/CVAM"), s.getCredential()); + assertEquals(2832, s.getIndex()); + assertEquals("RevocationList2020Status", s.getType()); + }); + } + } + + @Test + void testReadAccessGrantQualifiedName() throws IOException { + try (final InputStream resource = AccessGrantTest.class.getResourceAsStream("/access_grant4.json")) { + final AccessGrant grant = AccessGrant.of(resource); + assertEquals(Collections.singleton("Read"), grant.getModes()); + assertEquals(URI.create("https://accessgrant.example"), grant.getIssuer()); + final Set expectedTypes = new HashSet<>(); + expectedTypes.add("VerifiableCredential"); + expectedTypes.add("vc:SolidAccessGrant"); + assertEquals(expectedTypes, grant.getTypes()); + assertEquals(URI.create("https://accessrequest.example/1234"), grant.getAccessRequest()); assertEquals(Instant.parse("2022-08-27T12:00:00Z"), grant.getExpiration()); assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), grant.getIssuedAt()); assertEquals(URI.create("https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626"), diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessRequestTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessRequestTest.java index fe3059af83c..a6eea106eb4 100644 --- a/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessRequestTest.java +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/AccessRequestTest.java @@ -100,7 +100,6 @@ void testBuilderWithCollections() { assertNull(params.getIssuedAt()); } - @Test void testReadAccessRequest() throws IOException { try (final InputStream resource = AccessRequestTest.class.getResourceAsStream("/access_request1.json")) { @@ -132,6 +131,37 @@ void testReadAccessRequest() throws IOException { } } + @Test + void testReadAccessRequestQualifiedName() throws IOException { + try (final InputStream resource = AccessRequestTest.class.getResourceAsStream("/access_request3.json")) { + final AccessRequest request = AccessRequest.of(resource); + assertEquals(Collections.singleton("Read"), request.getModes()); + assertEquals(URI.create("https://accessgrant.test"), request.getIssuer()); + final Set expectedTypes = new HashSet<>(); + expectedTypes.add("VerifiableCredential"); + expectedTypes.add("vc:SolidAccessRequest"); + assertEquals(expectedTypes, request.getTypes()); + assertEquals(Instant.parse("2022-08-27T12:00:00Z"), request.getExpiration()); + assertEquals(Instant.parse("2022-08-25T20:34:05.153Z"), request.getIssuedAt()); + assertEquals(URI.create("https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab"), + request.getIdentifier()); + assertEquals(Collections.singleton(URI.create("https://purpose.test/Purpose1")), request.getPurposes()); + assertEquals(Collections.singleton( + URI.create("https://storage.test/data/")), + request.getResources()); + assertEquals(URI.create("https://id.test/username"), request.getCreator()); + assertEquals(Optional.of(URI.create("https://id.test/agent")), request.getRecipient()); + final Optional status = request.getStatus(); + assertTrue(status.isPresent()); + status.ifPresent(s -> { + assertEquals(URI.create("https://accessgrant.test/status/CVAM#2832"), s.getIdentifier()); + assertEquals(URI.create("https://accessgrant.test/status/CVAM"), s.getCredential()); + assertEquals(2832, s.getIndex()); + assertEquals("RevocationList2020Status", s.getType()); + }); + } + } + @Test void testReadAccessRequestSingletons() throws IOException { try (final InputStream resource = AccessRequestTest.class.getResourceAsStream("/access_request2.json")) { @@ -242,4 +272,19 @@ void testInvalidStream() throws IOException { void testInvalidString() throws IOException { assertThrows(IllegalArgumentException.class, () -> AccessRequest.of("not json")); } + + @Test + void testTemplate() { + assertEquals("https://{domain}/{+path}/custom-path", AccessRequest.template("custom-path")); + assertEquals("https://{domain}/{+path}/custom-path", AccessRequest.template("/custom-path")); + assertEquals("https://{domain}/{+path}/./custom-path", AccessRequest.template("./custom-path")); + assertEquals("https://{domain}/{+path}/", AccessRequest.template("")); + } + + @Test + void testTemplateValues() { + final var values = AccessRequest.templateValues("mydomain.com", "/storage/path"); + assertEquals("mydomain.com", values.get("domain")); + assertEquals("/storage/path", values.get("path")); + } } diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/CredentialFilterTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/CredentialFilterTest.java new file mode 100644 index 00000000000..ec035e344df --- /dev/null +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/CredentialFilterTest.java @@ -0,0 +1,192 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.accessgrant; + +import static org.junit.jupiter.api.Assertions.*; + +import com.inrupt.client.util.URIBuilder; + +import java.net.URI; +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.Test; + +class CredentialFilterTest { + + private static final URI ALICE = URI.create("https://agent.test/alice"); + private static final URI BOB = URI.create("https://agent.test/bob"); + private static final URI PURPOSE = URI.create("https://purpose.test/Purpose"); + private static final URI RESOURCE = URI.create("https://storage.test/data/"); + private static final URI BASE_URL = URI.create("https://credential.test/query"); + + @Test + void testCredentialEmptyFilterBuilder() { + final CredentialFilter filter = CredentialFilter.newBuilder().build(AccessGrant.class); + assertFalse(filter.getPage().isPresent()); + assertFalse(filter.getFromAgent().isPresent()); + assertFalse(filter.getToAgent().isPresent()); + assertFalse(filter.getPurpose().isPresent()); + assertFalse(filter.getResource().isPresent()); + assertFalse(filter.getStatus().isPresent()); + assertFalse(filter.getIssuedWithin().isPresent()); + assertFalse(filter.getRevokedWithin().isPresent()); + assertEquals(20, filter.getPageSize()); + assertEquals(AccessGrant.class, filter.getCredentialType()); + + final URI expectedUri = URIBuilder.newBuilder(BASE_URL) + .queryParam("type", "SolidAccessGrant") + .queryParam("pageSize", "20").build(); + assertEquals(expectedUri, filter.asURI(BASE_URL)); + } + + @Test + void testCredentialInvalidPageSizeFilterBuilder() { + final CredentialFilter filter = CredentialFilter.newBuilder() + .pageSize(-10) + .build(AccessDenial.class); + assertEquals(20, filter.getPageSize()); + assertEquals(AccessDenial.class, filter.getCredentialType()); + + final URI expectedUri = URIBuilder.newBuilder(BASE_URL) + .queryParam("type", "SolidAccessDenial") + .queryParam("pageSize", "20").build(); + assertEquals(expectedUri, filter.asURI(BASE_URL)); + } + + @Test + void testCredentialExcessivePageSizeFilterBuilder() { + final CredentialFilter filter = CredentialFilter.newBuilder() + .pageSize(200) + .build(AccessDenial.class); + assertEquals(20, filter.getPageSize()); + assertEquals(AccessDenial.class, filter.getCredentialType()); + + final URI expectedUri = URIBuilder.newBuilder(BASE_URL) + .queryParam("type", "SolidAccessDenial") + .queryParam("pageSize", "20").build(); + assertEquals(expectedUri, filter.asURI(BASE_URL)); + } + + @Test + void testCredentialFilterBuilder() { + final String page = UUID.randomUUID().toString(); + final CredentialFilter.CredentialStatus status = CredentialFilter.CredentialStatus.PENDING; + final CredentialFilter.CredentialDuration issuedWithin = CredentialFilter.CredentialDuration.P1D; + final CredentialFilter.CredentialDuration revokedWithin = CredentialFilter.CredentialDuration.P7D; + + final CredentialFilter filter = CredentialFilter.newBuilder() + .status(status) + .fromAgent(ALICE) + .toAgent(BOB) + .purpose(PURPOSE) + .resource(RESOURCE) + .issuedWithin(issuedWithin) + .revokedWithin(revokedWithin) + .page(page) + .pageSize(40) + .build(AccessRequest.class); + + assertEquals(Optional.of(ALICE), filter.getFromAgent()); + assertEquals(Optional.of(BOB), filter.getToAgent()); + assertEquals(Optional.of(PURPOSE), filter.getPurpose()); + assertEquals(Optional.of(RESOURCE), filter.getResource()); + assertEquals(Optional.of(status), filter.getStatus()); + assertEquals(Optional.of(issuedWithin), filter.getIssuedWithin()); + assertEquals(Optional.of(revokedWithin), filter.getRevokedWithin()); + assertEquals(Optional.of(page), filter.getPage()); + assertEquals(40, filter.getPageSize()); + assertEquals(AccessRequest.class, filter.getCredentialType()); + + final URI expectedUri = URIBuilder.newBuilder(BASE_URL) + .queryParam("type", "SolidAccessRequest") + .queryParam("pageSize", "40") + .queryParam("purpose", PURPOSE.toString()) + .queryParam("resource", RESOURCE.toString()) + .queryParam("fromAgent", ALICE.toString()) + .queryParam("toAgent", BOB.toString()) + .queryParam("status", status.getValue()) + .queryParam("issuedWithin", issuedWithin.name()) + .queryParam("revokedWithin", revokedWithin.name()) + .queryParam("page", page) + .build(); + assertEquals(expectedUri, filter.asURI(BASE_URL)); + } + + @Test + void testCredentialFilterAmendedBuilder() { + final String page = UUID.randomUUID().toString(); + final CredentialFilter.CredentialStatus status = CredentialFilter.CredentialStatus.PENDING; + final CredentialFilter.CredentialDuration issuedWithin = CredentialFilter.CredentialDuration.P1D; + final CredentialFilter.CredentialDuration revokedWithin = CredentialFilter.CredentialDuration.P7D; + + final CredentialFilter filter1 = CredentialFilter.newBuilder() + .status(status) + .fromAgent(BOB) + .toAgent(ALICE) + .purpose(PURPOSE) + .resource(RESOURCE) + .issuedWithin(issuedWithin) + .revokedWithin(revokedWithin) + .page(page) + .pageSize(10) + .build(AccessRequest.class); + + final String page2 = UUID.randomUUID().toString(); + final CredentialFilter filter2 = CredentialFilter.newBuilder(filter1) + .page(page2) + .build(AccessRequest.class); + + assertEquals(Optional.of(BOB), filter2.getFromAgent()); + assertEquals(Optional.of(ALICE), filter2.getToAgent()); + assertEquals(Optional.of(PURPOSE), filter2.getPurpose()); + assertEquals(Optional.of(RESOURCE), filter2.getResource()); + assertEquals(Optional.of(status), filter2.getStatus()); + assertEquals(Optional.of(issuedWithin), filter2.getIssuedWithin()); + assertEquals(Optional.of(revokedWithin), filter2.getRevokedWithin()); + assertEquals(Optional.of(page2), filter2.getPage()); + assertEquals(10, filter2.getPageSize()); + assertEquals(AccessRequest.class, filter2.getCredentialType()); + + final URI expectedUri = URIBuilder.newBuilder(BASE_URL) + .queryParam("type", "SolidAccessRequest") + .queryParam("pageSize", "10") + .queryParam("purpose", PURPOSE.toString()) + .queryParam("resource", RESOURCE.toString()) + .queryParam("fromAgent", BOB.toString()) + .queryParam("toAgent", ALICE.toString()) + .queryParam("status", status.getValue()) + .queryParam("issuedWithin", issuedWithin.name()) + .queryParam("revokedWithin", revokedWithin.name()) + .queryParam("page", page2) + .build(); + assertEquals(expectedUri, filter2.asURI(BASE_URL)); + } + + @Test + void testDurations() { + assertEquals(Duration.ofDays(1), CredentialFilter.CredentialDuration.P1D.asDuration()); + assertEquals(Duration.ofDays(7), CredentialFilter.CredentialDuration.P7D.asDuration()); + assertEquals(Duration.ofDays(30), CredentialFilter.CredentialDuration.P1M.asDuration()); + assertEquals(Duration.ofDays(90), CredentialFilter.CredentialDuration.P3M.asDuration()); + } +} diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/CredentialResultTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/CredentialResultTest.java new file mode 100644 index 00000000000..ed1689b369b --- /dev/null +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/CredentialResultTest.java @@ -0,0 +1,42 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.accessgrant; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +class CredentialResultTest { + + @Test + void checkNullCredentialResult() { + final CredentialResult result = new CredentialResult<>(Collections.emptyList(), + null, null, null, null); + + assertTrue(result.getItems().isEmpty()); + assertFalse(result.firstPage().isPresent()); + assertFalse(result.prevPage().isPresent()); + assertFalse(result.nextPage().isPresent()); + assertFalse(result.lastPage().isPresent()); + } +} diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/MockAccessGrantServer.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/MockAccessGrantServer.java index 5bde06e1ea2..cfd5e49d431 100644 --- a/access-grant/src/test/java/com/inrupt/client/accessgrant/MockAccessGrantServer.java +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/MockAccessGrantServer.java @@ -56,6 +56,12 @@ public void stop() { } private void setupMocks() { + wireMockServer.stubFor(get(urlEqualTo("/alternative/.well-known/vc-configuration")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(getResource("/alternative-vc-configuration.json", wireMockServer.baseUrl())))); + wireMockServer.stubFor(get(urlEqualTo("/.well-known/vc-configuration")) .willReturn(aResponse() .withStatus(200) @@ -133,6 +139,32 @@ private void setupMocks() { .withStatus(401) .withHeader("WWW-Authenticate", "Bearer,DPoP algs=\"ES256\""))); + wireMockServer.stubFor(get(urlEqualTo("/access-request-6")) + .atPriority(1) + .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(getResource("/template-6.json", wireMockServer.baseUrl())))); + + wireMockServer.stubFor(get(urlEqualTo("/access-request-6")) + .atPriority(2) + .willReturn(aResponse() + .withStatus(401) + .withHeader("WWW-Authenticate", "Bearer,DPoP algs=\"ES256\""))); + + wireMockServer.stubFor(delete(urlEqualTo("/access-request-6")) + .atPriority(1) + .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) + .willReturn(aResponse() + .withStatus(204))); + + wireMockServer.stubFor(delete(urlEqualTo("/access-request-6")) + .atPriority(2) + .willReturn(aResponse() + .withStatus(401) + .withHeader("WWW-Authenticate", "Bearer,DPoP algs=\"ES256\""))); + wireMockServer.stubFor(get(urlEqualTo("/access-grant-6")) .atPriority(1) .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) @@ -173,6 +205,67 @@ private void setupMocks() { .withStatus(401) .withHeader("WWW-Authenticate", "Bearer,DPoP algs=\"ES256\""))); + wireMockServer.stubFor(get(urlEqualTo("/query?type=SolidAccessGrant&pageSize=20")) + .atPriority(1) + .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(getResource("/query_grant_response.json", wireMockServer.baseUrl())))); + + wireMockServer.stubFor(get(urlEqualTo("/query?type=SolidAccessGrant&pageSize=20")) + .atPriority(2) + .willReturn(aResponse() + .withStatus(401) + .withHeader("WWW-Authenticate", "Bearer,DPoP algs=\"ES256\""))); + + wireMockServer.stubFor(get(urlEqualTo("/query?type=SolidAccessRequest&pageSize=20")) + .atPriority(1) + .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(getResource("/query_request_response.json", wireMockServer.baseUrl())))); + + wireMockServer.stubFor(get(urlEqualTo("/query?type=SolidAccessRequest&pageSize=20")) + .atPriority(2) + .willReturn(aResponse() + .withStatus(401) + .withHeader("WWW-Authenticate", "Bearer,DPoP algs=\"ES256\""))); + + wireMockServer.stubFor(get(urlEqualTo("/query?type=SolidAccessDenial&pageSize=20")) + .atPriority(1) + .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withBody(getResource("/query_denial_response.json", wireMockServer.baseUrl())))); + + wireMockServer.stubFor(get(urlEqualTo("/query?type=SolidAccessDenial&pageSize=20")) + .atPriority(2) + .willReturn(aResponse() + .withStatus(401) + .withHeader("WWW-Authenticate", "Bearer,DPoP algs=\"ES256\""))); + + wireMockServer.stubFor(get(urlEqualTo("/query?type=SolidAccessRequest&pageSize=5&page=1")) + .atPriority(1) + .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) + .willReturn(aResponse() + .withStatus(200) + .withHeader(CONTENT_TYPE, APPLICATION_JSON) + .withHeader("Link", "; rel=\"first\"") + .withHeader("Link", "; rel=\"next\"") + .withHeader("Link", "; rel=\"last\"") + .withHeader("Link", "<>; type=\"invalid\"") + .withHeader("Link", "; rel=\"self\"") + .withBody(getResource("/query_request_page_response.json", wireMockServer.baseUrl())))); + + wireMockServer.stubFor(get(urlEqualTo("/query?type=SolidAccessRequest&pageSize=5&page=1")) + .atPriority(2) + .willReturn(aResponse() + .withStatus(401) + .withHeader("WWW-Authenticate", "Bearer,DPoP algs=\"ES256\""))); + wireMockServer.stubFor(get(urlMatching("/not-found.*")) .willReturn(aResponse() .withStatus(404))); @@ -210,6 +303,17 @@ private void setupMocks() { .withHeader("Content-Type", "application/json") .withBody(getResource("/vc-4.json", wireMockServer.baseUrl())))); + wireMockServer.stubFor(post(urlEqualTo("/issue")) + .atPriority(1) + .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) + .withRequestBody(containing("\"providedConsent\"")) + .withRequestBody(containing("\"2022-08-27T12:00:00Z\"")) + .withRequestBody(containing("\"verifiedRequest\"")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(getResource("/vc-4-verified.json", wireMockServer.baseUrl())))); + // Access Request wireMockServer.stubFor(post(urlEqualTo("/issue")) .atPriority(1) @@ -273,94 +377,6 @@ private void setupMocks() { .willReturn(aResponse() .withStatus(401) .withHeader("WWW-Authenticate", "Bearer,DPoP algs=\"ES256\""))); - - wireMockServer.stubFor(post(urlEqualTo("/derive")) - .atPriority(1) - .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) - .withRequestBody(containing("SolidAccessGrant")) - .withRequestBody(containing("\"https://id.example/Purpose8\"")) - .withRequestBody(containing("\"https://id.example/Purpose9\"")) - .withRequestBody(containing("\"Read\"")) - .withRequestBody(containing("\"Write\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(getResource("/query_response7.json", wireMockServer.baseUrl())))); - - wireMockServer.stubFor(post(urlEqualTo("/derive")) - .atPriority(1) - .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) - .withRequestBody(containing("SolidAccessDenial")) - .withRequestBody(containing( - "\"https://storage.example/ef9c4b90-0459-408d-bfa9-1c61d46e1eaf/\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(getResource("/query_response6.json", wireMockServer.baseUrl())))); - - wireMockServer.stubFor(post(urlEqualTo("/derive")) - .atPriority(1) - .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) - .withRequestBody(containing("SolidAccessRequest")) - .withRequestBody(containing( - "\"https://storage.example/f1759e6d-4dda-4401-be61-d90d070a5474/\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(getResource("/query_response3.json", wireMockServer.baseUrl())))); - - wireMockServer.stubFor(post(urlEqualTo("/derive")) - .atPriority(1) - .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) - .withRequestBody(containing("SolidAccessGrant")) - .withRequestBody(containing( - "\"https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(getResource("/query_response1.json", wireMockServer.baseUrl())))); - - wireMockServer.stubFor(post(urlEqualTo("/derive")) - .atPriority(2) - .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) - .withRequestBody(containing("\"isProvidedTo\":\"https://id.test/user\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(getResource("/query_response4.json", wireMockServer.baseUrl())))); - - wireMockServer.stubFor(post(urlEqualTo("/derive")) - .atPriority(2) - .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) - .withRequestBody(containing("\"isConsentForDataSubject\":\"https://id.test/user\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(getResource("/query_response5.json", wireMockServer.baseUrl())))); - - wireMockServer.stubFor(post(urlEqualTo("/derive")) - .atPriority(2) - .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) - .withRequestBody(containing("\"id\":\"https://id.test/user\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(getResource("/query_response4.json", wireMockServer.baseUrl())))); - - wireMockServer.stubFor(post(urlEqualTo("/derive")) - .atPriority(2) - .withHeader("Authorization", containing("Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.")) - .withRequestBody(containing("\"https://storage.example/")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(getResource("/query_response2.json", wireMockServer.baseUrl())))); - - wireMockServer.stubFor(post(urlEqualTo("/derive")) - .atPriority(3) - .willReturn(aResponse() - .withStatus(401) - .withHeader("WWW-Authenticate", "Bearer,DPoP algs=\"ES256\""))); } private static String getResource(final String path) { @@ -374,7 +390,5 @@ private static String getResource(final String path) { private static String getResource(final String path, final String baseUrl) { return getResource(path).replace("{{baseUrl}}", baseUrl); } - - } diff --git a/access-grant/src/test/java/com/inrupt/client/accessgrant/UtilsTest.java b/access-grant/src/test/java/com/inrupt/client/accessgrant/UtilsTest.java new file mode 100644 index 00000000000..9d5e0f3bc1e --- /dev/null +++ b/access-grant/src/test/java/com/inrupt/client/accessgrant/UtilsTest.java @@ -0,0 +1,49 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.accessgrant; + +import static org.junit.jupiter.api.Assertions.*; + +import java.net.URI; +import java.util.Arrays; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +class UtilsTest { + + @Test + void testAsList() { + assertEquals(Optional.empty(), Utils.asList("not a list")); + final Object list = Arrays.asList("one", 2, 12.5f); + assertEquals(Optional.of(list), Utils.asList(list)); + } + + @Test + void testQueryParam() { + final URI uri = URI.create("https://example.com/?query&page=foo&item=bar"); + assertEquals("foo", Utils.getQueryParam(uri, "page")); + assertEquals("bar", Utils.getQueryParam(uri, "item")); + assertNull(Utils.getQueryParam(uri, "query")); + assertNull(Utils.getQueryParam(uri, "other")); + assertNull(Utils.getQueryParam(URI.create("https://example.com/path"), "param")); + } +} diff --git a/access-grant/src/test/resources/access_denial1.json b/access-grant/src/test/resources/access_denial1.json new file mode 100644 index 00000000000..f7781507540 --- /dev/null +++ b/access-grant/src/test/resources/access_denial1.json @@ -0,0 +1,37 @@ +{ + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.test/credential/fc2dbcd9-81d4-4fa4-8fd4-239e16dd83ab", + "type":["VerifiableCredential","SolidAccessDenial"], + "issuer":"https://accessgrant.test", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.test/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.test/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/grantor", + "providedConsent":{ + "mode":["Read"], + "request": "https://accessrequest.test/5678", + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRefused", + "isProvidedTo":"https://id.test/grantee", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/e973cc3d-5c28-4a10-98c5-e40079289358/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.test/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }] +} + diff --git a/access-grant/src/test/resources/access_denial2.json b/access-grant/src/test/resources/access_denial2.json new file mode 100644 index 00000000000..98b09d190c2 --- /dev/null +++ b/access-grant/src/test/resources/access_denial2.json @@ -0,0 +1,36 @@ +{ + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.test/credential/39a4fdd4-44b0-48a5-a9b5-7a9b648e9a67", + "type":["VerifiableCredential","vc:SolidAccessDenial"], + "issuer":"https://accessgrant.test", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.test/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.test/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/grantor", + "providedConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRefused", + "isProvidedTo":"https://id.test/grantee", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/d5ef4173-4f12-40b2-9a0e-18fa7cc0dd38/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.test/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }] +} + diff --git a/access-grant/src/test/resources/access_grant1.json b/access-grant/src/test/resources/access_grant1.json index ea139b6cb44..d86fc898702 100644 --- a/access-grant/src/test/resources/access_grant1.json +++ b/access-grant/src/test/resources/access_grant1.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/access_grant2.json b/access-grant/src/test/resources/access_grant2.json index 54e94002ea0..e14402fb3c0 100644 --- a/access-grant/src/test/resources/access_grant2.json +++ b/access-grant/src/test/resources/access_grant2.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/access_grant3.json b/access-grant/src/test/resources/access_grant3.json index 0f631cf5462..37916227d66 100644 --- a/access-grant/src/test/resources/access_grant3.json +++ b/access-grant/src/test/resources/access_grant3.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/access_grant4.json b/access-grant/src/test/resources/access_grant4.json new file mode 100644 index 00000000000..d48d126204b --- /dev/null +++ b/access-grant/src/test/resources/access_grant4.json @@ -0,0 +1,37 @@ +{ + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", + "type":["VerifiableCredential","vc:SolidAccessGrant"], + "issuer":"https://accessgrant.example", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.example/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.example/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.example/grantor", + "providedConsent":{ + "request":"https://accessrequest.example/1234", + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + "isProvidedTo":"https://id.example/grantee", + "forPurpose":["https://purpose.example/Purpose1"], + "forPersonalData":["https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }] +} + diff --git a/access-grant/src/test/resources/access_request1.json b/access-grant/src/test/resources/access_request1.json index 4404331f2b6..c8e88e6af8f 100644 --- a/access-grant/src/test/resources/access_request1.json +++ b/access-grant/src/test/resources/access_request1.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"https://accessgrant.test", diff --git a/access-grant/src/test/resources/access_request2.json b/access-grant/src/test/resources/access_request2.json index 7d6965e554e..3d3fa726f58 100644 --- a/access-grant/src/test/resources/access_request2.json +++ b/access-grant/src/test/resources/access_request2.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"https://accessgrant.test", diff --git a/access-grant/src/test/resources/access_request3.json b/access-grant/src/test/resources/access_request3.json new file mode 100644 index 00000000000..bfb0327a73c --- /dev/null +++ b/access-grant/src/test/resources/access_request3.json @@ -0,0 +1,36 @@ +{ + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", + "type":["VerifiableCredential","vc:SolidAccessRequest"], + "issuer":"https://accessgrant.test", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.test/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.test/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/username", + "hasConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", + "isConsentForDataSubject":"https://id.test/agent", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/data/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.test/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }] +} + diff --git a/access-grant/src/test/resources/alternative-vc-configuration.json b/access-grant/src/test/resources/alternative-vc-configuration.json new file mode 100644 index 00000000000..d85c7bc64ec --- /dev/null +++ b/access-grant/src/test/resources/alternative-vc-configuration.json @@ -0,0 +1,12 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.inrupt.com/credentials/v2.jsonld" + ], + "issuerService": "{{baseUrl}}/issue", + "statusService": "{{baseUrl}}/status", + "supportedSignatureTypes": [ + "Ed25519Signature2020" + ], + "verifierService": "{{baseUrl}}/verify" +} diff --git a/access-grant/src/test/resources/invalid_access_grant1.json b/access-grant/src/test/resources/invalid_access_grant1.json index c8790df7a8f..3cb9860a588 100644 --- a/access-grant/src/test/resources/invalid_access_grant1.json +++ b/access-grant/src/test/resources/invalid_access_grant1.json @@ -3,7 +3,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant10.json b/access-grant/src/test/resources/invalid_access_grant10.json index 2f03aa955e7..165e78e6446 100644 --- a/access-grant/src/test/resources/invalid_access_grant10.json +++ b/access-grant/src/test/resources/invalid_access_grant10.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":{"name":"VerifiableCredential"}, "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant11.json b/access-grant/src/test/resources/invalid_access_grant11.json index d5091964e67..cafd2b79742 100644 --- a/access-grant/src/test/resources/invalid_access_grant11.json +++ b/access-grant/src/test/resources/invalid_access_grant11.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant12.json b/access-grant/src/test/resources/invalid_access_grant12.json index 9b1a0345615..e78f9df6d69 100644 --- a/access-grant/src/test/resources/invalid_access_grant12.json +++ b/access-grant/src/test/resources/invalid_access_grant12.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant13.json b/access-grant/src/test/resources/invalid_access_grant13.json index 40369fb7f0a..2a46842c535 100644 --- a/access-grant/src/test/resources/invalid_access_grant13.json +++ b/access-grant/src/test/resources/invalid_access_grant13.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant14.json b/access-grant/src/test/resources/invalid_access_grant14.json index 757e5ac5b5b..ae16571564c 100644 --- a/access-grant/src/test/resources/invalid_access_grant14.json +++ b/access-grant/src/test/resources/invalid_access_grant14.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant2.json b/access-grant/src/test/resources/invalid_access_grant2.json index ec15077da3c..9ddd7bc79e0 100644 --- a/access-grant/src/test/resources/invalid_access_grant2.json +++ b/access-grant/src/test/resources/invalid_access_grant2.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "expirationDate":"2022-08-27T12:00:00Z", diff --git a/access-grant/src/test/resources/invalid_access_grant3.json b/access-grant/src/test/resources/invalid_access_grant3.json index a8afbecab26..e836c5f7d67 100644 --- a/access-grant/src/test/resources/invalid_access_grant3.json +++ b/access-grant/src/test/resources/invalid_access_grant3.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","http://www.w3.org/ns/solid/vc#SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant4.json b/access-grant/src/test/resources/invalid_access_grant4.json index bea31029ea9..7e89638cb86 100644 --- a/access-grant/src/test/resources/invalid_access_grant4.json +++ b/access-grant/src/test/resources/invalid_access_grant4.json @@ -5,7 +5,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant5.json b/access-grant/src/test/resources/invalid_access_grant5.json index cd7a3ce27d0..9e403c85e6f 100644 --- a/access-grant/src/test/resources/invalid_access_grant5.json +++ b/access-grant/src/test/resources/invalid_access_grant5.json @@ -11,7 +11,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant6.json b/access-grant/src/test/resources/invalid_access_grant6.json index 299ad3562b5..754fca9da29 100644 --- a/access-grant/src/test/resources/invalid_access_grant6.json +++ b/access-grant/src/test/resources/invalid_access_grant6.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", "expirationDate":"2022-08-27T12:00:00Z", diff --git a/access-grant/src/test/resources/invalid_access_grant7.json b/access-grant/src/test/resources/invalid_access_grant7.json index de18e3720a8..cdde4ceadc2 100644 --- a/access-grant/src/test/resources/invalid_access_grant7.json +++ b/access-grant/src/test/resources/invalid_access_grant7.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant8.json b/access-grant/src/test/resources/invalid_access_grant8.json index 1ff52f25a2b..b5479ffc5c1 100644 --- a/access-grant/src/test/resources/invalid_access_grant8.json +++ b/access-grant/src/test/resources/invalid_access_grant8.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_grant9.json b/access-grant/src/test/resources/invalid_access_grant9.json index 5202eafbe17..ffacc6d49d3 100644 --- a/access-grant/src/test/resources/invalid_access_grant9.json +++ b/access-grant/src/test/resources/invalid_access_grant9.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.example/credential/5c6060ad-2f16-4bc1-b022-dffb46bff626", "type":[{ "name": "VerifiableCredential"},["SolidAccessGrant"]], "issuer":"https://accessgrant.example", diff --git a/access-grant/src/test/resources/invalid_access_request1.json b/access-grant/src/test/resources/invalid_access_request1.json index 1e1b5280ea6..62435682d18 100644 --- a/access-grant/src/test/resources/invalid_access_request1.json +++ b/access-grant/src/test/resources/invalid_access_request1.json @@ -3,7 +3,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-request-5", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/invalid_access_request2.json b/access-grant/src/test/resources/invalid_access_request2.json index 6f9b9ffb0d4..07f7bbc355a 100644 --- a/access-grant/src/test/resources/invalid_access_request2.json +++ b/access-grant/src/test/resources/invalid_access_request2.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", "type":["VerifiableCredential","SolidAccessRequest"], "expirationDate":"2022-08-27T12:00:00Z", diff --git a/access-grant/src/test/resources/invalid_access_request3.json b/access-grant/src/test/resources/invalid_access_request3.json index 2fc78b7391f..985feb823f4 100644 --- a/access-grant/src/test/resources/invalid_access_request3.json +++ b/access-grant/src/test/resources/invalid_access_request3.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"https://accessgrant.test", diff --git a/access-grant/src/test/resources/invalid_access_request4.json b/access-grant/src/test/resources/invalid_access_request4.json index fe68834ad3d..c72ce489151 100644 --- a/access-grant/src/test/resources/invalid_access_request4.json +++ b/access-grant/src/test/resources/invalid_access_request4.json @@ -5,7 +5,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"https://accessgrant.test", diff --git a/access-grant/src/test/resources/invalid_access_request5.json b/access-grant/src/test/resources/invalid_access_request5.json index 9dfa22c57b6..2bfda144801 100644 --- a/access-grant/src/test/resources/invalid_access_request5.json +++ b/access-grant/src/test/resources/invalid_access_request5.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"https://accessgrant.test", "expirationDate":"2022-08-27T12:00:00Z", diff --git a/access-grant/src/test/resources/invalid_access_request6.json b/access-grant/src/test/resources/invalid_access_request6.json index f45e5ef2fc8..598ee846ff8 100644 --- a/access-grant/src/test/resources/invalid_access_request6.json +++ b/access-grant/src/test/resources/invalid_access_request6.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"https://accessgrant.test", diff --git a/access-grant/src/test/resources/invalid_access_request7.json b/access-grant/src/test/resources/invalid_access_request7.json index c40d927f700..b786d830f9c 100644 --- a/access-grant/src/test/resources/invalid_access_request7.json +++ b/access-grant/src/test/resources/invalid_access_request7.json @@ -6,7 +6,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"https://accessgrant.test", diff --git a/access-grant/src/test/resources/query_denial_response.json b/access-grant/src/test/resources/query_denial_response.json new file mode 100644 index 00000000000..7342a623aa5 --- /dev/null +++ b/access-grant/src/test/resources/query_denial_response.json @@ -0,0 +1,35 @@ +{ + "items": [ + { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.test/credential/fc2dbcd9-81d4-4fa4-8fd4-239e16dd83ab", + "type":["VerifiableCredential","SolidAccessDenial"], + "issuer":"https://accessgrant.test", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.test/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.test/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/grantor", + "providedConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRefused", + "isProvidedTo":"https://id.test/grantee", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/e973cc3d-5c28-4a10-98c5-e40079289358/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.test/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + } + ] +} diff --git a/access-grant/src/test/resources/query_grant_response.json b/access-grant/src/test/resources/query_grant_response.json new file mode 100644 index 00000000000..ec6fbf1a1df --- /dev/null +++ b/access-grant/src/test/resources/query_grant_response.json @@ -0,0 +1,34 @@ +{ + "items": [{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"{{baseUrl}}/access-grant-1", + "type":["VerifiableCredential","SolidAccessGrant"], + "issuer":"{{baseUrl}}", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.example/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.example/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.example/grantor", + "providedConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + "isProvidedTo":"https://id.example/grantee", + "forPurpose":["https://purpose.example/Purpose1"], + "forPersonalData":["https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }] +} + diff --git a/access-grant/src/test/resources/query_request_page_response.json b/access-grant/src/test/resources/query_request_page_response.json new file mode 100644 index 00000000000..0002b473362 --- /dev/null +++ b/access-grant/src/test/resources/query_request_page_response.json @@ -0,0 +1,160 @@ +{ + "items": [ + { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", + "type":["VerifiableCredential","SolidAccessRequest"], + "issuer":"https://accessgrant.test", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.test/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.test/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/username", + "hasConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", + "isConsentForDataSubject":"https://id.test/agent", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/data/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.test/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }, + { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.test/credential/52049498-fc2e-45be-be79-703a39086574", + "type":["VerifiableCredential","SolidAccessRequest"], + "issuer":"https://accessgrant.test", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.test/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.test/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/username", + "hasConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", + "isConsentForDataSubject":"https://id.test/agent", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/data/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.test/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }, + { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.test/credential/a0a73677-d139-48e4-8e62-0ae63925bd2b", + "type":["VerifiableCredential","SolidAccessRequest"], + "issuer":"https://accessgrant.test", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.test/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.test/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/username", + "hasConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", + "isConsentForDataSubject":"https://id.test/agent", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/data/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.test/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }, + { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.test/credential/3869d6d9-3b3e-4c77-b842-6c938367e3b5", + "type":["VerifiableCredential","SolidAccessRequest"], + "issuer":"https://accessgrant.test", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.test/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.test/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/username", + "hasConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", + "isConsentForDataSubject":"https://id.test/agent", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/data/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.test/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }, + { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.test/credential/3f61667a-4569-43f3-854e-1832f898049e", + "type":["VerifiableCredential","SolidAccessRequest"], + "issuer":"https://accessgrant.test", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.test/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.test/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/username", + "hasConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", + "isConsentForDataSubject":"https://id.test/agent", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/data/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.test/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + } + ] +} + diff --git a/access-grant/src/test/resources/query_request_response.json b/access-grant/src/test/resources/query_request_response.json new file mode 100644 index 00000000000..6c9de6378fc --- /dev/null +++ b/access-grant/src/test/resources/query_request_response.json @@ -0,0 +1,36 @@ +{ + "items": [ + { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"https://accessgrant.test/credential/d604c858-209a-4bb6-a7f8-2f52c9617cab", + "type":["VerifiableCredential","SolidAccessRequest"], + "issuer":"https://accessgrant.test", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.test/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.test/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/username", + "hasConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", + "isConsentForDataSubject":"https://id.test/agent", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/data/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.test/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + } + ] +} + diff --git a/access-grant/src/test/resources/query_response1.json b/access-grant/src/test/resources/query_response1.json index c1b81cea74e..888c1778cd5 100644 --- a/access-grant/src/test/resources/query_response1.json +++ b/access-grant/src/test/resources/query_response1.json @@ -4,7 +4,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-grant-1", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"{{baseUrl}}", @@ -29,6 +29,96 @@ "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", "type":"Ed25519Signature2020", "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }, { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"{{baseUrl}}/access-grant-2", + "type":["VerifiableCredential","SolidAccessGrant"], + "issuer":"{{baseUrl}}", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.example/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.example/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.example/grantor", + "providedConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + "isProvidedTo":"https://id.example/grantee", + "forPurpose":["https://purpose.example/Purpose1"], + "forPersonalData":["https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }, { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"{{baseUrl}}/access-grant-3", + "type":["VerifiableCredential","SolidAccessGrant"], + "issuer":"{{baseUrl}}", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.example/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.example/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.example/grantor", + "providedConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + "isProvidedTo":"https://id.example/grantee", + "forPurpose":["https://purpose.example/Purpose1"], + "forPersonalData":["https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }, { + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"{{baseUrl}}/access-grant-4", + "type":["VerifiableCredential","SolidAccessGrant"], + "issuer":"{{baseUrl}}", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.example/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.example/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.example/grantor", + "providedConsent":{ + "mode":["Read"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + "isProvidedTo":"https://id.example/grantee", + "forPurpose":["https://purpose.example/Purpose1"], + "forPersonalData":["https://storage.example/e973cc3d-5c28-4a10-98c5-e40079289358/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} }] } diff --git a/access-grant/src/test/resources/query_response3.json b/access-grant/src/test/resources/query_response3.json index 34246a49ad6..9ba49579a3d 100644 --- a/access-grant/src/test/resources/query_response3.json +++ b/access-grant/src/test/resources/query_response3.json @@ -4,7 +4,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-request-3", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/query_response4.json b/access-grant/src/test/resources/query_response4.json index 08a12d9a990..d31fc69ca96 100644 --- a/access-grant/src/test/resources/query_response4.json +++ b/access-grant/src/test/resources/query_response4.json @@ -4,7 +4,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-grant-1", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/query_response5.json b/access-grant/src/test/resources/query_response5.json index f463e4d914c..8f7a7074fd8 100644 --- a/access-grant/src/test/resources/query_response5.json +++ b/access-grant/src/test/resources/query_response5.json @@ -4,7 +4,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-request-3", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/query_response6.json b/access-grant/src/test/resources/query_response6.json index 9da48023a15..33a45b35e4e 100644 --- a/access-grant/src/test/resources/query_response6.json +++ b/access-grant/src/test/resources/query_response6.json @@ -4,7 +4,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-grant-1", "type":["VerifiableCredential","SolidAccessDenial"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/query_response7.json b/access-grant/src/test/resources/query_response7.json index 399c94bc5fb..46870caffa7 100644 --- a/access-grant/src/test/resources/query_response7.json +++ b/access-grant/src/test/resources/query_response7.json @@ -4,7 +4,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-grant-1", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/template-6.json b/access-grant/src/test/resources/template-6.json new file mode 100644 index 00000000000..ed803da730b --- /dev/null +++ b/access-grant/src/test/resources/template-6.json @@ -0,0 +1,31 @@ +{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"{{baseUrl}}/access-request-6", + "type":["VerifiableCredential","SolidAccessRequest"], + "issuer":"{{baseUrl}}", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.example/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.example/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/username", + "hasConsent":{ + "mode":["Read","Append"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusRequested", + "forPurpose":["https://purpose.test/Purpose1"], + "template":["https://{domain}/{+path}/data/"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} +} + diff --git a/access-grant/src/test/resources/vc-1.json b/access-grant/src/test/resources/vc-1.json index 11f15b6fe89..b1d1a8e6d24 100644 --- a/access-grant/src/test/resources/vc-1.json +++ b/access-grant/src/test/resources/vc-1.json @@ -3,7 +3,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-grant-1", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/vc-2.json b/access-grant/src/test/resources/vc-2.json index 23726166877..891942c8df8 100644 --- a/access-grant/src/test/resources/vc-2.json +++ b/access-grant/src/test/resources/vc-2.json @@ -3,7 +3,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-grant-2", "type":["VerifiableCredential","http://www.w3.org/ns/solid/vc#SolidAccessGrant"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/vc-4-verified.json b/access-grant/src/test/resources/vc-4-verified.json new file mode 100644 index 00000000000..ee74a4f5a39 --- /dev/null +++ b/access-grant/src/test/resources/vc-4-verified.json @@ -0,0 +1,34 @@ +{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v2.jsonld"], + "id":"{{baseUrl}}/access-grant-4", + "type":["VerifiableCredential","SolidAccessGrant"], + "issuer":"{{baseUrl}}", + "expirationDate":"2022-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.example/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.example/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"https://id.test/username", + "providedConsent":{ + "mode":["Read","Append"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + "isProvidedTo":"https://id.test/agent", + "forPurpose":["https://purpose.test/Purpose1"], + "forPersonalData":["https://storage.test/data/"], + "verifiedRequest": "http://localhost:33367/access-request-5"}}, + + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} +} + diff --git a/access-grant/src/test/resources/vc-4.json b/access-grant/src/test/resources/vc-4.json index d5c39ea9d0e..e0903397001 100644 --- a/access-grant/src/test/resources/vc-4.json +++ b/access-grant/src/test/resources/vc-4.json @@ -3,7 +3,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-grant-4", "type":["VerifiableCredential","SolidAccessGrant"], "issuer":"{{baseUrl}}", @@ -21,7 +21,8 @@ "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", "isProvidedTo":"https://id.test/agent", "forPurpose":["https://purpose.test/Purpose1"], - "forPersonalData":["https://storage.test/data/"]}}, + "forPersonalData":["https://storage.test/data/"], + "request": "http://localhost:33367/access-request-5"}}, "proof":{ "created":"2022-08-25T20:34:05.236Z", "proofPurpose":"assertionMethod", diff --git a/access-grant/src/test/resources/vc-5.json b/access-grant/src/test/resources/vc-5.json index 1e1b5280ea6..62435682d18 100644 --- a/access-grant/src/test/resources/vc-5.json +++ b/access-grant/src/test/resources/vc-5.json @@ -3,7 +3,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-request-5", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/vc-6.json b/access-grant/src/test/resources/vc-6.json index 3b8feec55a9..9dc606e584b 100644 --- a/access-grant/src/test/resources/vc-6.json +++ b/access-grant/src/test/resources/vc-6.json @@ -3,7 +3,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-grant-6", "type":["VerifiableCredential","http://www.w3.org/ns/solid/vc#SolidAccessGrant"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/vc-7.json b/access-grant/src/test/resources/vc-7.json index a133679b55d..eb5917e1ec5 100644 --- a/access-grant/src/test/resources/vc-7.json +++ b/access-grant/src/test/resources/vc-7.json @@ -3,7 +3,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-denial-1", "type":["VerifiableCredential","SolidAccessDenial"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/vc-8.json b/access-grant/src/test/resources/vc-8.json index 6691894239a..1ee3a613de0 100644 --- a/access-grant/src/test/resources/vc-8.json +++ b/access-grant/src/test/resources/vc-8.json @@ -3,7 +3,7 @@ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/ed25519-2020/v1", "https://w3id.org/vc-revocation-list-2020/v1", - "https://schema.inrupt.com/credentials/v1.jsonld"], + "https://schema.inrupt.com/credentials/v2.jsonld"], "id":"{{baseUrl}}/access-request-5", "type":["VerifiableCredential","SolidAccessRequest"], "issuer":"{{baseUrl}}", diff --git a/access-grant/src/test/resources/vc-configuration.json b/access-grant/src/test/resources/vc-configuration.json index 4a8398cdfe7..3f2e98b3f5a 100644 --- a/access-grant/src/test/resources/vc-configuration.json +++ b/access-grant/src/test/resources/vc-configuration.json @@ -1,10 +1,11 @@ { "@context": [ "https://www.w3.org/2018/credentials/v1", - "https://schema.inrupt.com/credentials/v1.jsonld" + "https://schema.inrupt.com/credentials/v2.jsonld" ], "derivationService": "{{baseUrl}}/derive", "issuerService": "{{baseUrl}}/issue", + "queryService": "{{baseUrl}}/query", "statusService": "{{baseUrl}}/status", "supportedSignatureTypes": [ "Ed25519Signature2020" diff --git a/acp/pom.xml b/acp/pom.xml new file mode 100644 index 00000000000..4233ea9e10b --- /dev/null +++ b/acp/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + com.inrupt.client + inrupt-client + 2.0.1-SNAPSHOT + + + inrupt-client-acp + Inrupt Java Client Libraries - Access Control Policies + + Access Control Policy support for the Inrupt Client Libraries. + + + + + com.inrupt.client + inrupt-client-api + ${project.version} + + + com.inrupt.client + inrupt-client-vocabulary + ${project.version} + + + com.inrupt.client + inrupt-client-solid + ${project.version} + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + org.wiremock + wiremock + ${wiremock.version} + test + + + com.inrupt.client + inrupt-client-jackson + ${project.version} + test + + + com.inrupt.client + inrupt-client-guava + ${project.version} + test + + + com.inrupt.client + inrupt-client-core + ${project.version} + test + + + com.inrupt.client + inrupt-client-httpclient + ${project.version} + test + + + com.inrupt.client + inrupt-client-jena + ${project.version} + test + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + + + org.jacoco + jacoco-maven-plugin + + + + diff --git a/acp/src/main/java/com/inrupt/client/acp/AccessControl.java b/acp/src/main/java/com/inrupt/client/acp/AccessControl.java new file mode 100644 index 00000000000..0e351769acd --- /dev/null +++ b/acp/src/main/java/com/inrupt/client/acp/AccessControl.java @@ -0,0 +1,69 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.acp; + +import static com.inrupt.client.acp.AccessControlResource.asIRI; + +import com.inrupt.client.vocabulary.ACP; +import com.inrupt.client.vocabulary.RDF; +import com.inrupt.rdf.wrapping.commons.ValueMappings; +import com.inrupt.rdf.wrapping.commons.WrapperIRI; + +import java.util.Set; + +import org.apache.commons.rdf.api.Graph; +import org.apache.commons.rdf.api.IRI; +import org.apache.commons.rdf.api.RDFTerm; + +/** + * An AccessControl type for use with Access Control Policies. + * + *

An access control applies {@link Policy} objects directly to a resource + * via {@code acp:accessControl} or to container members via {@code acp:memberAccessControl} + */ +public class AccessControl extends WrapperIRI { + + /** + * Create a new AccessControl. + * + * @param identifier the access control identifier + * @param graph the underlying graph + */ + public AccessControl(final RDFTerm identifier, final Graph graph) { + super(identifier, graph); + graph.add((IRI) identifier, asIRI(RDF.type), asIRI(ACP.AccessControl)); + } + + public Set apply() { + return objects(asIRI(ACP.apply), Policy::asResource, ValueMappings.as(Policy.class)); + } + + static IRI asResource(final AccessControl accessControl, final Graph graph) { + graph.add(accessControl, asIRI(RDF.type), asIRI(ACP.AccessControl)); + accessControl.apply().forEach(policy -> { + graph.add(accessControl, asIRI(ACP.apply), policy); + Policy.asResource(policy, graph); + }); + + return accessControl; + } +} + diff --git a/acp/src/main/java/com/inrupt/client/acp/AccessControlResource.java b/acp/src/main/java/com/inrupt/client/acp/AccessControlResource.java new file mode 100644 index 00000000000..eacf803375e --- /dev/null +++ b/acp/src/main/java/com/inrupt/client/acp/AccessControlResource.java @@ -0,0 +1,436 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.acp; + +import com.inrupt.client.RDFSource; +import com.inrupt.client.solid.SolidClient; +import com.inrupt.client.solid.SolidSyncClient; +import com.inrupt.client.vocabulary.ACP; +import com.inrupt.client.vocabulary.RDF; +import com.inrupt.rdf.wrapping.commons.ValueMappings; +import com.inrupt.rdf.wrapping.commons.WrapperIRI; + +import java.net.URI; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.apache.commons.rdf.api.BlankNodeOrIRI; +import org.apache.commons.rdf.api.Dataset; +import org.apache.commons.rdf.api.Graph; +import org.apache.commons.rdf.api.IRI; +import org.apache.commons.rdf.api.Quad; +import org.apache.commons.rdf.api.RDFTerm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An Access Control Resource type. + * + *

This is the root type for a resource that expresses access control policies. + */ +public class AccessControlResource extends RDFSource { + + private static final Logger LOGGER = LoggerFactory.getLogger(AccessControlResource.class); + + public static final URI SOLID_ACCESS_GRANT = URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant"); + + private static final Map EXPANSION_MAPPINGS = Map.of( + ACP.accessControl, asIRI(ACP.AccessControl), + ACP.memberAccessControl, asIRI(ACP.AccessControl), + ACP.apply, asIRI(ACP.Policy), + ACP.allOf, asIRI(ACP.Matcher), + ACP.anyOf, asIRI(ACP.Matcher), + ACP.noneOf, asIRI(ACP.Matcher)); + + private static final List EXPANSION_PROPERTIES = List.of(ACP.accessControl, ACP.memberAccessControl, + ACP.apply, ACP.allOf, ACP.anyOf, ACP.noneOf); + + /** + * Definitions for different matcher types, for use with {@link #find}. + */ + public enum MatcherType { + AGENT(ACP.agent), CLIENT(ACP.client), ISSUER(ACP.issuer), VC(ACP.vc); + + private final URI predicate; + + MatcherType(final URI predicate) { + this.predicate = predicate; + } + + /** + * Return the matcher type as an IRI. + * + * @return the IRI for this predicate + */ + public IRI asIRI() { + return AccessControlResource.asIRI(asURI()); + } + + /** + * Return the matcher type as a URI. + * + * @return the URI for this predicate + */ + public URI asURI() { + return predicate; + } + } + + /** + * Create a new Access Control Resource. + * + * @param identifier the resource identifier + * @param dataset the underlying dataset for the resource + */ + public AccessControlResource(final URI identifier, final Dataset dataset) { + super(identifier, dataset); + dataset.add(null, asIRI(identifier), asIRI(RDF.type), asIRI(ACP.AccessControlResource)); + } + + /** + * Retrieve the acp:accessControl structures. + * + *

accessControl resources are applied (non-recursively) to a container or resource. + * + * @return a collection of {@link AccessControl} objects + */ + public Set accessControl() { + return new ACPNode(asIRI(getIdentifier()), getGraph()).accessControl(); + } + + /** + * Retrieve the acp:memberAccessControl structures. + * + *

memberAccessControl resources are applied recursively to a containment hierarchy. + * + * @return a collection of {@link AccessControl} objects + */ + public Set memberAccessControl() { + return new ACPNode(asIRI(getIdentifier()), getGraph()).memberAccessControl(); + } + + /** + * Expand the internal data using a synchronous client. + * + * @param client the solid client + * @return an expanded access control resource + */ + public AccessControlResource expand(final SolidSyncClient client) { + // Copy the data from the existing ACR into a new dataset + final var dataset = rdf.createDataset(); + stream().forEach(dataset::add); + + try (final var cache = rdf.createDataset()) { + for (final var property : EXPANSION_PROPERTIES) { + expandType(dataset, cache, asIRI(property), EXPANSION_MAPPINGS.get(property), + uri -> populateCache(client, uri, cache)); + } + } catch (final Exception ex) { + LOGGER.atDebug().setMessage("Unable to close dataset: {}").addArgument(ex::getMessage).log(); + } + + return new AccessControlResource(getIdentifier(), dataset); + } + + /** + * Expand the internal data using an asynchronous client. + * + * @param client the solid client + * @return an expanded access control resource + */ + public AccessControlResource expand(final SolidClient client) { + // Copy the data from the existing ACR into a new dataset + final var dataset = rdf.createDataset(); + stream().forEach(dataset::add); + + try (final var cache = rdf.createDataset()) { + for (final var property : EXPANSION_PROPERTIES) { + expandType(dataset, cache, asIRI(property), EXPANSION_MAPPINGS.get(property), + uri -> populateCacheAsync(client, uri, cache)); + } + } catch (final Exception ex) { + LOGGER.atDebug().setMessage("Unable to close dataset: {}").addArgument(ex::getMessage).log(); + } + + return new AccessControlResource(getIdentifier(), dataset); + } + + /** + * Compact the internal data. + */ + public void compact() { + final var accessControls = stream(null, null, asIRI(RDF.type), asIRI(ACP.AccessControl)) + .map(Quad::getSubject).toList(); + for (final var accessControl : accessControls) { + removeUnusedStatements(accessControl); + } + + final var policies = stream(null, null, asIRI(RDF.type), asIRI(ACP.Policy)).map(Quad::getSubject).toList(); + for (final var policy : policies) { + removeUnusedStatements(policy); + } + + final var matchers = stream(null, null, asIRI(RDF.type), asIRI(ACP.Matcher)).map(Quad::getSubject).toList(); + for (final var matcher : matchers) { + removeUnusedStatements(matcher); + } + } + + /** + * Merge two or more policies into a single policies with combined matchers. + * + * @param allow the modes to allow + * @param policies the policies to merge + * @return the merged policy + */ + public Policy merge(final Set allow, final Policy... policies) { + final var baseUri = getIdentifier().getScheme() + ":" + getIdentifier().getSchemeSpecificPart(); + final var policy = new Policy(asIRI(baseUri + "#" + UUID.randomUUID()), getGraph()); + for (final var p : policies) { + policy.allOf().addAll(p.allOf()); + policy.anyOf().addAll(p.anyOf()); + policy.noneOf().addAll(p.noneOf()); + } + policy.allow().addAll(allow); + return policy; + } + + /** + * Find a policy, given a type, value and set of modes. + * + * @param type the matcher type + * @param value the matcher value, may be {@code null} + * @param modes the expected modes of the enclosing policy, may be {@code null} + * @return the matched policies + */ + public Set find(final MatcherType type, final URI value, final Set modes) { + final IRI matcherValue = value != null ? asIRI(value) : null; + final Set matcherModes = modes != null ? modes : Collections.emptySet(); + return stream(null, null, type.asIRI(), matcherValue) + .map(Quad::getSubject) + .flatMap(matcher -> stream(null, null, null, matcher)) + .map(Quad::getSubject) + .filter(policy -> contains(null, policy, asIRI(RDF.type), asIRI(ACP.Policy))) + .filter(policy -> stream(null, policy, asIRI(ACP.allow), null) + .map(Quad::getObject).filter(IRI.class::isInstance).map(IRI.class::cast) + .map(IRI::getIRIString).map(URI::create).toList().containsAll(matcherModes)) + .map(policy -> new Policy(policy, getGraph())) + .collect(Collectors.toSet()); + } + + /** + * Add policies to the access control resource. + * + * @param policies the policies to add + * @return the access control structure + */ + public AccessControl accessControl(final Policy... policies) { + final var baseUri = getIdentifier().getScheme() + ":" + getIdentifier().getSchemeSpecificPart(); + final var ac = new AccessControl(asIRI(baseUri + "#" + UUID.randomUUID()), getGraph()); + for (final var policy : policies) { + ac.apply().add(policy); + } + return ac; + } + + /** + * Create a policy that matches authenticated agents. + * + * @param access the access levels, such as Read or Write + * @return the new policy + */ + public Policy authenticatedAgentPolicy(final URI... access) { + return agentPolicy(ACP.AuthenticatedAgent, access); + } + + /** + * Create a policy that matches all agents. + * + * @param access the access levels, such as Read or Write + * @return the new policy + */ + public Policy anyAgentPolicy(final URI... access) { + return agentPolicy(ACP.PublicAgent, access); + } + + /** + * Create a policy that matches all clients. + * + * @param access the access levels, such as Read or Write + * @return the new policy + */ + public Policy anyClientPolicy(final URI... access) { + return clientPolicy(ACP.PublicClient, access); + } + + /** + * Create a policy that matches all issuers. + * + * @param access the access levels, such as Read or Write + * @return the new policy + */ + public Policy anyIssuerPolicy(final URI... access) { + return issuerPolicy(ACP.PublicIssuer, access); + } + + /** + * Create a policy that matches access grants. + * + * @param access the access levels, such as Read or Write + * @return the new policy + */ + public Policy accessGrantsPolicy(final URI... access) { + return simplePolicy(matcher -> matcher.vc().add(SOLID_ACCESS_GRANT), access); + } + + /** + * Create a policy that matches a particular agent. + * + * @param agent the agent identifier + * @param access the access levels, such as Read or Write + * @return the new policy + */ + public Policy agentPolicy(final URI agent, final URI... access) { + return simplePolicy(matcher -> matcher.agent().add(agent), access); + } + + /** + * Create a policy that matches a particular client. + * + * @param client the client identifier + * @param access the access levels, such as Read or Write + * @return the new policy + */ + public Policy clientPolicy(final URI client, final URI... access) { + return simplePolicy(matcher -> matcher.client().add(client), access); + } + + /** + * Create a policy that matches a particular issuer. + * + * @param issuer the issuer identifier + * @param access the access levels, such as Read or Write + * @return the new policy + */ + public Policy issuerPolicy(final URI issuer, final URI... access) { + return simplePolicy(matcher -> matcher.issuer().add(issuer), access); + } + + static class ACPNode extends WrapperIRI { + public ACPNode(final RDFTerm original, final Graph graph) { + super(original, graph); + } + + public Set memberAccessControl() { + return objects(asIRI(ACP.memberAccessControl), + AccessControl::asResource, ValueMappings.as(AccessControl.class)); + } + + public Set accessControl() { + return objects(asIRI(ACP.accessControl), + AccessControl::asResource, ValueMappings.as(AccessControl.class)); + } + } + + Policy simplePolicy(final Consumer handler, final URI... access) { + final var baseUri = getIdentifier().getScheme() + ":" + getIdentifier().getSchemeSpecificPart(); + final var matcher = new Matcher(asIRI(baseUri + "#" + UUID.randomUUID()), getGraph()); + handler.accept(matcher); + + final var policy = new Policy(asIRI(baseUri + "#" + UUID.randomUUID()), getGraph()); + for (final var item : access) { + policy.allow().add(item); + } + policy.allOf().add(matcher); + return policy; + } + + void removeUnusedStatements(final T resource) { + if (!contains(null, null, null, resource)) { + for (final var quad : stream(null, resource, null, null).toList()) { + remove(quad); + } + } + } + + void expandType(final Dataset dataset, final Dataset cache, final IRI predicate, final IRI type, + final Consumer handler) { + final var subjects = dataset.stream(null, null, predicate, null) + .map(Quad::getObject).filter(IRI.class::isInstance).map(IRI.class::cast) + .filter(subject -> !dataset.contains(null, subject, asIRI(RDF.type), type)).toList(); + + for (final var subject : subjects) { + if (!cache.contains(null, subject, asIRI(RDF.type), type)) { + handler.accept(URI.create(subject.getIRIString())); + } + cache.stream(null, subject, null, null).forEach(dataset::add); + } + } + + void populateCache(final SolidSyncClient client, final URI uri, final Dataset cache) { + try (final var acr = client.read(uri, AccessControlResource.class)) { + acr.stream() + .filter(quad -> !isAccessControlResourceType(quad)) + .forEach(cache::add); + } catch (final Exception ex) { + LOGGER.atDebug() + .setMessage("Unable to fetch access control resource from {}: {}") + .addArgument(uri) + .addArgument(ex::getMessage) + .log(); + } + } + + void populateCacheAsync(final SolidClient client, final URI uri, final Dataset cache) { + client.read(uri, AccessControlResource.class).thenAccept(res -> { + try (final var acr = res) { + acr.stream() + .filter(quad -> !isAccessControlResourceType(quad)) + .forEach(cache::add); + } + }) + .exceptionally(err -> { + LOGGER.atDebug() + .setMessage("Unable to fetch access control resource from {}: {}") + .addArgument(uri) + .addArgument(err::getMessage) + .log(); + return null; + }).toCompletableFuture().join(); + } + + static boolean isAccessControlResourceType(final Quad quad) { + return asIRI(RDF.type).equals(quad.getPredicate()) && asIRI(ACP.AccessControlResource).equals(quad.getObject()); + } + + static IRI asIRI(final URI uri) { + return asIRI(uri.toString()); + } + + static IRI asIRI(final String uri) { + return rdf.createIRI(uri); + } +} diff --git a/acp/src/main/java/com/inrupt/client/acp/Matcher.java b/acp/src/main/java/com/inrupt/client/acp/Matcher.java new file mode 100644 index 00000000000..58de0310f67 --- /dev/null +++ b/acp/src/main/java/com/inrupt/client/acp/Matcher.java @@ -0,0 +1,107 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.acp; + +import static com.inrupt.client.acp.AccessControlResource.asIRI; + +import com.inrupt.client.vocabulary.ACP; +import com.inrupt.client.vocabulary.RDF; +import com.inrupt.rdf.wrapping.commons.TermMappings; +import com.inrupt.rdf.wrapping.commons.ValueMappings; +import com.inrupt.rdf.wrapping.commons.WrapperIRI; + +import java.net.URI; +import java.util.Set; + +import org.apache.commons.rdf.api.Graph; +import org.apache.commons.rdf.api.IRI; +import org.apache.commons.rdf.api.RDFTerm; + +/** + * A Matcher type for use with Access Control Policies. + * + *

A matcher is associated with {@link Policy} objects, defining + * agents, clients, issuers or verifiable credential (vc) types. + */ +public class Matcher extends WrapperIRI { + + /** + * Create a new Matcher. + * + * @param identifier the matcher identifier + * @param graph the underlying graph + */ + public Matcher(final RDFTerm identifier, final Graph graph) { + super(identifier, graph); + graph.add((IRI) identifier, asIRI(RDF.type), asIRI(ACP.Matcher)); + } + + + /** + * Retrieve the acp:vc values. + * + * @return a collection of verifiable credential types + */ + public Set vc() { + return objects(asIRI(ACP.vc), TermMappings::asIri, ValueMappings::iriAsUri); + } + + /** + * Retrieve the acp:agent values. + * + * @return a collection of agent identifiers + */ + public Set agent() { + return objects(asIRI(ACP.agent), TermMappings::asIri, ValueMappings::iriAsUri); + } + + /** + * Retrieve the acp:client values. + * + * @return a collection of client identifiers + */ + public Set client() { + return objects(asIRI(ACP.client), TermMappings::asIri, ValueMappings::iriAsUri); + } + + /** + * Retrieve the acp:issuer values. + * + * @return a collection of issuer identifiers + */ + public Set issuer() { + return objects(asIRI(ACP.issuer), TermMappings::asIri, ValueMappings::iriAsUri); + } + + static RDFTerm asResource(final Matcher matcher, final Graph graph) { + graph.add(matcher, asIRI(RDF.type), asIRI(ACP.Matcher)); + matcher.vc().forEach(vc -> + graph.add(matcher, asIRI(ACP.vc), asIRI(vc))); + matcher.agent().forEach(agent -> + graph.add(matcher, asIRI(ACP.agent), asIRI(agent))); + matcher.client().forEach(client -> + graph.add(matcher, asIRI(ACP.client), asIRI(client))); + matcher.issuer().forEach(issuer -> + graph.add(matcher, asIRI(ACP.issuer), asIRI(issuer))); + return matcher; + } +} + diff --git a/acp/src/main/java/com/inrupt/client/acp/Policy.java b/acp/src/main/java/com/inrupt/client/acp/Policy.java new file mode 100644 index 00000000000..3f57945e29a --- /dev/null +++ b/acp/src/main/java/com/inrupt/client/acp/Policy.java @@ -0,0 +1,116 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.acp; + +import static com.inrupt.client.acp.AccessControlResource.asIRI; + +import com.inrupt.client.vocabulary.ACP; +import com.inrupt.client.vocabulary.RDF; +import com.inrupt.rdf.wrapping.commons.TermMappings; +import com.inrupt.rdf.wrapping.commons.ValueMappings; +import com.inrupt.rdf.wrapping.commons.WrapperIRI; + +import java.net.URI; +import java.util.Set; + +import org.apache.commons.rdf.api.Graph; +import org.apache.commons.rdf.api.IRI; +import org.apache.commons.rdf.api.RDFTerm; + +/** + * A Policy type for use with Access Control Policies. + * + *

A policy will reference various {@link Matcher} objects + * and apply access rules such as {@code Read} or {@code Write}. + */ +public class Policy extends WrapperIRI { + + /** + * Create a new Policy. + * + * @param identifier the policy identifier + * @param graph the underlying graph for this resource + */ + public Policy(final RDFTerm identifier, final Graph graph) { + super(identifier, graph); + graph.add((IRI) identifier, asIRI(RDF.type), asIRI(ACP.Policy)); + } + + /** + * Retrieve the acp:allOf structures. + * + * @return a collection of {@link Matcher} objects + */ + public Set allOf() { + return objects(asIRI(ACP.allOf), + Matcher::asResource, ValueMappings.as(Matcher.class)); + } + + /** + * Retrieve the acp:anyOf structures. + * + * @return a collection of {@link Matcher} objects + */ + public Set anyOf() { + return objects(asIRI(ACP.anyOf), + Matcher::asResource, ValueMappings.as(Matcher.class)); + } + + /** + * Retrieve the acp:noneOf structures. + * + * @return a collection of {@link Matcher} objects + */ + public Set noneOf() { + return objects(asIRI(ACP.noneOf), + Matcher::asResource, ValueMappings.as(Matcher.class)); + } + + /** + * Retrieve the acp:allow values. + * + * @return a collection of access values, such as {@code ACL.Read} + */ + public Set allow() { + return objects(asIRI(ACP.allow), + TermMappings::asIri, ValueMappings::iriAsUri); + } + + static IRI asResource(final Policy policy, final Graph graph) { + graph.add(policy, asIRI(RDF.type), asIRI(ACP.Policy)); + policy.allOf().forEach(matcher -> { + graph.add(policy, asIRI(ACP.allOf), matcher); + Matcher.asResource(matcher, graph); + }); + policy.anyOf().forEach(matcher -> { + graph.add(policy, asIRI(ACP.anyOf), matcher); + Matcher.asResource(matcher, graph); + }); + policy.noneOf().forEach(matcher -> { + graph.add(policy, asIRI(ACP.noneOf), matcher); + Matcher.asResource(matcher, graph); + }); + policy.allow().forEach(allow -> + graph.add(policy, asIRI(ACP.allow), asIRI(allow))); + return policy; + } +} + diff --git a/acp/src/main/java/com/inrupt/client/acp/package-info.java b/acp/src/main/java/com/inrupt/client/acp/package-info.java new file mode 100644 index 00000000000..5b96b0cf47d --- /dev/null +++ b/acp/src/main/java/com/inrupt/client/acp/package-info.java @@ -0,0 +1,51 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +/** + *

Access Control Policy support for the Inrupt Java Client Libraries.

+ * + *

This module contains classes and methods to convert ACP resources into + * a @{@link AccessControlResource} Java object. + * + *

The following example reads a Solid Access Control Resource and presents it as an {@link AccessControlResource} + * Java object: + * + *

{@code
+ *      try (AccessControlResource acr = client.read(uri, AccessControlResource.class)) {
+ *          // find policies that grant {@code agent} read access
+ *          Set policies = acr.expand(client).find(MatcherType.AGENT, agent, Set.of(ACL.Read));
+ *
+ *          // remove these policies from the ACR
+ *          Set accessControls = new HashSet<>();
+ *          accessControls.addAll(acr.accessControl());
+ *          accessControls.addAll(acr.memberAccessControl());
+ *          for (Policy p : policies) {
+ *              for (AccessControl accessControl = accessControls) {
+ *                  for (Policy policy : accessControl.apply()) {
+ *                      policy.remove(p);
+ *                  }
+ *              }
+ *          }
+ *          acr.compact();
+ *          client.update(acr);
+ *      }}
+ *  
+ */ +package com.inrupt.client.acp; diff --git a/acp/src/test/java/com/inrupt/client/acp/AccessControlResourceTest.java b/acp/src/test/java/com/inrupt/client/acp/AccessControlResourceTest.java new file mode 100644 index 00000000000..bf7afcd0ec4 --- /dev/null +++ b/acp/src/test/java/com/inrupt/client/acp/AccessControlResourceTest.java @@ -0,0 +1,392 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.acp; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.*; + +import com.inrupt.client.solid.SolidClient; +import com.inrupt.client.solid.SolidSyncClient; +import com.inrupt.client.spi.RDFFactory; +import com.inrupt.client.vocabulary.ACL; + +import java.io.IOException; +import java.net.URI; +import java.util.Set; + +import org.apache.commons.rdf.api.RDF; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class AccessControlResourceTest { + + static final AcpMockHttpService mockHttpServer = new AcpMockHttpService(); + static final SolidSyncClient client = SolidSyncClient.getClient(); + static final RDF rdf = RDFFactory.getInstance(); + + @BeforeAll + static void setup() { + mockHttpServer.start(); + } + + @AfterAll + static void teardown() { + mockHttpServer.stop(); + } + + @Test + void testAcr1CheckValues() { + final var uri = mockHttpServer.acr1(); + try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) { + assertEquals(2, acr.accessControl().size()); + assertEquals(2, acr.memberAccessControl().size()); + + final var matchers1 = acr.accessControl().stream() + .flatMap(ac -> ac.apply().stream()) + .filter(policy -> + policy.allow().contains(ACL.Write) && policy.allow().size() == 2) + .flatMap(policy -> policy.allOf().stream()) + .toList(); + assertEquals(1, matchers1.size()); + assertTrue(matchers1.get(0).agent().contains(URI.create("https://id.example/user"))); + + final var matchers2 = acr.accessControl().stream() + .flatMap(ac -> ac.apply().stream()) + .filter(policy -> + !policy.allow().contains(ACL.Write) && policy.allow().size() == 1) + .flatMap(policy -> policy.allOf().stream()) + .toList(); + assertEquals(1, matchers2.size()); + assertTrue(matchers2.get(0).vc().contains(URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant"))); + + final var matchers3 = acr.memberAccessControl().stream() + .flatMap(ac -> ac.apply().stream()) + .filter(policy -> + policy.allow().contains(ACL.Write) && policy.allow().size() == 2) + .flatMap(policy -> policy.allOf().stream()) + .toList(); + assertEquals(1, matchers3.size()); + assertTrue(matchers3.get(0).agent().contains(URI.create("https://id.example/user"))); + + final var matchers4 = acr.memberAccessControl().stream() + .flatMap(ac -> ac.apply().stream()) + .filter(policy -> + !policy.allow().contains(ACL.Write) && policy.allow().size() == 1) + .flatMap(policy -> policy.allOf().stream()) + .toList(); + assertEquals(1, matchers4.size()); + assertTrue(matchers4.get(0).client().contains(URI.create("https://app.example/id"))); + assertTrue(matchers4.get(0).vc().contains(URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant"))); + } + } + + @Test + void testAcr1Mutation() throws IOException { + final var uri = mockHttpServer.acr1(); + try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) { + final var quads = acr.stream().toList(); + + acr.accessControl().forEach(ac -> + ac.apply().forEach(policy -> policy.allow().add(ACL.Append))); + assertEquals(quads.size(), acr.size() - 2); + + try (var entity = acr.getEntity()) { + assertTrue(new String(entity.readAllBytes(), UTF_8).contains(ACL.Append.toString())); + } + + acr.accessControl().forEach(ac -> + ac.apply().forEach(policy -> policy.allow().remove(ACL.Write))); + assertEquals(quads.size(), acr.size() - 1); + } + } + + @Test + void testAcr2CheckValues() { + final var uri = mockHttpServer.acr2(); + try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) { + assertEquals(2, acr.accessControl().size()); + assertEquals(3, acr.memberAccessControl().size()); + + final var matchers1 = acr.accessControl().stream() + .flatMap(ac -> ac.apply().stream()) + .filter(policy -> + policy.allow().contains(ACL.Write) && policy.allow().size() == 2) + .flatMap(policy -> policy.allOf().stream()) + .toList(); + assertEquals(1, matchers1.size()); + assertTrue(matchers1.get(0).agent().contains(URI.create("https://id.example/user2"))); + + final var matchers2 = acr.accessControl().stream() + .flatMap(ac -> ac.apply().stream()) + .filter(policy -> + !policy.allow().contains(ACL.Write) && policy.allow().size() == 1) + .flatMap(policy -> policy.allOf().stream()) + .toList(); + assertEquals(1, matchers2.size()); + assertTrue(matchers2.get(0).agent().contains(URI.create("https://bot.example/id"))); + + final var matchers3 = acr.memberAccessControl().stream() + .flatMap(ac -> ac.apply().stream()) + .filter(policy -> + !policy.allow().contains(ACL.Write) && policy.allow().size() == 1) + .flatMap(policy -> policy.allOf().stream()) + .toList(); + assertEquals(2, matchers3.size()); + assertTrue(matchers3.stream().anyMatch(matcher -> + matcher.vc().contains(URI.create("http://www.w3.org/ns/solid/vc#SolidAccessGrant")))); + assertTrue(matchers3.stream().anyMatch(matcher -> + matcher.agent().contains(URI.create("https://bot.example/id")))); + } + } + + @Test + void buildAcr() { + final var identifier = "https://data.example/resource"; + final var dataset = rdf.createDataset(); + final var uri = URI.create(identifier); + + final var matcher1 = new Matcher(rdf.createIRI(identifier + "#matcher1"), rdf.createGraph()); + matcher1.agent().add(URI.create("https://id.example/agent")); + + final var matcher2 = new Matcher(rdf.createIRI(identifier + "#matcher2"), rdf.createGraph()); + matcher2.client().add(URI.create("https://app.example/id")); + + final var matcher3 = new Matcher(rdf.createIRI(identifier + "#matcher3"), rdf.createGraph()); + matcher3.issuer().add(URI.create("https://openid.example")); + + final var policy = new Policy(rdf.createIRI(identifier + "#policy"), rdf.createGraph()); + policy.allOf().add(matcher1); + policy.anyOf().add(matcher2); + policy.noneOf().add(matcher3); + policy.allow().add(ACL.Read); + policy.allow().add(ACL.Write); + + final var accessControl = new AccessControl(rdf.createIRI(identifier + "#access-control"), rdf.createGraph()); + accessControl.apply().add(policy); + + final var acr = new AccessControlResource(uri, dataset); + acr.accessControl().add(accessControl); + + assertEquals(16, acr.size()); + } + + @Test + void testAcrFindPolicies() { + final var uri = mockHttpServer.acr2(); + try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) { + assertEquals(1, acr.find(AccessControlResource.MatcherType.VC, + AccessControlResource.SOLID_ACCESS_GRANT, Set.of(ACL.Read)).size()); + + assertEquals(0, acr.find(AccessControlResource.MatcherType.VC, + AccessControlResource.SOLID_ACCESS_GRANT, Set.of(ACL.Read, ACL.Write)).size()); + + assertEquals(0, acr.find(AccessControlResource.MatcherType.AGENT, + URI.create("https://bot.example/id"), Set.of(ACL.Read, ACL.Write)).size()); + + assertEquals(1, acr.find(AccessControlResource.MatcherType.AGENT, + URI.create("https://bot.example/id"), Set.of(ACL.Read)).size()); + + assertEquals(2, acr.find(AccessControlResource.MatcherType.AGENT, + null, Set.of(ACL.Read)).size()); + + assertEquals(1, acr.find(AccessControlResource.MatcherType.AGENT, + URI.create("https://bot.example/id"), null).size()); + } + } + + @Test + void testAcr1RemoveAccessControl() { + final var uri = mockHttpServer.acr2(); + try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) { + assertEquals(2, acr.accessControl().size()); + assertEquals(3, acr.memberAccessControl().size()); + + // Check dataset size + assertEquals(29, acr.size()); + + acr.accessControl().stream().findFirst().ifPresent(acr.accessControl()::remove); + + // Check dataset size + assertEquals(28, acr.size()); + acr.compact(); + assertEquals(28, acr.size()); + + assertEquals(1, acr.accessControl().size()); + assertEquals(3, acr.memberAccessControl().size()); + + } + } + + @Test + void testAcr1RemoveValues() { + final var uri = mockHttpServer.acr2(); + try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) { + assertEquals(2, acr.accessControl().size()); + assertEquals(3, acr.memberAccessControl().size()); + + // Check dataset size + assertEquals(29, acr.size()); + + // Remove single matcher and compact + for (final var accessControl : acr.accessControl()) { + for (final var policy : accessControl.apply()) { + if (policy.allow().contains(ACL.Write) && policy.allow().size() == 2) { + for (final var matcher : policy.allOf()) { + policy.allOf().remove(matcher); + } + } + } + } + assertEquals(28, acr.size()); + acr.compact(); + assertEquals(26, acr.size()); + + // Remove single policy and compact + for (final var accessControl : acr.accessControl()) { + for (final var policy : accessControl.apply()) { + if (policy.allow().contains(ACL.Write) && policy.allow().size() == 2) { + accessControl.apply().remove(policy); + } + } + } + assertEquals(25, acr.size()); + acr.compact(); + assertEquals(22, acr.size()); + + // Remove single access control and compact - no compaction involved + final var ac = acr.accessControl().stream().findFirst().get(); + acr.accessControl().remove(ac); + assertEquals(21, acr.size()); + acr.compact(); + assertEquals(21, acr.size()); + } + } + + @Test + void buildAcrWithExistingPolicies() { + final var identifier = "https://data.example/resource"; + final var dataset = rdf.createDataset(); + final var uri = URI.create(identifier); + + final var acr = new AccessControlResource(uri, dataset); + acr.accessControl().add(acr.accessControl( + acr.authenticatedAgentPolicy(ACL.Read, ACL.Write), + acr.anyAgentPolicy(ACL.Read), + acr.anyClientPolicy(ACL.Read, ACL.Write))); + acr.memberAccessControl().add(acr.accessControl( + acr.anyIssuerPolicy(ACL.Read), + acr.accessGrantsPolicy(ACL.Read, ACL.Write))); + + assertEquals(38, acr.size()); + } + + @Test + void expandAcr3Sync() { + final var uri = mockHttpServer.acr3(); + try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) { + assertEquals(2, acr.accessControl().size()); + assertEquals(3, acr.memberAccessControl().size()); + + // Check dataset size + assertEquals(12, acr.size()); + + final var expanded = acr.expand(client); + assertEquals(29, expanded.size()); + assertEquals(12, acr.size()); + } + } + + @Test + void expandAcr3Async() { + final var uri = mockHttpServer.acr3(); + final var asyncClient = SolidClient.getClient(); + asyncClient.read(uri, AccessControlResource.class).thenAccept(res -> { + try (final var acr = res) { + assertEquals(2, acr.accessControl().size()); + assertEquals(3, acr.memberAccessControl().size()); + + // Check dataset size + assertEquals(12, acr.size()); + + final var expanded = acr.expand(asyncClient); + assertEquals(29, expanded.size()); + assertEquals(12, acr.size()); + } + }).toCompletableFuture().join(); + } + + @Test + void expandAcr4Sync() { + final var uri = mockHttpServer.acr4(); + try (final AccessControlResource acr = client.read(uri, AccessControlResource.class)) { + assertEquals(2, acr.accessControl().size()); + assertEquals(3, acr.memberAccessControl().size()); + + // Check dataset size + assertEquals(20, acr.size()); + + final var expanded = acr.expand(client); + assertEquals(22, expanded.size()); + assertEquals(20, acr.size()); + } + } + + @Test + void expandAcr4Async() { + final var uri = mockHttpServer.acr4(); + final var asyncClient = SolidClient.getClient(); + asyncClient.read(uri, AccessControlResource.class).thenAccept(res -> { + try (final var acr = res) { + assertEquals(2, acr.accessControl().size()); + assertEquals(3, acr.memberAccessControl().size()); + + // Check dataset size + assertEquals(20, acr.size()); + + final var expanded = acr.expand(asyncClient); + assertEquals(22, expanded.size()); + assertEquals(20, acr.size()); + } + }).toCompletableFuture().join(); + } + + @Test + void testMerge() { + final var identifier = "https://data.example/resource"; + final var agent = URI.create("https://id.example/agent"); + final var app = URI.create("https://app.example/id"); + + final var acr = new AccessControlResource(URI.create(identifier), rdf.createDataset()); + final var policy = acr.merge(Set.of(ACL.Read, ACL.Write), acr.agentPolicy(agent), acr.clientPolicy(app)); + + assertEquals(2, policy.allow().size()); + assertTrue(policy.allow().contains(ACL.Read)); + assertTrue(policy.allow().contains(ACL.Write)); + + assertEquals(2, policy.allOf().size()); + assertEquals(0, policy.anyOf().size()); + assertEquals(0, policy.noneOf().size()); + + assertTrue(policy.allOf().stream().anyMatch(matcher -> matcher.client().contains(app))); + assertTrue(policy.allOf().stream().anyMatch(matcher -> matcher.agent().contains(agent))); + } +} diff --git a/acp/src/test/java/com/inrupt/client/acp/AcpMockHttpService.java b/acp/src/test/java/com/inrupt/client/acp/AcpMockHttpService.java new file mode 100644 index 00000000000..e15d587f014 --- /dev/null +++ b/acp/src/test/java/com/inrupt/client/acp/AcpMockHttpService.java @@ -0,0 +1,111 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.acp; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; + +import java.net.URI; + +public class AcpMockHttpService { + + private final WireMockServer wireMockServer; + + public AcpMockHttpService() { + wireMockServer = new WireMockServer(WireMockConfiguration.options().dynamicPort()); + } + + public URI acr1() { + return URI.create(wireMockServer.baseUrl() + "/acr-1"); + } + + public URI acr2() { + return URI.create(wireMockServer.baseUrl() + "/acr-2"); + } + + public URI acr3() { + return URI.create(wireMockServer.baseUrl() + "/acr-3"); + } + + public URI acr4() { + return URI.create(wireMockServer.baseUrl() + "/acr-4"); + } + + private void setupMocks() { + wireMockServer.stubFor(get(urlEqualTo("/acr-1")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/turtle") + .withBodyFile("acr-1.ttl") + ) + ); + wireMockServer.stubFor(get(urlEqualTo("/acr-2")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/turtle") + .withBodyFile("acr-2.ttl") + ) + ); + wireMockServer.stubFor(get(urlEqualTo("/acr-3")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/turtle") + .withBodyFile("acr-3.ttl") + ) + ); + wireMockServer.stubFor(get(urlEqualTo("/acr-4")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/turtle") + .withBodyFile("acr-4.ttl") + ) + ); + wireMockServer.stubFor(get(urlEqualTo("/not-an-acr")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/turtle") + .withBodyFile("not-an-acr.ttl") + ) + ); + wireMockServer.stubFor(get(urlEqualTo("/also-not-an-acr")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBodyFile("not-an-acr.json") + ) + ); + } + + public String start() { + wireMockServer.start(); + + setupMocks(); + + return wireMockServer.baseUrl(); + } + + public void stop() { + wireMockServer.stop(); + } +} + diff --git a/acp/src/test/resources/__files/acr-1.ttl b/acp/src/test/resources/__files/acr-1.ttl new file mode 100644 index 00000000000..4750ad5fd5f --- /dev/null +++ b/acp/src/test/resources/__files/acr-1.ttl @@ -0,0 +1,31 @@ +@prefix acp: . +@prefix acl: . +@prefix vc: . + +<> + a acp:AccessControlResource ; + acp:accessControl <#owner-access-control> , <#access-grants-access-control> ; + acp:memberAccessControl <#owner-access-control> , <#access-grants-access-control> . + +<#owner-access-control> + a acp:AccessControl ; + acp:apply <#owner-policy> . +<#owner-policy> + a acp:Policy ; + acp:allOf <#owner-matcher> ; + acp:allow acl:Read , acl:Write . +<#owner-matcher> + a acp:Matcher ; + acp:agent . + +<#access-grants-access-control> + a acp:AccessControl ; + acp:apply <#access-grants-policy> . +<#access-grants-policy> + a acp:Policy ; + acp:allOf <#access-grants-matcher> ; + acp:allow acl:Read . +<#access-grants-matcher> + a acp:Matcher ; + acp:client ; + acp:vc vc:SolidAccessGrant . diff --git a/acp/src/test/resources/__files/acr-2.ttl b/acp/src/test/resources/__files/acr-2.ttl new file mode 100644 index 00000000000..fb9497c424b --- /dev/null +++ b/acp/src/test/resources/__files/acr-2.ttl @@ -0,0 +1,42 @@ +@prefix acp: . +@prefix acl: . +@prefix vc: . + +<> + a acp:AccessControlResource ; + acp:accessControl <#owner-access-control> , <#indexer-access-control> ; + acp:memberAccessControl <#owner-access-control> , <#indexer-access-control> , <#vc-access-control> . + +<#owner-access-control> + a acp:AccessControl ; + acp:apply <#owner-policy> . +<#owner-policy> + a acp:Policy ; + acp:allOf <#owner-matcher> ; + acp:allow acl:Read , acl:Write . +<#owner-matcher> + a acp:Matcher ; + acp:agent . + +<#indexer-access-control> + a acp:AccessControl ; + acp:apply <#indexer-policy> . +<#indexer-policy> + a acp:Policy ; + acp:allOf <#indexer-matcher> ; + acp:allow acl:Read . +<#indexer-matcher> + a acp:Matcher ; + acp:issuer ; + acp:agent . + +<#vc-access-control> + a acp:AccessControl ; + acp:apply <#vc-policy> . +<#vc-policy> + a acp:Policy ; + acp:allOf <#vc-matcher> ; + acp:allow acl:Read . +<#vc-matcher> + a acp:Matcher ; + acp:vc vc:SolidAccessGrant . diff --git a/acp/src/test/resources/__files/acr-3.ttl b/acp/src/test/resources/__files/acr-3.ttl new file mode 100644 index 00000000000..8ffaeca57a8 --- /dev/null +++ b/acp/src/test/resources/__files/acr-3.ttl @@ -0,0 +1,18 @@ +@prefix acp: . + +<> + a acp:AccessControlResource ; + acp:accessControl <#owner-access-control> , <#indexer-access-control> ; + acp:memberAccessControl <#owner-access-control> , <#indexer-access-control> , <#vc-access-control> . + +<#owner-access-control> + a acp:AccessControl ; + acp:apply . + +<#indexer-access-control> + a acp:AccessControl ; + acp:apply . + +<#vc-access-control> + a acp:AccessControl ; + acp:apply . diff --git a/acp/src/test/resources/__files/acr-4.ttl b/acp/src/test/resources/__files/acr-4.ttl new file mode 100644 index 00000000000..eb277f5d394 --- /dev/null +++ b/acp/src/test/resources/__files/acr-4.ttl @@ -0,0 +1,31 @@ +@prefix acp: . +@prefix acl: . +@prefix vc: . + +<> + a acp:AccessControlResource ; + acp:accessControl <#owner-access-control> , <#indexer-access-control> ; + acp:memberAccessControl <#owner-access-control> , <#indexer-access-control> , <#vc-access-control> . + +<#owner-access-control> + a acp:AccessControl ; + acp:apply . + +<#indexer-access-control> + a acp:AccessControl ; + acp:apply <#indexer-policy> . +<#indexer-policy> + a acp:Policy ; + acp:noneOf ; + acp:allow acl:Read . + +<#vc-access-control> + a acp:AccessControl ; + acp:apply <#vc-policy> . +<#vc-policy> + a acp:Policy ; + acp:anyOf <#vc-matcher> ; + acp:allow acl:Read . +<#vc-matcher> + a acp:Matcher ; + acp:vc vc:SolidAccessGrant . diff --git a/acp/src/test/resources/__files/not-an-acr.json b/acp/src/test/resources/__files/not-an-acr.json new file mode 100644 index 00000000000..48272642adc --- /dev/null +++ b/acp/src/test/resources/__files/not-an-acr.json @@ -0,0 +1,4 @@ +{ + "type": "Type", + "title": "Example resource" +} diff --git a/acp/src/test/resources/__files/not-an-acr.ttl b/acp/src/test/resources/__files/not-an-acr.ttl new file mode 100644 index 00000000000..ad179e6536d --- /dev/null +++ b/acp/src/test/resources/__files/not-an-acr.ttl @@ -0,0 +1,7 @@ +@prefix dc: . +@prefix ex: . + +<> + a ex:Type ; + dc:title "Example resource" . + diff --git a/rdf-legacy/src/test/resources/simplelogger.properties b/acp/src/test/resources/simplelogger.properties similarity index 100% rename from rdf-legacy/src/test/resources/simplelogger.properties rename to acp/src/test/resources/simplelogger.properties diff --git a/api/pom.xml b/api/pom.xml index a5e2b0dece9..6a0eab5de73 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-api @@ -13,11 +13,6 @@ The Application Programming Interfaces for the Inrupt Java Client Libraries. - - 1.8 - 1.8 - - commons-io diff --git a/api/src/main/java/com/inrupt/client/Headers.java b/api/src/main/java/com/inrupt/client/Headers.java index ca03c39910c..0ed12afaf0f 100644 --- a/api/src/main/java/com/inrupt/client/Headers.java +++ b/api/src/main/java/com/inrupt/client/Headers.java @@ -23,6 +23,7 @@ import com.inrupt.client.auth.Challenge; import com.inrupt.client.spi.ServiceProvider; +import java.io.Serializable; import java.net.URI; import java.util.Arrays; import java.util.Collections; @@ -39,7 +40,9 @@ /** * A read-only view of a collection of HTTP headers. */ -public final class Headers { +public final class Headers implements Serializable { + + private static final long serialVersionUID = 3845207335727836025L; private final NavigableMap> data = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); diff --git a/api/src/main/java/com/inrupt/client/ProblemDetails.java b/api/src/main/java/com/inrupt/client/ProblemDetails.java new file mode 100644 index 00000000000..cab99569e63 --- /dev/null +++ b/api/src/main/java/com/inrupt/client/ProblemDetails.java @@ -0,0 +1,71 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client; + +import java.net.URI; + +/** + * A data class representing a structured problem description sent by the server on error response. + * + * @see RFC 9457 Problem Details for HTTP APIs + */ +public interface ProblemDetails { + + /** + * The RFC9457 default MIME type. + */ + String MIME_TYPE = "application/problem+json"; + + /** + * The RFC9457 default problem type. + */ + URI DEFAULT_TYPE = URI.create("about:blank"); + + /** + * The problem type. + * @return the type + */ + URI getType(); + + /** + * The problem title. + * @return the title + */ + String getTitle(); + + /** + * The problem detail. + * @return the detail + */ + String getDetail(); + + /** + * The problem status code. + * @return the status code + */ + int getStatus(); + + /** + * The problem instance. + * @return the instance + */ + URI getInstance(); +} diff --git a/api/src/main/java/com/inrupt/client/auth/Authenticator.java b/api/src/main/java/com/inrupt/client/auth/Authenticator.java index 5554a25e761..596f1bf0217 100644 --- a/api/src/main/java/com/inrupt/client/auth/Authenticator.java +++ b/api/src/main/java/com/inrupt/client/auth/Authenticator.java @@ -53,5 +53,4 @@ public interface Authenticator { * @return the next stage of completion, containing the access token */ CompletionStage authenticate(Session session, Request request, Set algorithms); - } diff --git a/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java b/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java index adb74908e41..40b91afd00e 100644 --- a/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java +++ b/api/src/main/java/com/inrupt/client/auth/ReactiveAuthorization.java @@ -119,7 +119,7 @@ public CompletionStage> negotiate(final Session session, fi // Use the first authenticator, sorted by priority authenticators.sort(comparator); final Authenticator auth = authenticators.get(0); - LOGGER.debug("Using {} authenticator", auth.getName()); + LOGGER.debug("Using {} authenticator", auth); return session.authenticate(auth, request, algorithms); } return CompletableFuture.completedFuture(Optional.empty()); diff --git a/api/src/main/java/com/inrupt/client/spi/ClientProviderResolver.java b/api/src/main/java/com/inrupt/client/spi/ClientProviderResolver.java index 646e26d332b..00df72c255d 100644 --- a/api/src/main/java/com/inrupt/client/spi/ClientProviderResolver.java +++ b/api/src/main/java/com/inrupt/client/spi/ClientProviderResolver.java @@ -30,7 +30,7 @@ */ public abstract class ClientProviderResolver { - private static ClientProviderResolver instance = null; + private static ClientProviderResolver instance; /** * Get the {@link Client} for this application. diff --git a/api/src/main/java/com/inrupt/client/spi/RDFFactory.java b/api/src/main/java/com/inrupt/client/spi/RDFFactory.java index ac8dbac9e54..0717a3548e2 100644 --- a/api/src/main/java/com/inrupt/client/spi/RDFFactory.java +++ b/api/src/main/java/com/inrupt/client/spi/RDFFactory.java @@ -30,7 +30,7 @@ */ public final class RDFFactory { - private static RDF instance = null; + private static RDF instance; /** * Find and return the RDF instance. diff --git a/api/src/site/site.xml b/api/src/site/site.xml index c3b68769338..d09cbb4a5dc 100644 --- a/api/src/site/site.xml +++ b/api/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/archetypes/java/pom.xml b/archetypes/java/pom.xml index 04d967987fc..587e3d34db7 100644 --- a/archetypes/java/pom.xml +++ b/archetypes/java/pom.xml @@ -3,7 +3,7 @@ com.inrupt.client inrupt-client-archetype-parent - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-archetype-java diff --git a/archetypes/java/src/main/resources/META-INF/maven/archetype-metadata.xml b/archetypes/java/src/main/resources/META-INF/maven/archetype-metadata.xml index 186a1409c22..230a0b12e1c 100644 --- a/archetypes/java/src/main/resources/META-INF/maven/archetype-metadata.xml +++ b/archetypes/java/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -4,12 +4,36 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.1.0 https://maven.apache.org/xsd/archetype-descriptor-1.1.0.xsd"> - - ${junit.version} + + ${maven.compiler.release} ${project.version} + + ${junit.version} + + + ${clean.plugin.version} + + + ${antrun.plugin.version} + + + ${assembly.plugin.version} + + + ${dependency.plugin.version} + + + ${install.plugin.version} + + + ${jar.plugin.version} + + + ${resources.plugin.version} + diff --git a/archetypes/java/src/main/resources/archetype-resources/pom.xml b/archetypes/java/src/main/resources/archetype-resources/pom.xml index 8eab3f6cf4d..0ec6a9f84ed 100644 --- a/archetypes/java/src/main/resources/archetype-resources/pom.xml +++ b/archetypes/java/src/main/resources/archetype-resources/pom.xml @@ -13,9 +13,8 @@ UTF-8 UTF-8 - 11 - 11 - 11 + true + ${java-version} @@ -102,41 +101,42 @@ org.apache.maven.plugins maven-clean-plugin - 3.2.0 + ${maven-clean-plugin} org.apache.maven.plugins maven-antrun-plugin - 3.1.0 + ${maven-antrun-plugin} org.apache.maven.plugins maven-assembly-plugin - 3.6.0 + ${maven-assembly-plugin} org.apache.maven.plugins maven-dependency-plugin - 3.6.0 + ${maven-dependency-plugin} org.apache.maven.plugins maven-install-plugin - 3.1.1 + ${maven-install-plugin} org.apache.maven.plugins maven-jar-plugin - 3.3.0 + ${maven-jar-plugin} + org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.13.0 org.apache.maven.plugins maven-resources-plugin - 3.3.1 + ${maven-resources-plugin} UTF-8 UTF-8 diff --git a/archetypes/java/src/test/resources/projects/test/archetype.properties b/archetypes/java/src/test/resources/projects/test/archetype.properties index 345ee587eaa..844c3eb9d9c 100644 --- a/archetypes/java/src/test/resources/projects/test/archetype.properties +++ b/archetypes/java/src/test/resources/projects/test/archetype.properties @@ -3,5 +3,13 @@ artifactId=inrupt-client-test version=1.0-SNAPSHOT package=com.inrupt.test +java-version=${maven.compiler.release} inrupt-client-version=${project.version} junit-version=${junit.version} +maven-antrun-plugin=${antrun.plugin.version} +maven-assembly-plugin=${assembly.plugin.version} +maven-clean-plugin=${clean.plugin.version} +maven-dependency-plugin=${dependency.plugin.version} +maven-install-plugin=${install.plugin.version} +maven-jar-plugin=${jar.plugin.version} +maven-resources-plugin=${resources.plugin.version} diff --git a/archetypes/pom.xml b/archetypes/pom.xml index 1af37f66aaf..f007a615a3f 100644 --- a/archetypes/pom.xml +++ b/archetypes/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-archetype-parent diff --git a/bom/pom.xml b/bom/pom.xml index 7cad6aebe9f..93caa85987e 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -5,7 +5,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT .. @@ -28,6 +28,11 @@ inrupt-client-accessgrant ${project.version} + + com.inrupt.client + inrupt-client-acp + ${project.version} + com.inrupt.client inrupt-client-caffeine @@ -88,11 +93,6 @@ inrupt-client-rdf4j ${project.version} - - com.inrupt.client - inrupt-client-rdf-legacy - ${project.version} - com.inrupt.client inrupt-client-runtime @@ -125,11 +125,6 @@ - - com.squareup.okhttp3 - okhttp - ${okhttp.version} - org.antlr antlr4-runtime diff --git a/bom/src/site/site.xml b/bom/src/site/site.xml index 86cb644d20e..373c6f66a23 100644 --- a/bom/src/site/site.xml +++ b/bom/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/build-tools/owasp/suppressions.xml b/build-tools/owasp/suppressions.xml deleted file mode 100644 index 3ee1ade313a..00000000000 --- a/build-tools/owasp/suppressions.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - ^pkg:maven/com\.inrupt\.client/inrupt\-client\-openid@.*$ - cpe:/a:openid:openid - - - - - - ^pkg:maven/com\.jayway\.jsonpath/json\-path@.*$ - CVE-2023-51074 - - - - ^pkg:maven/org\.eclipse\.jetty\.http2/http2\-.*@.*$ - CVE-2023-44487 - - - - ^pkg:maven/org\.eclipse\.jetty/jetty\-.*@.*$ - CVE-2023-44487 - - diff --git a/build-tools/pmd/pmd.xml b/build-tools/pmd/pmd.xml index 47c158c6c08..07c7c0126b0 100644 --- a/build-tools/pmd/pmd.xml +++ b/build-tools/pmd/pmd.xml @@ -18,7 +18,7 @@ - + diff --git a/caffeine/pom.xml b/caffeine/pom.xml index ca1e146bbdd..c92c7560d27 100644 --- a/caffeine/pom.xml +++ b/caffeine/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-caffeine diff --git a/core/pom.xml b/core/pom.xml index 58bb19c1b12..cb53a29d8f8 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-core @@ -13,11 +13,6 @@ Core Classes and Utilities for the Inrupt Java Client Libraries. - - 1.8 - 1.8 - - diff --git a/core/src/site/site.xml b/core/src/site/site.xml index e0110dd5c3d..40f6f0db2ec 100644 --- a/core/src/site/site.xml +++ b/core/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/examples/cli/pom.xml b/examples/cli/pom.xml index b8bdf8ad03f..8c2843c8d31 100644 --- a/examples/cli/pom.xml +++ b/examples/cli/pom.xml @@ -5,27 +5,24 @@ com.inrupt.client inrupt-client-examples-parent - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-examples-cli - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT Inrupt Java Client Libraries - CLI Example Sample CLI application. - 17 - 17 - - 3.6.5 + 3.37.0 - io.quarkus + io.quarkus.platform quarkus-bom ${quarkus.version} pom @@ -104,7 +101,7 @@ - io.quarkus + io.quarkus.platform quarkus-maven-plugin ${quarkus.version} true diff --git a/examples/cli/src/main/java/com/inrupt/client/examples/cli/SolidApp.java b/examples/cli/src/main/java/com/inrupt/client/examples/cli/SolidApp.java index b799da3cb77..d1b91fbc847 100644 --- a/examples/cli/src/main/java/com/inrupt/client/examples/cli/SolidApp.java +++ b/examples/cli/src/main/java/com/inrupt/client/examples/cli/SolidApp.java @@ -35,13 +35,14 @@ import jakarta.inject.Inject; +import java.io.IOException; import java.io.PrintWriter; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import org.apache.commons.cli.help.HelpFormatter; import org.slf4j.Logger; /** @@ -127,9 +128,12 @@ boolean filterResource(final SolidSyncClient client, final SolidResource resourc } void showHelp(final Options options) { - final var formatter = new HelpFormatter(); - formatter.printHelp(printWriter, formatter.getWidth(), "java -jar inrupt-openid-jwk-runner.jar", - null, options, formatter.getLeftPadding(), formatter.getDescPadding(), null, false); + final var formatter = HelpFormatter.builder().get(); + try { + formatter.printHelp("java -jar inrupt-client-examples-cli-runner.jar", null, options, null, false); + } catch (IOException ex) { + LOGGER.error("Failed to print help output: {}", ex.getMessage()); + } } } diff --git a/examples/cli/src/site/site.xml b/examples/cli/src/site/site.xml index 18e2903fced..c33fcc629b8 100644 --- a/examples/cli/src/site/site.xml +++ b/examples/cli/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/examples/pom.xml b/examples/pom.xml index 4aa68999797..dd2afae85cc 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-examples-parent @@ -14,37 +14,29 @@ pom + + cli + springboot + spring-web + webapp + + - org.owasp - dependency-check-maven - ${owasp.plugin.version} + org.apache.maven.plugins + maven-deploy-plugin true org.apache.maven.plugins - maven-deploy-plugin + maven-javadoc-plugin true - - - - java-17 - - [17,) - - - cli - springboot - webapp - - - diff --git a/examples/spring-web/pom.xml b/examples/spring-web/pom.xml new file mode 100644 index 00000000000..0597b9e40ac --- /dev/null +++ b/examples/spring-web/pom.xml @@ -0,0 +1,133 @@ + + + 4.0.0 + + + com.inrupt.client + inrupt-client-examples-parent + 2.0.1-SNAPSHOT + + + inrupt-client-examples-spring-web + 2.0.1-SNAPSHOT + Inrupt Java Client Libraries - SpringBoot WebApp Example + + Sample SpringBoot Web Application. + + + + 4.1.0 + + + + + + org.springframework.boot + spring-boot-dependencies + ${springboot.version} + pom + import + + + + + + + com.inrupt.client + inrupt-client-runtime + ${project.version} + + + com.inrupt.client + inrupt-client-spring + ${project.version} + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + + + com.nimbusds + nimbus-jose-jwt + 10.9.1 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-restclient + test + + + org.springframework.boot + spring-boot-resttestclient + test + + + org.springframework.security + spring-security-test + test + + + io.smallrye + smallrye-jwt-build + ${smallrye.jwt.version} + test + + + io.smallrye.config + smallrye-config + ${smallrye.config.version} + test + + + org.wiremock + wiremock-standalone + ${wiremock.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + WARN + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${springboot.version} + + + + repackage + + + + + + + diff --git a/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/SecurityConfiguration.java b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/SecurityConfiguration.java new file mode 100644 index 00000000000..d2a78f8d50c --- /dev/null +++ b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/SecurityConfiguration.java @@ -0,0 +1,73 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.examples.spring.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenDecoderFactory; +import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.jwt.JwtDecoderFactory; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + + @Autowired + ClientRegistrationRepository clientRegistrationRepository; + + @Value("${spring.security.oidc.post-logout-redirect-uri}") + private String postLogoutRedirectUri; + + @Value("${spring.security.oidc.signature-algorithm}") + private SignatureAlgorithm signatureAlgorithm; + + @Bean + public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception { + http + .logout(logout -> logout + .logoutSuccessHandler(oidcLogoutSuccessHandler()) + ) + .oauth2Login(Customizer.withDefaults()); + return http.build(); + } + + @Bean + public JwtDecoderFactory idTokenDecoderFactory() { + final var decoder = new OidcIdTokenDecoderFactory(); + decoder.setJwsAlgorithmResolver(clientRegistration -> signatureAlgorithm); + return decoder; + } + + OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler() { + final var successHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository); + successHandler.setPostLogoutRedirectUri(postLogoutRedirectUri); + return successHandler; + } +} diff --git a/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/WebApplication.java b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/WebApplication.java new file mode 100644 index 00000000000..951e1b116de --- /dev/null +++ b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/WebApplication.java @@ -0,0 +1,33 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.examples.spring.web; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +// Spring Boot's class requirements conflict with the checkstyle rules in this case +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") +@SpringBootApplication +public class WebApplication { + public static void main(final String[] args) { + SpringApplication.run(WebApplication.class, args); + } +} diff --git a/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/controller/SolidController.java b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/controller/SolidController.java new file mode 100644 index 00000000000..4dd6d6b86c8 --- /dev/null +++ b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/controller/SolidController.java @@ -0,0 +1,96 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.examples.spring.web.controller; + +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.security.oauth2.core.OAuth2ErrorCodes.INVALID_TOKEN; + +import com.inrupt.client.auth.Session; +import com.inrupt.client.examples.spring.web.model.*; +import com.inrupt.client.openid.OpenIdException; +import com.inrupt.client.solid.SolidClientException; +import com.inrupt.client.solid.SolidRDFSource; +import com.inrupt.client.solid.SolidSyncClient; +import com.inrupt.client.spring.SessionUtils; +import com.inrupt.client.webid.WebIdProfile; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import java.io.IOException; +import java.net.URI; +import java.util.*; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.web.bind.annotation.*; + +@SpringBootApplication +@RestController +@RequestMapping("/api") +public class SolidController { + + private static final SolidSyncClient client = SolidSyncClient.getClient(); + + @Autowired + HttpSession httpSession; + + @GetMapping(value = "/resource", produces = "text/turtle") + public SolidResource getResource(final @AuthenticationPrincipal OAuth2User user, final @RequestParam URI uri) { + final var session = SessionUtils.asSession(user) + .orElseThrow(() -> new OAuth2AuthenticationException(INVALID_TOKEN)); + try (final var resource = client.session(session).read(uri, SolidRDFSource.class)) { + return new SolidResource(resource); + } + } + + @GetMapping(value = "/webid", produces = APPLICATION_JSON_VALUE) + public WebId getUser(final @AuthenticationPrincipal OAuth2User user) { + final var webid = SessionUtils.asSession(user).flatMap(Session::getPrincipal) + .orElseThrow(() -> new OAuth2AuthenticationException(INVALID_TOKEN)); + return new WebId(webid); + } + + @GetMapping(value = "/profile", produces = APPLICATION_JSON_VALUE) + public Profile getProfile(final @AuthenticationPrincipal OAuth2User user) { + final var session = SessionUtils.asSession(user) + .orElseThrow(() -> new OAuth2AuthenticationException(INVALID_TOKEN)); + final var webid = session.getPrincipal().orElseThrow(() -> new OAuth2AuthenticationException(INVALID_TOKEN)); + try (final var profile = client.session(session).read(webid, WebIdProfile.class)) { + return Profile.of(profile); + } + } + + @ExceptionHandler(SolidClientException.class) + public void clientException(final HttpServletResponse response) throws IOException { + httpSession.invalidate(); + response.sendError(400); + } + + @ExceptionHandler({OAuth2AuthenticationException.class, OpenIdException.class}) + public void sessionException(final HttpServletResponse response) throws IOException { + httpSession.invalidate(); + response.sendError(401); + } +} diff --git a/integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/CustomOkHttpResponse.java b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/model/Profile.java similarity index 58% rename from integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/CustomOkHttpResponse.java rename to examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/model/Profile.java index 772894a5ce4..8b021e09d4c 100644 --- a/integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/CustomOkHttpResponse.java +++ b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/model/Profile.java @@ -18,42 +18,15 @@ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.inrupt.client.integration.customokhttp; +package com.inrupt.client.examples.spring.web.model; -import com.inrupt.client.Headers; -import com.inrupt.client.Response; +import com.inrupt.client.webid.WebIdProfile; import java.net.URI; +import java.util.Set; -class CustomOkHttpResponse implements Response { - private final URI responseUri; - private final T responseBody; - private final Response.ResponseInfo info; - - public CustomOkHttpResponse(final URI uri, final Response.ResponseInfo info, final T body) { - this.responseUri = uri; - this.responseBody = body; - this.info = info; - } - - @Override - public T body() { - return responseBody; - } - - @Override - public Headers headers() { - return info.headers(); +public record Profile(URI id, Set storages, Set issuers) { + public static Profile of(final WebIdProfile profile) { + return new Profile(profile.getIdentifier(), profile.getStorages(), profile.getOidcIssuers()); } - - @Override - public URI uri() { - return responseUri; - } - - @Override - public int statusCode() { - return info.statusCode(); - } - } diff --git a/integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/CustomOkHttpResponseInfo.java b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/model/SolidResource.java similarity index 60% rename from integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/CustomOkHttpResponseInfo.java rename to examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/model/SolidResource.java index 2bb16a341f2..a87cf990a4a 100644 --- a/integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/CustomOkHttpResponseInfo.java +++ b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/model/SolidResource.java @@ -18,47 +18,38 @@ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.inrupt.client.integration.customokhttp; +package com.inrupt.client.examples.spring.web.model; -import com.inrupt.client.Headers; -import com.inrupt.client.Response.ResponseInfo; +import com.inrupt.client.RDFSource; import java.io.IOException; import java.io.UncheckedIOException; -import java.net.URI; -import java.nio.ByteBuffer; -import okhttp3.Response; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; -class CustomOkHttpResponseInfo implements ResponseInfo { +public class SolidResource extends HttpEntity { - private final Response response; + private final HttpHeaders headers = new HttpHeaders(); + private final RDFSource resource; - public CustomOkHttpResponseInfo(final Response response) { - this.response = response; + public SolidResource(final RDFSource resource) { + this.resource = resource; + this.headers.setContentType(MediaType.valueOf(resource.getContentType())); } @Override - public Headers headers() { - return Headers.of(response.headers().toMultimap()); - } - - @Override - public int statusCode() { - return response.code(); - } - - @Override - public URI uri() { - return response.request().url().uri(); - } - - @Override - public ByteBuffer body() { + public byte[] getBody() { try { - return ByteBuffer.wrap(response.body().bytes()); + return resource.getEntity().readAllBytes(); } catch (final IOException ex) { - throw new UncheckedIOException("Unable to handle response data", ex); + throw new UncheckedIOException(ex); } } + + @Override + public HttpHeaders getHeaders() { + return headers; + } } diff --git a/rdf-legacy/src/main/java/com/inrupt/client/rdf/legacy/package-info.java b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/model/WebId.java similarity index 89% rename from rdf-legacy/src/main/java/com/inrupt/client/rdf/legacy/package-info.java rename to examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/model/WebId.java index 29dd762ea69..3d77df21ddd 100644 --- a/rdf-legacy/src/main/java/com/inrupt/client/rdf/legacy/package-info.java +++ b/examples/spring-web/src/main/java/com/inrupt/client/examples/spring/web/model/WebId.java @@ -18,7 +18,9 @@ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -/** - *

Legacy (Java 8) RDF support for the Inrupt Java Client Libraries.

- */ -package com.inrupt.client.rdf.legacy; +package com.inrupt.client.examples.spring.web.model; + +import java.net.URI; + +public record WebId(URI id) { +} diff --git a/examples/spring-web/src/main/resources/application.yml b/examples/spring-web/src/main/resources/application.yml new file mode 100644 index 00000000000..7aee9692921 --- /dev/null +++ b/examples/spring-web/src/main/resources/application.yml @@ -0,0 +1,24 @@ +logging: + level: + root: info +spring: + main: + banner-mode: "off" + security: + oidc: + post-logout-redirect-uri: http://localhost:8080/ + signature-algorithm: ES256 + oauth2: + client: + registration: + myApp: + client-id: https://inrupt.github.io/solid-client-java/clients/springboot-testing.jsonld + provider: inrupt + scope: + - openid + - webid + client-authentication-method: none + authorization-grant-type: authorization_code + provider: + inrupt: + issuer-uri: https://login.inrupt.com diff --git a/examples/spring-web/src/main/resources/static/assets/script.js b/examples/spring-web/src/main/resources/static/assets/script.js new file mode 100644 index 00000000000..f39125d62be --- /dev/null +++ b/examples/spring-web/src/main/resources/static/assets/script.js @@ -0,0 +1,48 @@ +(async function() { + // Clear any state in the URL + history.pushState({}, "", "/"); + + // General API request handler + async function get(uri) { + const res = await fetch(uri); + if (res.ok) { + return res.headers.get("Content-Type") === "application/json" ? res.json() : res.text(); + } + throw new Error("Error fetching data"); + } + + // Specific API methods + function getUser() { + return get("/api/webid"); + } + function getProfile() { + return get("/api/profile"); + } + function getResource(uri) { + return get(`/api/resource?uri=${uri}`); + } + + try { + // Load the application data + const user = await getUser(); + document.getElementById("user").innerHTML = `logout`; + document.getElementById("webid").value = user.id; + + const profile = await getProfile(); + document.getElementById("storage").value = profile.storages[0]; + document.getElementById("load").disabled = false; + document.getElementById("load").addEventListener('click', async evt => { + document.getElementById("fetch-output").value = "Loading..."; + try { + const resource = await getResource(new URL(profile.storages[0])); + document.getElementById("fetch-output").value = resource; + } catch (error) { + // An error here means that the access token has timed out + window.location.href = "/"; + } + }); + } catch (error) { + // An error here means that the user is not logged in. In that case, display a login link. + document.getElementById("webid").innerHTML = `login`; + } +})(); diff --git a/examples/spring-web/src/main/resources/static/assets/styles.css b/examples/spring-web/src/main/resources/static/assets/styles.css new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/spring-web/src/main/resources/static/index.html b/examples/spring-web/src/main/resources/static/index.html new file mode 100644 index 00000000000..ba0b878bd21 --- /dev/null +++ b/examples/spring-web/src/main/resources/static/index.html @@ -0,0 +1,49 @@ + + + + + + + Authentication Demo + + + + +
+
+ +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+
+ + +
+
+
+
+
+ + + diff --git a/examples/spring-web/src/main/resources/templates/error.html b/examples/spring-web/src/main/resources/templates/error.html new file mode 100644 index 00000000000..32578ec61a0 --- /dev/null +++ b/examples/spring-web/src/main/resources/templates/error.html @@ -0,0 +1,28 @@ + + + + + + + Application Error + + + + +
+

Application Error ()

+

+ Page not found + + Sorry, an error occurred + +

+
+ + + diff --git a/examples/spring-web/src/test/java/com/inrupt/client/examples/spring/web/MockWebServer.java b/examples/spring-web/src/test/java/com/inrupt/client/examples/spring/web/MockWebServer.java new file mode 100644 index 00000000000..976949b2361 --- /dev/null +++ b/examples/spring-web/src/test/java/com/inrupt/client/examples/spring/web/MockWebServer.java @@ -0,0 +1,65 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.examples.spring.web; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; + +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; + +class MockWebServer { + + private final WireMockServer wireMockServer; + + public MockWebServer() { + wireMockServer = new WireMockServer(WireMockConfiguration.options() + .dynamicPort()); + } + + public void start() { + + wireMockServer.start(); + + wireMockServer.stubFor(get(urlEqualTo("/user")) + .willReturn(aResponse() + .withHeader("Content-Type", "text/turtle") + .withBody(""" + <> ; + ."""))); + + wireMockServer.stubFor(get(urlEqualTo("/storage/")) + .willReturn(aResponse() + .withHeader("Content-Type", "text/turtle") + .withBody(""" + <> , , . + """))); + } + + public String baseUrl() { + return wireMockServer.baseUrl(); + } + + public void stop() { + if (wireMockServer != null) { + wireMockServer.stop(); + } + } +} diff --git a/examples/spring-web/src/test/java/com/inrupt/client/examples/spring/web/WebApplicationTests.java b/examples/spring-web/src/test/java/com/inrupt/client/examples/spring/web/WebApplicationTests.java new file mode 100644 index 00000000000..9ef19b1f93a --- /dev/null +++ b/examples/spring-web/src/test/java/com/inrupt/client/examples/spring/web/WebApplicationTests.java @@ -0,0 +1,171 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.examples.spring.web; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import com.inrupt.client.examples.spring.web.controller.SolidController; +import com.inrupt.client.examples.spring.web.model.*; + +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.util.ResourceUtils; + +import java.io.IOException; +import java.net.URI; +import java.time.Instant; +import java.util.Map; + +import org.jose4j.jwk.PublicJsonWebKey; +import org.jose4j.lang.JoseException; +import org.jose4j.lang.UncheckedJoseException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.resttestclient.TestRestTemplate; +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; + +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = WebApplication.class) +@AutoConfigureTestRestTemplate +class WebApplicationTests { + + static final MockWebServer mockServer = new MockWebServer(); + + @Autowired + SolidController controller; + + @Autowired + TestRestTemplate restTemplate; + + @BeforeAll + static void setup() { + mockServer.start(); + } + + @AfterAll + static void teardown() { + mockServer.stop(); + } + + @Test + void contextLoads() { + assertThat(controller).isNotNull(); + } + + @Test + void testIndexPage() { + final var entity = restTemplate.getForEntity("/", String.class); + assertThat(entity.getStatusCode().value()).isEqualTo(200); + assertThat(entity.getHeaders().getFirst("Content-Type")).isEqualTo("text/html;charset=UTF-8"); + assertThat(entity.hasBody()); + assertThat(entity.getBody()).contains("Authentication Demo"); + } + + @Test + void testScriptAsset() { + final var entity = restTemplate.getForEntity("/assets/script.js", String.class); + assertThat(entity.getStatusCode().value()).isEqualTo(200); + assertThat(entity.getHeaders().getFirst("Content-Type")).isEqualTo("text/javascript"); + assertThat(entity.hasBody()); + } + + @Test + void testUnauthenticatedProfileEndpoint() { + final var entity = restTemplate.getForEntity("/api/profile", Map.class); + assertThat(entity.getStatusCode().value()).isEqualTo(401); + } + + @Test + void testUnauthenticatedWebIdEndpoint() { + final var entity = restTemplate.getForEntity("/api/webid", Map.class); + assertThat(entity.getStatusCode().value()).isEqualTo(401); + } + + @Test + void testUnauthenticatedResourceEndpoint() { + final var entity = restTemplate.getForEntity("/api/resource?uri=https://example.com", byte[].class); + assertThat(entity.getStatusCode().value()).isEqualTo(401); + } + + @Test + void testControllerWebId() { + final var idToken = generateIdToken("user", "https://issuer.example", "https://example.com"); + final var oauthUser = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, + OidcIdToken.withTokenValue(idToken) + .expiresAt(Instant.now().plusSeconds(100)).subject("user").claim("webid", "https://example.com") + .build()); + final var webid = controller.getUser(oauthUser); + assertThat(webid.id()).isEqualTo(URI.create("https://example.com")); + } + + @Test + void testControllerProfile() { + final var webid = mockServer.baseUrl() + "/user"; + final var issuer = mockServer.baseUrl() + "/oidc"; + final var storage = mockServer.baseUrl() + "/storage/"; + final var idToken = generateIdToken("user", issuer, webid); + final var oauthUser = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, + OidcIdToken.withTokenValue(idToken) + .expiresAt(Instant.now().plusSeconds(100)).subject("user").claim("webid", webid) + .build()); + final var profile = controller.getProfile(oauthUser); + assertThat(profile.id()).isEqualTo(URI.create(webid)); + assertThat(profile.storages()).contains(URI.create(storage)); + assertThat(profile.issuers()).contains(URI.create(issuer)); + } + + @Test + void testControllerStorage() { + final var webid = mockServer.baseUrl() + "/user"; + final var storage = mockServer.baseUrl() + "/storage/"; + final var idToken = generateIdToken("user", mockServer.baseUrl() + "/oidc", webid); + final var oauthUser = new DefaultOidcUser(AuthorityUtils.NO_AUTHORITIES, + OidcIdToken.withTokenValue(idToken) + .expiresAt(Instant.now().plusSeconds(100)).subject("user").claim("webid", webid) + .build()); + final var resource = controller.getResource(oauthUser, URI.create(storage)); + final var body = new String(resource.getBody(), UTF_8); + assertThat(body).contains(""); + assertThat(body).contains("<" + storage + "resource1>"); + } + + String generateIdToken(final String sub, final String issuer, final String webid) { + try { + final var jwk = PublicJsonWebKey.Factory + .newPublicJwk(ResourceUtils.readResource("testKey.json")); + return Jwt.claims() + .subject(sub) + .issuer(issuer) + .claim("webid", webid) + .audience("solid").jws() + .keyId("7ZG5H7LrFxphsVW1G_wTKA").sign(jwk.getPrivateKey()); + } catch (final IOException | JoseException ex) { + throw new UncheckedJoseException("Could not build token", ex); + } + } +} diff --git a/examples/spring-web/src/test/resources/testKey.json b/examples/spring-web/src/test/resources/testKey.json new file mode 100644 index 00000000000..41281001173 --- /dev/null +++ b/examples/spring-web/src/test/resources/testKey.json @@ -0,0 +1,3 @@ +{"kty":"EC","kid":"7ZG5H7LrFxphsVW1G_wTKA","use":"sig","alg":"ES256","x":"XjD4jZD76KpwQHUZsvokyXqyHLZktVMNIcgk6MlOzk4","y":"1AQeTsyf_6Ho-YieemVdaI-D7xGpgg9W53Kbe0xTUj8","crv":"P-256","d":"R95IpFAla_vN4OZ0KhpxvG-bHrbcT4X1h4SE8RhXNE8"} + + diff --git a/examples/springboot/pom.xml b/examples/springboot/pom.xml index 0858de9d60a..c4d655080bc 100644 --- a/examples/springboot/pom.xml +++ b/examples/springboot/pom.xml @@ -5,21 +5,18 @@ com.inrupt.client inrupt-client-examples-parent - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-examples-springboot - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT Inrupt Java Client Libraries - Spring Boot Example Sample Spring Boot application. - 17 - 17 - 3.1.1.RELEASE - 3.2.1 + 4.1.0 @@ -108,6 +105,14 @@ spring-boot-devtools true
+ + + + + com.nimbusds + nimbus-jose-jwt + 10.9.1 +
diff --git a/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/ApplicationStartController.java b/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/ApplicationStartController.java index b8b400ecfa6..c728ea8a048 100644 --- a/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/ApplicationStartController.java +++ b/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/ApplicationStartController.java @@ -52,7 +52,6 @@ public class ApplicationStartController { private String userName; - @GetMapping("/") public String index() { this.userName = null; @@ -135,4 +134,4 @@ private void setUsernameFrontendState(final Model model) { model.addAttribute(FRONTEND_USERNAME, this.userName); } -} \ No newline at end of file +} diff --git a/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/model/Book.java b/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/model/Book.java index c4e9cc93730..bceb0740dfb 100644 --- a/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/model/Book.java +++ b/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/model/Book.java @@ -20,7 +20,7 @@ */ package com.inrupt.client.examples.springboot.model; -import com.inrupt.client.solid.Metadata; +import com.inrupt.client.Headers; import com.inrupt.client.solid.SolidRDFSource; import com.inrupt.rdf.wrapping.commons.TermMappings; import com.inrupt.rdf.wrapping.commons.ValueMappings; @@ -40,8 +40,8 @@ public class Book extends SolidRDFSource { private final IRI author = rdf.createIRI(Vocabulary.BOOK_AUTHOR); private final IRI description = rdf.createIRI(Vocabulary.BOOK_DESCRIPTION); - public Book(final URI identifier, final Dataset dataset, final Metadata metadata) { - super(identifier, dataset, metadata); + public Book(final URI identifier, final Dataset dataset, final Headers headers) { + super(identifier, dataset, headers); this.bookId = new Node(rdf.createIRI(identifier.toString()), getGraph()); } diff --git a/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/model/BookLibrary.java b/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/model/BookLibrary.java index 518d9f79283..2bbd52296a7 100644 --- a/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/model/BookLibrary.java +++ b/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/model/BookLibrary.java @@ -20,7 +20,7 @@ */ package com.inrupt.client.examples.springboot.model; -import com.inrupt.client.solid.Metadata; +import com.inrupt.client.Headers; import com.inrupt.client.solid.SolidRDFSource; import com.inrupt.rdf.wrapping.commons.TermMappings; import com.inrupt.rdf.wrapping.commons.ValueMappings; @@ -39,8 +39,8 @@ public class BookLibrary extends SolidRDFSource { private Node bookLibraryId; private final IRI contains = rdf.createIRI(Vocabulary.CONTAINS_BOOK); - public BookLibrary(final URI identifier, final Dataset dataset, final Metadata metadata) { - super(identifier, dataset, metadata); + public BookLibrary(final URI identifier, final Dataset dataset, final Headers headers) { + super(identifier, dataset, headers); this.bookLibraryId = new Node(rdf.createIRI(identifier.toString()), getGraph()); } diff --git a/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/service/BookLibraryService.java b/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/service/BookLibraryService.java index 775c4bb4981..5c277a9615c 100644 --- a/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/service/BookLibraryService.java +++ b/examples/springboot/src/main/java/com/inrupt/client/examples/springboot/service/BookLibraryService.java @@ -48,6 +48,7 @@ public class BookLibraryService implements IBookLibraryService { private BookLibrary bookLib; private String uriOfBookLibraryResource; + @Override public void loadBookLibrary(final String bookLibResource) { client = getClient(); @@ -60,25 +61,20 @@ public void loadBookLibrary(final String bookLibResource) { this.bookLib = client.read(bookLibraryId, BookLibrary.class); } catch (SolidClientException exception) { if (authenticationFail(exception.getStatusCode())) { - throw new AuthenticationException("You are not authenticated! Try logging in."); + throw new AuthenticationException("You are not authenticated! Try logging in.", exception); } if (authorizationFail(exception.getStatusCode())) { - throw new AuthorizationException("You do not have the corresponding access rights."); + throw new AuthorizationException("You do not have the corresponding access rights.", exception); } } } + @Override public void clearBookLibrary() { this.bookLib = null; } - public Set getAllBookURIs() { - if (this.bookLib == null) { - throw new AuthorizationException("Not allowed."); - } - return this.bookLib.getAllBooks(); - } - + @Override public Set getAllBook() { if (this.bookLib == null) { throw new AuthorizationException("Not allowed."); @@ -91,6 +87,7 @@ public Set getAllBook() { return allBooks; } + @Override public Set getBookForTitle(final String bookTitle) { if (this.bookLib == null) { throw new AuthorizationException("Not allowed."); @@ -109,6 +106,7 @@ public Set getBookForTitle(final String bookTitle) { return foundBooks; } + @Override public Set getBookForAuthor(final String bookAuthor) { if (this.bookLib == null) { throw new AuthorizationException("Not allowed."); @@ -127,6 +125,7 @@ public Set getBookForAuthor(final String bookAuthor) { return foundBooks; } + @Override public String getBookLibraryUri() { return this.uriOfBookLibraryResource; } @@ -138,7 +137,7 @@ private SolidSyncClient getClient() { try { defaultClient = defaultClient.session(OpenIdSession.ofIdToken(userService.getCurrentUser().getToken())); } catch (OpenIdException exception) { - throw new AuthorizationException("Token expired, please log out and re-login."); + throw new AuthorizationException("Token expired, please log out and re-login.", exception); } } @@ -152,5 +151,4 @@ private boolean authenticationFail(final int statusCode) { private boolean authorizationFail(final int statusCode) { return statusCode == 403; } - } diff --git a/examples/springboot/src/site/site.xml b/examples/springboot/src/site/site.xml index a6989ad1caa..8c42c5a3f37 100644 --- a/examples/springboot/src/site/site.xml +++ b/examples/springboot/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/examples/src/site/site.xml b/examples/src/site/site.xml index dc7d8499019..b80ca81c525 100644 --- a/examples/src/site/site.xml +++ b/examples/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/examples/webapp/pom.xml b/examples/webapp/pom.xml index 08842b08b48..018c48fdb99 100644 --- a/examples/webapp/pom.xml +++ b/examples/webapp/pom.xml @@ -5,27 +5,24 @@ com.inrupt.client inrupt-client-examples-parent - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-examples-webapp - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT Inrupt Java Client Libraries - Quarkus Example Sample web application. - 17 - 17 - - 3.6.5 + 3.37.0 - io.quarkus + io.quarkus.platform quarkus-bom ${quarkus.version} pom @@ -112,7 +109,7 @@ - io.quarkus + io.quarkus.platform quarkus-maven-plugin ${quarkus.version} true diff --git a/examples/webapp/src/main/java/com/inrupt/client/examples/webapp/SolidStorage.java b/examples/webapp/src/main/java/com/inrupt/client/examples/webapp/SolidStorage.java index fe56553cdc1..152e1846cd9 100644 --- a/examples/webapp/src/main/java/com/inrupt/client/examples/webapp/SolidStorage.java +++ b/examples/webapp/src/main/java/com/inrupt/client/examples/webapp/SolidStorage.java @@ -104,12 +104,10 @@ static URI filterResource(final SolidClient client, final SolidResource resource .HEAD() .build(); final var res = client.send(req, Response.BodyHandlers.discarding()).toCompletableFuture().join(); - final var contentType = res.headers().firstValue("Content-Type"); - if (contentType.isPresent() && (contentType.get().toLowerCase().contains("text/turtle")) ) { - return LDP.RDFSource; - } - if (contentType.isPresent() && !(contentType.get().toLowerCase().contains("text/turtle"))) { - return LDP.NonRDFSource; + final var contentTypes = res.headers().allValues("Content-Type"); + if (!contentTypes.isEmpty()) { + return contentTypes.stream().map(type -> type.split(";")[0].trim()) + .filter("text/turtle"::equalsIgnoreCase).findFirst().isPresent() ? LDP.RDFSource : LDP.NonRDFSource; } return LDP.Resource; } diff --git a/examples/webapp/src/site/site.xml b/examples/webapp/src/site/site.xml index 20f2a385989..3ba07a7b1dd 100644 --- a/examples/webapp/src/site/site.xml +++ b/examples/webapp/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/gradle/.gitignore b/gradle/.gitignore deleted file mode 100644 index 7f6823bcc0f..00000000000 --- a/gradle/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.gradle -build/ diff --git a/gradle/build.gradle b/gradle/build.gradle deleted file mode 100644 index 9900fd60df6..00000000000 --- a/gradle/build.gradle +++ /dev/null @@ -1 +0,0 @@ -apply plugin: 'java-library' diff --git a/gradle/gradle.properties b/gradle/gradle.properties deleted file mode 100644 index 82841c381f6..00000000000 --- a/gradle/gradle.properties +++ /dev/null @@ -1,10 +0,0 @@ -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -kotlin.code.style=official -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true diff --git a/gradle/gradle/wrapper/gradle-wrapper.jar b/gradle/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c023e..00000000000 Binary files a/gradle/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/gradle/gradle/wrapper/gradle-wrapper.properties b/gradle/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ceee333a02f..00000000000 --- a/gradle/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Feb 13 10:50:37 CST 2023 -distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip -distributionPath=wrapper/dists -zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME diff --git a/gradle/gradlew b/gradle/gradlew deleted file mode 100755 index 4f906e0c811..00000000000 --- a/gradle/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/gradle/gradlew.bat b/gradle/gradlew.bat deleted file mode 100644 index ac1b06f9382..00000000000 --- a/gradle/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/gradle/pom.xml b/gradle/pom.xml deleted file mode 100644 index 1322724eb06..00000000000 --- a/gradle/pom.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - 4.0.0 - - com.inrupt.client - inrupt-client - 1.2.0-SNAPSHOT - - - inrupt-client-gradle - Inrupt Java Client Libraries - Gradle Examples - - Gradle-based code to work with the Inrupt Java Client Libraries. - - pom - - - ./gradlew - build - false - - - - - windows - - - windows - - - - gradlew.bat - - - - maven.test.skip - - - maven.test.skip - true - - - - assemble - - - - skipTests - - - skipTests - true - - - - assemble - - - - - - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - - org.codehaus.mojo - exec-maven-plugin - ${exec.plugin.version} - - - gradle - prepare-package - - ${gradle.executable} - - clean - ${gradle.task} - -Dmaven.repo.local=${settings.localRepository} - --no-daemon - - - ${env.MAVEN_OPTS} - - ${skip.gradle.build} - - - exec - - - - - - - - diff --git a/gradle/settings.gradle b/gradle/settings.gradle deleted file mode 100644 index 4074a69782f..00000000000 --- a/gradle/settings.gradle +++ /dev/null @@ -1,22 +0,0 @@ -pluginManagement { - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id "com.gradle.enterprise" version "3.16.1" -} - -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - mavenLocal() - } -} - -rootProject.name = "inrupt-gradle-app" diff --git a/guava/pom.xml b/guava/pom.xml index 2c59229b9a2..ba774df3d15 100644 --- a/guava/pom.xml +++ b/guava/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-guava diff --git a/httpclient/pom.xml b/httpclient/pom.xml index 437efe25f66..f9c901f0af9 100644 --- a/httpclient/pom.xml +++ b/httpclient/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-httpclient diff --git a/httpclient/src/main/java/com/inrupt/client/httpclient/HttpClientService.java b/httpclient/src/main/java/com/inrupt/client/httpclient/HttpClientService.java index 4818cad9757..f7d56a950c1 100644 --- a/httpclient/src/main/java/com/inrupt/client/httpclient/HttpClientService.java +++ b/httpclient/src/main/java/com/inrupt/client/httpclient/HttpClientService.java @@ -68,6 +68,7 @@ public CompletionStage> send(final Request request, final Respon .orElseGet(HttpRequest.BodyPublishers::noBody); builder.method(request.method(), publisher); + request.timeout().ifPresent(builder::timeout); for (final Map.Entry> entry : request.headers().asMap().entrySet()) { for (final String value : entry.getValue()) { diff --git a/httpclient/src/site/site.xml b/httpclient/src/site/site.xml index 80771952b4a..af726a05dcd 100644 --- a/httpclient/src/site/site.xml +++ b/httpclient/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/integration/base/pom.xml b/integration/base/pom.xml index 9f53b0a5267..abac47e5bab 100644 --- a/integration/base/pom.xml +++ b/integration/base/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client-integration-tests - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-integration-base-tests Inrupt Java Client Libraries - Integration Base Tests @@ -14,9 +14,20 @@ 2.1.1 - 3.5.2 + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + com.inrupt.client @@ -35,7 +46,7 @@ com.inrupt.client - inrupt-client-integration-customokhttp + inrupt-client-okhttp ${project.version} @@ -76,7 +87,6 @@ com.fasterxml.jackson.core jackson-annotations - ${jackson.version} io.smallrye.config @@ -108,10 +118,15 @@ org.wiremock - wiremock + wiremock-standalone ${wiremock.version} provided + + org.hamcrest + hamcrest + ${hamcrest.version} + @@ -127,16 +142,10 @@ org.apache.maven.plugins maven-surefire-plugin - - - org.apache.maven.plugins maven-failsafe-plugin - - - org.jacoco diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/AccessGrantScenarios.java b/integration/base/src/main/java/com/inrupt/client/integration/base/AccessGrantScenarios.java index 3869466dc7b..a2d2331003a 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/AccessGrantScenarios.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/AccessGrantScenarios.java @@ -20,6 +20,7 @@ */ package com.inrupt.client.integration.base; +import static com.inrupt.client.accessgrant.CredentialFilter.CredentialStatus.ACTIVE; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.assumeFalse; @@ -41,11 +42,10 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Arrays; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.commons.rdf.api.IRI; @@ -97,10 +97,10 @@ public class AccessGrantScenarios { protected static final String GRANT_MODE_READ = "Read"; private static final String GRANT_MODE_APPEND = "Append"; private static final String GRANT_MODE_WRITE = "Write"; - private static final URI PURPOSE1 = URI.create("https://purpose.example/212efdf4-e1a4-4dcd-9d3b-d6eb92e0205f"); - private static final URI PURPOSE2 = URI.create("https://purpose.example/de605b08-76c7-4f04-9cec-a438810b0c03"); - protected static final Set PURPOSES = new HashSet<>(Arrays.asList(PURPOSE1, PURPOSE2)); - protected static final String GRANT_EXPIRATION = Instant.now().plus(1, ChronoUnit.HOURS) + private static URI PURPOSE1; + private static URI PURPOSE2; + protected static final Set PURPOSES = new HashSet<>(); + protected static final String GRANT_EXPIRATION = Instant.now().plus(20, ChronoUnit.MINUTES) .truncatedTo(ChronoUnit.SECONDS).toString(); private static final String sharedTextFileName = "sharedFile.txt"; protected static URI sharedTextFileURI; @@ -111,6 +111,11 @@ public class AccessGrantScenarios { @BeforeAll static void setup() throws IOException { LOGGER.info("Setup AccessGrantScenarios test"); + PURPOSE1 = URI.create("https://purpose.example/" + UUID.randomUUID()); + PURPOSE2 = URI.create("https://purpose.example/" + UUID.randomUUID()); + PURPOSES.clear(); + PURPOSES.add(PURPOSE1); + PURPOSES.add(PURPOSE2); if (config.getOptionalValue("inrupt.test.webid", String.class).isPresent()) { LOGGER.info("Running AccessGrantScenarios on live server"); webidUrl = config.getOptionalValue("inrupt.test.webid", String.class).get(); @@ -177,7 +182,8 @@ static void setup() throws IOException { URI.create(webidUrl), URI.create(requesterWebidUrl), sharedTextFileURI, - authServer.getMockServerUrl() + authServer.getMockServerUrl(), + PURPOSE1 ); accessGrantServer.start(); ACCESS_GRANT_PROVIDER = accessGrantServer.getMockServerUrl(); @@ -219,10 +225,10 @@ void accessGrantIssuanceLifecycleTest(final Session resourceOwnerSession, final URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ)); + final Set modes = Set.of(GRANT_MODE_READ); final Instant expiration = Instant.parse(GRANT_EXPIRATION); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(requesterWebidUrl), - new HashSet<>(Arrays.asList(sharedTextFileURI)), modes, PURPOSES, expiration) + Set.of(sharedTextFileURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( @@ -230,23 +236,38 @@ void accessGrantIssuanceLifecycleTest(final Session resourceOwnerSession, final ).session(resourceOwnerSession); final AccessGrant grant = resourceOwnerAccessGrantClient.grantAccess(request) .toCompletableFuture().join(); + final AccessDenial denial = resourceOwnerAccessGrantClient.denyAccess(request) + .toCompletableFuture().join(); //2. call verify endpoint to verify grant final var grantVerification = resourceOwnerAccessGrantClient.verify(grant).toCompletableFuture().join(); assertTrue(grantVerification.getChecks().size() > 0); assertEquals(grantVerification.getErrors().size(), 0); assertEquals(grantVerification.getWarnings().size(), 0); - final AccessGrant grantFromVcProvider = resourceOwnerAccessGrantClient.fetch(grant.getIdentifier(), AccessGrant.class) .toCompletableFuture().join(); assertEquals(grant.getPurposes(), grantFromVcProvider.getPurposes()); + final var denialVerification = resourceOwnerAccessGrantClient.verify(denial).toCompletableFuture().join(); + assertTrue(denialVerification.getChecks().size() > 0); + assertEquals(denialVerification.getErrors().size(), 0); + assertEquals(denialVerification.getWarnings().size(), 0); + final AccessDenial denialFromVcProvider = + resourceOwnerAccessGrantClient.fetch(denial.getIdentifier(), AccessDenial.class) + .toCompletableFuture().join(); + assertEquals(denial.getPurposes(), denialFromVcProvider.getPurposes()); + //unauthorized request test final SolidSyncClient requesterClient = SolidSyncClient.getClientBuilder().build(); final var err = assertThrows(UnauthorizedException.class, () -> requesterClient.read(sharedTextFileURI, SolidNonRDFSource.class)); assertEquals(Utils.UNAUTHORIZED, err.getStatusCode()); + Utils.checkProblemDetails(err).ifPresent(problemDetails -> { + assertEquals("Unauthorized", problemDetails.getTitle()); + assertNotNull(problemDetails.getDetail()); + assertNotNull(problemDetails.getInstance()); + }); //authorized request test final Session accessSession = AccessGrantSession.ofAccessGrant(requesterSession, grant); @@ -255,20 +276,30 @@ void accessGrantIssuanceLifecycleTest(final Session resourceOwnerSession, final assertDoesNotThrow(() -> requesterAuthClient.read(sharedTextFileURI, SolidNonRDFSource.class)); assertDoesNotThrow(resourceOwnerAccessGrantClient.revoke(grant).toCompletableFuture()::join); + assertDoesNotThrow(resourceOwnerAccessGrantClient.revoke(denial).toCompletableFuture()::join); //6. call verify endpoint to check the grant is not valid final var revokedGrantVerification = resourceOwnerAccessGrantClient.verify(grant).toCompletableFuture().join(); - assertTrue(grantVerification.getChecks().size() > 0); + assertTrue(revokedGrantVerification.getChecks().size() > 0); assertEquals(revokedGrantVerification.getErrors().size(), 1); - assertEquals(grantVerification.getWarnings().size(), 0); + assertEquals(revokedGrantVerification.getWarnings().size(), 0); + + final var revokedDenialVerification = resourceOwnerAccessGrantClient.verify(denial) + .toCompletableFuture().join(); + assertTrue(revokedDenialVerification.getChecks().size() > 0); + assertEquals(revokedDenialVerification.getErrors().size(), 1); + assertEquals(revokedDenialVerification.getWarnings().size(), 0); // Once revoked, the Access Grant should no longer grant access to the resource. The previously issued access // token may still be valid, so cache is cleared for the test. accessSession.reset(); - assertThrows( - UnauthorizedException.class, - () -> requesterAuthClient.read(sharedTextFileURI, SolidNonRDFSource.class) - ); + final var err2 = assertThrows(UnauthorizedException.class, + () -> requesterAuthClient.read(sharedTextFileURI, SolidNonRDFSource.class)); + Utils.checkProblemDetails(err2).ifPresent(problemDetails -> { + assertEquals("Unauthorized", problemDetails.getTitle()); + assertNotNull(problemDetails.getDetail()); + assertNotNull(problemDetails.getInstance()); + }); } @ParameterizedTest @@ -282,10 +313,10 @@ void accessGrantWithRequestOverridesTest(final Session resourceOwnerSession, fin URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_APPEND)); + final Set modes = Set.of(GRANT_MODE_READ, GRANT_MODE_APPEND); final Instant expiration = Instant.now().plus(90, ChronoUnit.DAYS); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(requesterWebidUrl), - new HashSet<>(Arrays.asList(sharedTextFileURI)), modes, PURPOSES, expiration) + Set.of(sharedTextFileURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( @@ -314,10 +345,10 @@ void accessGrantQueryByRequesterTest(final Session resourceOwnerSession, final S URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); + final Set modes = Set.of(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND); final Instant expiration = Instant.parse(GRANT_EXPIRATION); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(requesterWebidUrl), - new HashSet<>(Arrays.asList(sharedTextFileURI)), modes, PURPOSES, expiration) + Set.of(sharedTextFileURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( @@ -326,18 +357,12 @@ void accessGrantQueryByRequesterTest(final Session resourceOwnerSession, final S final AccessGrant grant = resourceOwnerAccessGrantClient.grantAccess(request) .toCompletableFuture().join(); - final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() - .recipient(URI.create(requesterWebidUrl)).resource(sharedTextFileURI) - .mode(GRANT_MODE_READ).mode(GRANT_MODE_WRITE).mode(GRANT_MODE_APPEND).build(AccessGrant.class); - final List grants = resourceOwnerAccessGrantClient.query(query).toCompletableFuture().join(); - assertEquals(1, grants.size()); - - final AccessCredentialQuery query2 = AccessCredentialQuery.newBuilder() - .recipient(URI.create("https://someuser.test")).resource(sharedTextFileURI).build(AccessGrant.class); - final List randomGrants = - resourceOwnerAccessGrantClient.query(query2).toCompletableFuture().join(); - - assertEquals(0, randomGrants.size()); + final CredentialFilter query = CredentialFilter.newBuilder().status(ACTIVE) + .toAgent(URI.create(requesterWebidUrl)).resource(sharedTextFileURI) + .build(AccessGrant.class); + final CredentialResult results = resourceOwnerAccessGrantClient.query(query) + .toCompletableFuture().join(); + assertEquals(1, results.getItems().size()); //cleanup assertDoesNotThrow(resourceOwnerAccessGrantClient.revoke(grant).toCompletableFuture()::join); @@ -354,10 +379,10 @@ void accessGrantGrantAccessByIdTest(final Session resourceOwnerSession, final Se URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ)); + final Set modes = Set.of(GRANT_MODE_READ); final Instant expiration = Instant.parse(GRANT_EXPIRATION); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(webidUrl), - new HashSet<>(Arrays.asList(sharedTextFileURI)), modes, PURPOSES, expiration) + Set.of(sharedTextFileURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final var grantId = request.getIdentifier(); @@ -393,13 +418,14 @@ void accessGrantGrantAccessByIdTest(final Session resourceOwnerSession, final Se AccessGrant.class).toCompletableFuture().join(); //check that the approved grant is the same we find when querying for all grants - final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() - .recipient(URI.create(requesterWebidUrl)).resource(sharedTextFileURI) - .mode(GRANT_MODE_READ).build(AccessGrant.class); - final List grants = resourceOwnerAccessGrantClient.query(query).toCompletableFuture().join(); - assertEquals(1, grants.size()); - assertEquals(grant.getIdentifier(), grants.get(0).getIdentifier()); - assertEquals(accessGrant.getIdentifier(), grants.get(0).getIdentifier()); + final CredentialFilter query = CredentialFilter.newBuilder().status(ACTIVE) + .toAgent(URI.create(requesterWebidUrl)).resource(sharedTextFileURI) + .build(AccessGrant.class); + final CredentialResult results = resourceOwnerAccessGrantClient.query(query) + .toCompletableFuture().join(); + assertEquals(1, results.getItems().size()); + assertEquals(grant.getIdentifier(), results.getItems().get(0).getIdentifier()); + assertEquals(accessGrant.getIdentifier(), results.getItems().get(0).getIdentifier()); //cleanup assertDoesNotThrow(resourceOwnerAccessGrantClient.revoke(grant).toCompletableFuture()::join); @@ -416,10 +442,10 @@ void accessGrantQueryByPurposeTest(final Session resourceOwnerSession, final Ses URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_APPEND)); + final Set modes = Set.of(GRANT_MODE_APPEND); final Instant expiration = Instant.parse(GRANT_EXPIRATION); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(requesterWebidUrl), - new HashSet<>(Arrays.asList(sharedTextFileURI)), modes, PURPOSES, expiration) + Set.of(sharedTextFileURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( @@ -429,17 +455,11 @@ void accessGrantQueryByPurposeTest(final Session resourceOwnerSession, final Ses .toCompletableFuture().join(); //query for all grants with a dedicated purpose - final AccessCredentialQuery query = AccessCredentialQuery.newBuilder() - .resource(sharedTextFileURI).purpose(PURPOSE1).mode(GRANT_MODE_APPEND).build(AccessGrant.class); - final List grants = resourceOwnerAccessGrantClient.query(query).toCompletableFuture().join(); - assertEquals(1, grants.size()); - - //query for all grants of dedicated purpose combinations - final AccessCredentialQuery query2 = AccessCredentialQuery.newBuilder() - .resource(sharedTextFileURI).purpose(PURPOSE1).mode(GRANT_MODE_WRITE).build(AccessGrant.class); - final List randomGrants = - resourceOwnerAccessGrantClient.query(query2).toCompletableFuture().join(); - assertEquals(0, randomGrants.size()); //our grant is actually a APPEND + final CredentialFilter query = CredentialFilter.newBuilder().status(ACTIVE) + .resource(sharedTextFileURI).purpose(PURPOSE1).build(AccessGrant.class); + final CredentialResult results = resourceOwnerAccessGrantClient.query(query) + .toCompletableFuture().join(); + assertEquals(1, results.getItems().size()); //cleanup assertDoesNotThrow(resourceOwnerAccessGrantClient.revoke(grant).toCompletableFuture()::join); @@ -470,10 +490,10 @@ void accessGrantGetRdfTest(final Session resourceOwnerSession, final Session req URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); + final Set modes = Set.of(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND); final Instant expiration = Instant.parse(GRANT_EXPIRATION); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(requesterWebidUrl), - new HashSet<>(Arrays.asList(testRDFresourceURI)), modes, PURPOSES, expiration) + Set.of(testRDFresourceURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( @@ -517,11 +537,10 @@ void accessGrantSetRdfTest(final Session resourceOwnerSession, final Session req URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList( - GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); + final Set modes = Set.of(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND); final Instant expiration = Instant.parse(GRANT_EXPIRATION); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(requesterWebidUrl), - new HashSet<>(Arrays.asList(testRDFresourceURI)), modes, PURPOSES, expiration) + Set.of(testRDFresourceURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( @@ -571,10 +590,10 @@ void accessGrantCreateRdfTest(final Session resourceOwnerSession, final Session URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); + final Set modes = Set.of(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND); final Instant expiration = Instant.parse(GRANT_EXPIRATION); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(requesterWebidUrl), - new HashSet<>(Arrays.asList(newTestFileURI)), modes, PURPOSES, expiration) + Set.of(newTestFileURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( @@ -621,10 +640,10 @@ void accessGrantGetNonRdfTest(final Session resourceOwnerSession, final Session URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); + final Set modes = Set.of(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND); final Instant expiration = Instant.parse(GRANT_EXPIRATION); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(requesterWebidUrl), - new HashSet<>(Arrays.asList(newTestFileURI)), modes, PURPOSES, expiration) + Set.of(newTestFileURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( @@ -670,10 +689,10 @@ void accessGrantSetNonRdfTest(final Session resourceOwnerSession, final Session URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); + final Set modes = Set.of(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND); final Instant expiration = Instant.parse(GRANT_EXPIRATION); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(requesterWebidUrl), - new HashSet<>(Arrays.asList(newTestFileURI)), modes, PURPOSES, expiration) + Set.of(newTestFileURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( @@ -690,7 +709,7 @@ void accessGrantSetNonRdfTest(final Session resourceOwnerSession, final Session try (final InputStream newis = new ByteArrayInputStream( StandardCharsets.UTF_8.encode("Test text").array())) { final SolidNonRDFSource testResource = - new SolidNonRDFSource(newTestFileURI, Utils.PLAIN_TEXT, newis, resource.getMetadata()); + new SolidNonRDFSource(newTestFileURI, Utils.PLAIN_TEXT, newis, resource.getHeaders()); assertDoesNotThrow(() -> requesterAuthClient.update(testResource)); } } @@ -726,10 +745,10 @@ void accessGrantCreateNonRdfTest(final Session resourceOwnerSession, final Sessi URI.create(ACCESS_GRANT_PROVIDER) ).session(requesterSession); - final Set modes = new HashSet<>(Arrays.asList(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND)); + final Set modes = Set.of(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND); final Instant expiration = Instant.parse(GRANT_EXPIRATION); final AccessRequest request = requesterAccessGrantClient.requestAccess(URI.create(requesterWebidUrl), - new HashSet<>(Arrays.asList(newTestFileURI)), modes, PURPOSES, expiration) + Set.of(newTestFileURI), modes, PURPOSES, expiration) .toCompletableFuture().join(); final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( @@ -752,6 +771,71 @@ void accessGrantCreateNonRdfTest(final Session resourceOwnerSession, final Sessi assertDoesNotThrow(resourceOwnerAccessGrantClient.revoke(grant).toCompletableFuture()::join); } + @ParameterizedTest + @MethodSource("provideSessions") + @DisplayName("Creating non-RDF using a templated Access Grant") + void accessGrantCreateTemplateTest(final Session resourceOwnerSession, final Session requesterSession) + throws IOException { + + LOGGER.info("Integration Test - Creating non-RDF using a templated Access Grant"); + + final URI newTestFileURI = URIBuilder.newBuilder(privateContainerURI) + .path("newFile-accessGrantCreateNonRdfTest2.txt") + .build(); + + final String template = privateContainerURI.getScheme() + + "://{domain}/{path}/newFile-accessGrantCreateNonRdfTest2.txt"; + + final SolidSyncClient resourceOwnerClient = SolidSyncClient.getClientBuilder() + .build().session(resourceOwnerSession); + + final AccessGrantClient requesterAccessGrantClient = new AccessGrantClient( + URI.create(ACCESS_GRANT_PROVIDER) + ).session(requesterSession); + + final Set modes = Set.of(GRANT_MODE_READ, GRANT_MODE_WRITE, GRANT_MODE_APPEND); + final Instant expiration = Instant.parse(GRANT_EXPIRATION); + final var request = AccessRequest.RequestParameters.newBuilder() + .template(template) + .modes(modes) + .purposes(PURPOSES) + .expiration(expiration) + .build(); + + final AccessRequest accessRequest = requesterAccessGrantClient.requestAccess(request) + .exceptionally(ex -> null) + .toCompletableFuture().join(); + + if (accessRequest == null) { + LOGGER.info("Templated Access Requests not supported"); + return; + } + + final AccessGrantClient resourceOwnerAccessGrantClient = new AccessGrantClient( + URI.create(ACCESS_GRANT_PROVIDER) + ).session(resourceOwnerSession); + final AccessGrant grant = resourceOwnerAccessGrantClient.grantAccess(accessRequest, templates -> + templates.stream().map(t -> t + .replace("{domain}", privateContainerURI.getHost()) + .replace("{path}", privateContainerURI.getPath().replaceAll("^/", "").replaceAll("/$", ""))) + .map(URI::create) + .collect(Collectors.toSet())) + .toCompletableFuture().join(); + + final Session newSession = AccessGrantSession.ofAccessGrant(requesterSession, grant); + final SolidSyncClient requesterAuthClient = SolidSyncClient.getClient().session(newSession); + + try (final InputStream is = new ByteArrayInputStream( + StandardCharsets.UTF_8.encode("Test test test text").array())) { + final SolidNonRDFSource testResource = + new SolidNonRDFSource(newTestFileURI, Utils.PLAIN_TEXT, is); + assertDoesNotThrow(() -> requesterAuthClient.create(testResource)); + } + + resourceOwnerClient.delete(newTestFileURI); + assertDoesNotThrow(resourceOwnerAccessGrantClient.revoke(grant).toCompletableFuture()::join); + } + private static void prepareAcpOfResource(final SolidSyncClient authClient, final URI resourceURI, final Class clazz) { diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/ApplicationRequestMetadataScenarios.java b/integration/base/src/main/java/com/inrupt/client/integration/base/ApplicationRequestMetadataScenarios.java index b9025ac1dbd..9114327f370 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/ApplicationRequestMetadataScenarios.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/ApplicationRequestMetadataScenarios.java @@ -85,6 +85,7 @@ public class ApplicationRequestMetadataScenarios { private static final String CLIENT_ID = config.getValue("inrupt.test.client-id", String.class); private static final String CLIENT_SECRET = config.getValue("inrupt.test.client-secret", String.class); + private static final String FOLDER_SEPARATOR = "/"; private static URI publicContainerURI; @@ -322,6 +323,11 @@ void matchOnAuthFailedRequestSyncHighLevelClientTest(final Session session) { final var exception = assertThrows(BadRequestException.class, ()-> client.create(testResource)); matchHeaders(requestHeaders, exception.getHeaders()); + Utils.checkProblemDetails(exception).ifPresent(problemDetails -> { + assertEquals("Bad Request", problemDetails.getTitle()); + assertNotNull(problemDetails.getInstance()); + assertNotNull(problemDetails.getDetail()); + }); } } @@ -345,8 +351,12 @@ void matchOnUnAuthFailedRequestSyncHighLevelClientTest(final Session session) { final var exception = assertThrows(UnauthorizedException.class, ()-> client.create(testResource)); matchHeaders(requestHeaders, exception.getHeaders()); + Utils.checkProblemDetails(exception).ifPresent(problemDetails -> { + assertEquals("Unauthorized", problemDetails.getTitle()); + assertNotNull(problemDetails.getInstance()); + assertNotNull(problemDetails.getDetail()); + }); } - } @SuppressWarnings("unchecked") diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/AuthenticationScenarios.java b/integration/base/src/main/java/com/inrupt/client/integration/base/AuthenticationScenarios.java index 3ce89947379..e900baebf6c 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/AuthenticationScenarios.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/AuthenticationScenarios.java @@ -75,6 +75,7 @@ public class AuthenticationScenarios { private static final String AUTH_METHOD = config .getOptionalValue("inrupt.test.auth-method", String.class) .orElse("client_secret_basic"); + private static SolidSyncClient localAuthClient; @BeforeAll @@ -190,6 +191,7 @@ void fetchPrivateResourceUnauthenticatedTest(final Session session) { final var err = assertThrows(UnauthorizedException.class, () -> client.read(privateResourceURI, SolidRDFSource.class)); assertEquals(Utils.UNAUTHORIZED, err.getStatusCode()); + Utils.checkProblemDetails(err); assertDoesNotThrow(() -> authClient.delete(testResource)); } @@ -245,6 +247,7 @@ void fetchPrivateResourceUnauthAuthTest(final Session session) { final var err = assertThrows(UnauthorizedException.class, () -> client.read(privateResourceURI, SolidRDFSource.class)); assertEquals(Utils.UNAUTHORIZED, err.getStatusCode()); + Utils.checkProblemDetails(err); final SolidSyncClient authClient2 = client.session(session); assertDoesNotThrow(() -> authClient2.read(privateResourceURI, SolidRDFSource.class)); @@ -281,6 +284,7 @@ void multiSessionTest(final Session session) { final var err = assertThrows(UnauthorizedException.class, () -> client.read(privateResourceURI, SolidRDFSource.class)); assertEquals(Utils.UNAUTHORIZED, err.getStatusCode()); + Utils.checkProblemDetails(err); //delete both resources with whichever client assertDoesNotThrow(() -> authClient1.delete(testResource2)); diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/DomainModulesResource.java b/integration/base/src/main/java/com/inrupt/client/integration/base/DomainModulesResource.java index 72b6476ef4b..0356d06db2d 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/DomainModulesResource.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/DomainModulesResource.java @@ -22,9 +22,7 @@ import static org.junit.jupiter.api.Assertions.*; -import com.inrupt.client.Headers; -import com.inrupt.client.Request; -import com.inrupt.client.Response; +import com.inrupt.client.*; import com.inrupt.client.auth.Session; import com.inrupt.client.openid.OpenIdSession; import com.inrupt.client.solid.*; @@ -73,6 +71,7 @@ public class DomainModulesResource { private static final String AUTH_METHOD = config .getOptionalValue("inrupt.test.auth-method", String.class) .orElse("client_secret_basic"); + private static final String CONTENT_TYPE = "Content-Type"; private static final String CLIENT_ID = config.getValue("inrupt.test.client-id", String.class); private static final String CLIENT_SECRET = config.getValue("inrupt.test.client-secret", String.class); private static final String FOLDER_SEPARATOR = "/"; @@ -263,9 +262,9 @@ void blankNodesTest() { @Test @DisplayName("https://w3id.org/inrupt/qa/manifest/solid-client-java/podStorageFinding " + - "find pod storage from webID") + "find Pod storage from WebID") void findStorageTest() { - LOGGER.info("Integration Test - find pod storage from webID"); + LOGGER.info("Integration Test - find Pod storage from WebID"); try (final WebIdProfile sameProfile = client.read(URI.create(webidUrl), WebIdProfile.class)) { assertFalse(sameProfile.getStorages().isEmpty()); @@ -275,6 +274,12 @@ void findStorageTest() { final var err = assertThrows(NotFoundException.class, () -> client.read(missingWebId, WebIdProfile.class)); assertEquals(404, err.getStatusCode()); assertEquals(missingWebId, err.getUri()); + + Utils.checkProblemDetails(err).ifPresent(problemDetails -> { + assertEquals("Not Found", problemDetails.getTitle()); + assertNotNull(problemDetails.getInstance()); + assertNotNull(problemDetails.getDetail()); + }); } @Test diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/MockAccessGrantServer.java b/integration/base/src/main/java/com/inrupt/client/integration/base/MockAccessGrantServer.java index da48de35c37..8c035c7ca47 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/MockAccessGrantServer.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/MockAccessGrantServer.java @@ -43,13 +43,14 @@ class MockAccessGrantServer { private static final String USER_AGENT = "InruptJavaClient/" + Request.class .getPackage().getImplementationVersion(); private static final String DERIVE = "/derive"; + private static final String QUERY = "/query"; private static final String ISSUE = "/issue"; - private static final String VERIFY = "/verify"; private static final String HEADER_AUTHORIZATION = "Authorization"; private static final String SCHEME_BEARER = "Bearer"; + private static final String SOLID_ACCESS_GRANT = "SolidAccessGrant"; // An identifier to enable statefulness in Wiremock, useful to manage revocation and verification. private static final String SCENARIO_ACCESS_GRANT = "AccessGrant"; @@ -60,6 +61,7 @@ class MockAccessGrantServer { private final String ownerWebId; private final String requesterWebId; private final String sharedResource; + private final URI purpose; private final String authorisationServerUrl; @@ -67,15 +69,16 @@ public MockAccessGrantServer( final URI ownerWebId, final URI requesterWebId, final URI sharedResource, - final String authorisationServerUrl + final String authorisationServerUrl, + final URI purpose ) { this.ownerWebId = ownerWebId.toString(); this.requesterWebId = requesterWebId.toString(); this.sharedResource = sharedResource.toString(); this.authorisationServerUrl = authorisationServerUrl; + this.purpose = purpose; wireMockServer = new WireMockServer(WireMockConfiguration.options() - .httpDisabled(true) - .dynamicHttpsPort()); + .dynamicPort()); } private void setupMocks() { @@ -223,11 +226,42 @@ private void setupMocks() { .willReturn(aResponse() .withStatus(Utils.NO_CONTENT))); + wireMockServer.stubFor(get(urlPathEqualTo(QUERY)) + .atPriority(1) + .withQueryParam("type", equalTo(SOLID_ACCESS_GRANT)) + .withQueryParam("resource", equalTo(this.sharedResource)) + .withQueryParam("purpose", equalTo(purpose.toString())) + .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)) + .willReturn(aResponse() + .withStatus(Utils.SUCCESS) + .withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON) + .withBody(getResource("/query_endpoint_response.json", wireMockServer.baseUrl(), + this.requesterWebId, this.ownerWebId, this.sharedResource)))); + + wireMockServer.stubFor(get(urlPathEqualTo(QUERY)) + .atPriority(2) + .withQueryParam("type", equalTo(SOLID_ACCESS_GRANT)) + .withQueryParam("resource", equalTo(this.sharedResource)) + .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)) + .willReturn(aResponse() + .withStatus(Utils.SUCCESS) + .withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON) + .withBody(getResource("/query_endpoint_response.json", wireMockServer.baseUrl(), + this.requesterWebId, this.ownerWebId, this.sharedResource)))); + + wireMockServer.stubFor(get(urlPathEqualTo(QUERY)) + .atPriority(3) + .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)) + .willReturn(aResponse() + .withStatus(Utils.SUCCESS) + .withHeader(Utils.CONTENT_TYPE, Utils.APPLICATION_JSON) + .withBody(getResource("/query_endpoint_response_empty.json")))); + wireMockServer.stubFor(post(urlEqualTo(DERIVE)) .atPriority(1) .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)) .withRequestBody(containing("\"Append\"")) - .withRequestBody(containing("\"https://purpose.example/212efdf4-e1a4-4dcd-9d3b-d6eb92e0205f\"")) + .withRequestBody(containing("\"" + purpose + "\"")) .withRequestBody(containing("\"" + this.sharedResource + "\"")) .withRequestBody(containing("SolidAccessGrant")) .willReturn(aResponse() diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/MockOpenIDProvider.java b/integration/base/src/main/java/com/inrupt/client/integration/base/MockOpenIDProvider.java index 82645273bdb..5b069ed939c 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/MockOpenIDProvider.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/MockOpenIDProvider.java @@ -54,8 +54,7 @@ class MockOpenIDProvider { public MockOpenIDProvider(final String username) { this.username = username; wireMockServer = new WireMockServer(WireMockConfiguration.options() - .httpDisabled(true) - .dynamicHttpsPort()); + .dynamicPort()); } private void setupMocks() { diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/MockSolidServer.java b/integration/base/src/main/java/com/inrupt/client/integration/base/MockSolidServer.java index a0f46b98d77..682a1581171 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/MockSolidServer.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/MockSolidServer.java @@ -43,8 +43,7 @@ public MockSolidServer(final String asUri) { storage.put("/", new ServerBody(new byte[0], Utils.TEXT_TURTLE)); //add root to storage wireMockServer = new WireMockServer(WireMockConfiguration.options() - .httpDisabled(true) - .dynamicHttpsPort() + .dynamicPort() .extensions(new SolidServerTransformer(storage, this.asUri))); } diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/MockUMAAuthorizationServer.java b/integration/base/src/main/java/com/inrupt/client/integration/base/MockUMAAuthorizationServer.java index 35fc2bc3401..b2dd13b7277 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/MockUMAAuthorizationServer.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/MockUMAAuthorizationServer.java @@ -26,6 +26,7 @@ import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import com.inrupt.client.ProblemDetails; import com.inrupt.client.Request; import java.io.IOException; @@ -67,6 +68,16 @@ private void setupMocks() { .withBody( "{\"access_token\":\"token-67890\",\"token_type\":\"Bearer\",\"expires_in\":\"3600\"}" ))); + wireMockServer.stubFor(post(urlPathMatching("/" + Utils.UMA_TOKEN_ENDPOINT)) + .withRequestBody(WireMock.notContaining("claim_token")) + .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)) + .willReturn(aResponse() + .withStatus(403) + .withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE) + .withBody("{" + + "\"status\":403, \"title\": \"Forbidden\"," + + "\"description\": \"The client does not have access to this resource.\"," + + "\"instance\": \"urn:uuid:8dfa92c4-f56b-4f01-93a0-983484cfbb26\"}"))); } public void start() { diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/MockWebIdService.java b/integration/base/src/main/java/com/inrupt/client/integration/base/MockWebIdService.java index 34b529db2eb..3b686c9431f 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/MockWebIdService.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/MockWebIdService.java @@ -49,8 +49,7 @@ public MockWebIdService(final String storage, final String issuer, final String this.issuerUrl = issuer; this.username = username; wireMockServer = new WireMockServer(WireMockConfiguration.options() - .httpDisabled(true) - .dynamicHttpsPort()); + .dynamicPort()); } private void setupMocks() { @@ -63,6 +62,11 @@ private void setupMocks() { wireMockServer.baseUrl() + "/" + this.username, storageUrl, issuerUrl)))); + + wireMockServer.stubFor(get(urlPathMatching("/([a-z]+)/([a-f0-9-]+)")) + .withHeader(USER_AGENT_HEADER, equalTo(USER_AGENT)) + .willReturn(aResponse() + .withStatus(404))); } public void start() { diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/SolidServerTransformer.java b/integration/base/src/main/java/com/inrupt/client/integration/base/SolidServerTransformer.java index 8cb2ce4a1a2..94bf8803b0a 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/SolidServerTransformer.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/SolidServerTransformer.java @@ -21,13 +21,12 @@ package com.inrupt.client.integration.base; import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder; -import com.github.tomakehurst.wiremock.common.FileSource; -import com.github.tomakehurst.wiremock.extension.Parameters; -import com.github.tomakehurst.wiremock.extension.ResponseDefinitionTransformer; -import com.github.tomakehurst.wiremock.http.Request; +import com.github.tomakehurst.wiremock.extension.ResponseDefinitionTransformerV2; import com.github.tomakehurst.wiremock.http.RequestMethod; import com.github.tomakehurst.wiremock.http.ResponseDefinition; +import com.github.tomakehurst.wiremock.stubbing.ServeEvent; import com.inrupt.client.Headers; +import com.inrupt.client.ProblemDetails; import com.inrupt.client.integration.base.MockSolidServer.ServerBody; import com.inrupt.client.vocabulary.PIM; @@ -37,7 +36,7 @@ import java.util.Map; import java.util.UUID; -class SolidServerTransformer extends ResponseDefinitionTransformer { +class SolidServerTransformer implements ResponseDefinitionTransformerV2 { private static final String SLASH = "/"; @@ -55,18 +54,21 @@ public String getName() { } @Override - public ResponseDefinition transform(final Request request, final ResponseDefinition responseDefinition, - final FileSource files, final Parameters parameters) { + public ResponseDefinition transform(final ServeEvent event) { final var res = new ResponseDefinitionBuilder(); + final var request = event.getRequest(); //determine if authenticated on private resources if (Utils.isPrivateResource(request.getUrl()) && request.getHeader("Authorization") == null) { + res.withStatus(Utils.UNAUTHORIZED); res.withHeader("WWW-Authenticate", "Bearer, DPoP algs=\"ES256\", UMA ticket=token-67890, as_uri=\"" + this.asUri + "\""); - res.withStatus(Utils.UNAUTHORIZED); + res.withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE); + res.withBody(problemDetails(Utils.UNAUTHORIZED, "Unauthorized", + "The client does not have a valid access token", UUID.randomUUID())); return res.build(); } @@ -84,7 +86,11 @@ public ResponseDefinition transform(final Request request, final ResponseDefinit } return res.build(); } else { - return res.withStatus(Utils.NOT_FOUND).build(); + return res.withStatus(Utils.NOT_FOUND) + .withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE) + .withBody(problemDetails(Utils.NOT_FOUND, "Not Found", "The resource does not exist", + UUID.randomUUID())) + .build(); } } @@ -99,9 +105,15 @@ public ResponseDefinition transform(final Request request, final ResponseDefinit res.withHeader("Location", location); } else { res.withStatus(Utils.NOT_FOUND); + res.withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE); + res.withBody(problemDetails(Utils.NOT_FOUND, "Not Found", "The resource does not exist", + UUID.randomUUID())); } } else { res.withStatus(Utils.NOT_ALLOWED); + res.withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE); + res.withBody(problemDetails(Utils.NOT_ALLOWED, "Method Not Allowed", + "The provided HTTP method is not allowed at this URL", UUID.randomUUID())); } return res.build(); } @@ -111,6 +123,9 @@ public ResponseDefinition transform(final Request request, final ResponseDefinit res.withStatus(Utils.SUCCESS); } else { res.withStatus(Utils.NOT_FOUND); + res.withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE); + res.withBody(problemDetails(Utils.NOT_FOUND, "Not Found", "The resource does not exist", + UUID.randomUUID())); } res.build(); } @@ -124,6 +139,9 @@ public ResponseDefinition transform(final Request request, final ResponseDefinit res.withStatus(Utils.NO_CONTENT); } else { res.withStatus(Utils.PRECONDITION_FAILED); + res.withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE); + res.withBody(problemDetails(Utils.PRECONDITION_FAILED, "Precondition Failed", + "The expected preconditions could not be fulfilled", UUID.randomUUID())); } return res.build(); } @@ -142,9 +160,15 @@ public ResponseDefinition transform(final Request request, final ResponseDefinit res.withStatus(Utils.NO_CONTENT); } catch (IOException e) { res.withStatus(Utils.ERROR); + res.withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE); + res.withBody(problemDetails(Utils.ERROR, "Internal Server Error", + "The server encountered an internal problem", UUID.randomUUID())); } } else { res.withStatus(Utils.ERROR); + res.withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE); + res.withBody(problemDetails(Utils.ERROR, "Internal Server Error", + "The server encountered an internal problem", UUID.randomUUID())); } return res.build(); } @@ -155,11 +179,18 @@ public ResponseDefinition transform(final Request request, final ResponseDefinit res.withStatus(Utils.NO_CONTENT); } else { res.withStatus(Utils.NOT_FOUND); + res.withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE); + res.withBody(problemDetails(Utils.NOT_FOUND, "Not Found", "The resource was not found", + UUID.randomUUID())); } return res.build(); } - return res.withStatus(Utils.NOT_ALLOWED).build(); + return res.withStatus(Utils.NOT_ALLOWED) + .withHeader(Utils.CONTENT_TYPE, ProblemDetails.MIME_TYPE) + .withBody(problemDetails(Utils.NOT_ALLOWED, "Method Not Allowed", + "The HTTP method is not allowed at this URL", UUID.randomUUID())) + .build(); } private void addSubContainersToStorage(final String path, final String mimeType) { @@ -174,4 +205,15 @@ private void addSubContainersToStorage(final String path, final String mimeType) } } + static String problemDetails(final int status, final String title, final String description, + final UUID instance) { + final var builder = new StringBuilder(); + builder.append("{"); + builder.append("\"status\":" + status); + builder.append(",\"title\":\"" + (title == null ? "(title)" : title) + "\""); + builder.append(",\"instance\":\"urn:uuid:" + (instance == null ? UUID.randomUUID() : instance) + "\""); + builder.append(",\"description\":\"" + (description == null ? "(description)" : description) + "\""); + builder.append("}"); + return builder.toString(); + } } diff --git a/integration/base/src/main/java/com/inrupt/client/integration/base/Utils.java b/integration/base/src/main/java/com/inrupt/client/integration/base/Utils.java index 67eef45c1bb..65703adceb0 100644 --- a/integration/base/src/main/java/com/inrupt/client/integration/base/Utils.java +++ b/integration/base/src/main/java/com/inrupt/client/integration/base/Utils.java @@ -22,9 +22,10 @@ import static com.inrupt.client.vocabulary.RDF.type; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import com.inrupt.client.Headers; +import com.inrupt.client.ProblemDetails; import com.inrupt.client.Request; import com.inrupt.client.Response; import com.inrupt.client.solid.*; @@ -119,6 +120,18 @@ public static byte[] modifyBody(final byte[] originalBody, final String requestB } } + public static Optional checkProblemDetails(final SolidClientException err) { + final ProblemDetails problemDetails = err.getProblemDetails(); + assertEquals(err.getStatusCode(), problemDetails.getStatus()); + if (err.getHeaders().firstValue(CONTENT_TYPE).equals(Optional.of(ProblemDetails.MIME_TYPE))) { + assertNotNull(problemDetails.getType(), "Type field should not be null!"); + return Optional.of(problemDetails); + } else { + assertEquals(ProblemDetails.DEFAULT_TYPE, problemDetails.getType()); + return Optional.empty(); + } + } + public static PublicJsonWebKey getDpopKey(final String resource) { try (final InputStream stream = Utils.class.getResourceAsStream(resource)) { final String jwks = IOUtils.toString(stream, UTF_8); diff --git a/integration/base/src/main/resources/query_endpoint_response.json b/integration/base/src/main/resources/query_endpoint_response.json new file mode 100644 index 00000000000..255db3217d8 --- /dev/null +++ b/integration/base/src/main/resources/query_endpoint_response.json @@ -0,0 +1,36 @@ +{ + "items": [{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v1.jsonld"], + "id":"{{baseUrl}}/vc-grant", + "type":["VerifiableCredential","SolidAccessGrant"], + "issuer":"{{baseUrl}}", + "expirationDate":"2024-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.example/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.example/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"{{ownerWebId}}", + "providedConsent":{ + "mode":["Read","Append"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + "isProvidedTo":"{{requesterWebId}}", + "forPurpose":["https://purpose.example/Purpose1"], + "forPersonalData":["{{sharedFile}}"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }], + "summary": { + "total": 1 + } +} diff --git a/integration/base/src/main/resources/query_endpoint_response_empty.json b/integration/base/src/main/resources/query_endpoint_response_empty.json new file mode 100644 index 00000000000..a1441d94816 --- /dev/null +++ b/integration/base/src/main/resources/query_endpoint_response_empty.json @@ -0,0 +1,6 @@ +{ + "items": [], + "summary": { + "total": 0 + } +} diff --git a/integration/base/src/main/resources/query_response.json b/integration/base/src/main/resources/query_response.json index f97ee47155c..9d411c6355d 100644 --- a/integration/base/src/main/resources/query_response.json +++ b/integration/base/src/main/resources/query_response.json @@ -30,4 +30,4 @@ "type":"Ed25519Signature2020", "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} }] -} \ No newline at end of file +} diff --git a/integration/base/src/main/resources/query_response_empty.json b/integration/base/src/main/resources/query_response_empty.json index 0ea0f73ccdf..d19d57ed7f3 100644 --- a/integration/base/src/main/resources/query_response_empty.json +++ b/integration/base/src/main/resources/query_response_empty.json @@ -1,4 +1,37 @@ { - "verifiableCredential": [] + "items": [{ + "@context":[ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/vc-revocation-list-2020/v1", + "https://schema.inrupt.com/credentials/v1.jsonld"], + "id":"{{baseUrl}}/vc-grant", + "type":["VerifiableCredential","SolidAccessGrant"], + "issuer":"{{baseUrl}}", + "expirationDate":"2024-08-27T12:00:00Z", + "issuanceDate":"2022-08-25T20:34:05.153Z", + "credentialStatus":{ + "id":"https://accessgrant.example/status/CVAM#2832", + "revocationListCredential":"https://accessgrant.example/status/CVAM", + "revocationListIndex":"2832", + "type":"RevocationList2020Status"}, + "credentialSubject":{ + "id":"{{ownerWebId}}", + "providedConsent":{ + "mode":["Read","Append"], + "hasStatus":"https://w3id.org/GConsent#ConsentStatusExplicitlyGiven", + "isProvidedTo":"{{requesterWebId}}", + "forPurpose":["https://purpose.example/Purpose1"], + "forPersonalData":["{{sharedFile}}"]}}, + "proof":{ + "created":"2022-08-25T20:34:05.236Z", + "proofPurpose":"assertionMethod", + "proofValue":"nIeQF44XVik7onnAbdkbp8xxJ2C8JoTw6-VtCkAzxuWYRFsSfYpft5MuAJaivyeKDmaK82Lj_YsME2xgL2WIBQ", + "type":"Ed25519Signature2020", + "verificationMethod":"https://accessgrant.example/key/1e332728-4af5-46e4-a5db-4f7b89e3f378"} + }], + "summary": { + "total": 3 + } } diff --git a/integration/base/src/main/resources/vc-configuration.json b/integration/base/src/main/resources/vc-configuration.json index 4a8398cdfe7..b9ac9c58582 100644 --- a/integration/base/src/main/resources/vc-configuration.json +++ b/integration/base/src/main/resources/vc-configuration.json @@ -9,5 +9,6 @@ "supportedSignatureTypes": [ "Ed25519Signature2020" ], - "verifierService": "{{baseUrl}}/verify" + "verifierService": "{{baseUrl}}/verify", + "queryService": "{{baseUrl}}/query" } diff --git a/integration/base/src/site/site.xml b/integration/base/src/site/site.xml index 2065e7f4dbc..47e6ca7c914 100644 --- a/integration/base/src/site/site.xml +++ b/integration/base/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/integration/base/src/test/java/com/inrupt/client/integration/base/Playlist.java b/integration/base/src/test/java/com/inrupt/client/integration/base/Playlist.java index f30435141b4..f3b743dbb62 100644 --- a/integration/base/src/test/java/com/inrupt/client/integration/base/Playlist.java +++ b/integration/base/src/test/java/com/inrupt/client/integration/base/Playlist.java @@ -20,7 +20,7 @@ */ package com.inrupt.client.integration.base; -import com.inrupt.client.solid.Metadata; +import com.inrupt.client.Headers; import com.inrupt.client.solid.SolidRDFSource; import com.inrupt.rdf.wrapping.commons.TermMappings; import com.inrupt.rdf.wrapping.commons.ValueMappings; @@ -40,8 +40,8 @@ class Playlist extends SolidRDFSource { private final IRI exSong; private final Node subject; - Playlist(final URI identifier, final Dataset dataset, final Metadata metadata) { - super(identifier, dataset, metadata); + Playlist(final URI identifier, final Dataset dataset, final Headers headers) { + super(identifier, dataset, headers); this.subject = new Node(rdf.createIRI(identifier.toString()), getGraph()); this.dcTitle = rdf.createIRI("http://purl.org/dc/terms/title"); diff --git a/integration/customokhttp/pom.xml b/integration/customokhttp/pom.xml deleted file mode 100644 index c6947873eac..00000000000 --- a/integration/customokhttp/pom.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - 4.0.0 - - com.inrupt.client - inrupt-client-integration-tests - 1.2.0-SNAPSHOT - - - inrupt-client-integration-customokhttp - Inrupt Java Client Libraries - Custom OkHttp - - Custom OkHttp Client for Integration tests for the Inrupt Java Client Libraries. - - - - 1.8 - 1.8 - - - - - com.inrupt.client - inrupt-client-api - ${project.version} - - - com.squareup.okhttp3 - okhttp - ${okhttp.version} - - - org.slf4j - slf4j-api - ${slf4j.version} - - - - - - com.squareup.okio - okio - 3.7.0 - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - - - org.jacoco - jacoco-maven-plugin - - - - diff --git a/integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/CustomOkHttpService.java b/integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/CustomOkHttpService.java deleted file mode 100644 index 572cc1998bc..00000000000 --- a/integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/CustomOkHttpService.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright Inrupt Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.inrupt.client.integration.customokhttp; - -import com.inrupt.client.Request; -import com.inrupt.client.Response; -import com.inrupt.client.spi.HttpService; - -import java.io.IOException; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.RequestBody; - -/** - * A {@link HttpService} using the {@code okhttp3.OkHttpClient}. - */ -public class CustomOkHttpService implements HttpService { - - static final Logger LOGGER = LoggerFactory.getLogger(CustomOkHttpService.class); - - private static final Set NO_BODY_METHODS = new HashSet<>(Arrays.asList("GET", "HEAD", "DELETE")); - - private final OkHttpClient client; - - /** - * Create an HTTP client service with a default {@link OkHttpClient}. - */ - public CustomOkHttpService() { - LOGGER.debug("Initializing CustomOkHttpClient service which trusts all certificates."); - this.client = trustAllCertsClient(); - } - - private CustomOkHttpService(final OkHttpClient client) { - LOGGER.debug("Initializing OkHttpClient service for HTTP client support"); - this.client = client; - } - - @Override - public CompletionStage> send(final Request request, final Response.BodyHandler handler) { - final CompletableFuture> future = new CompletableFuture<>(); - final okhttp3.Request req = prepareRequest(request); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Sending Request. Method: {}, URI: {}", req.method(), req.url()); - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("Request Headers: {}", req.headers()); - } - } - client.newCall(req).enqueue(new Callback() { - @Override - public void onResponse(final Call call, final okhttp3.Response res) throws IOException { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Response Status Code: {}", res.code()); - if (LOGGER.isTraceEnabled()) { - LOGGER.trace("Response Headers: {}", res.headers()); - } - } - try (final okhttp3.Response r = res) { - final Response.ResponseInfo info = new CustomOkHttpResponseInfo(r); - future.complete(new CustomOkHttpResponse<>(res.request().url().uri(), info, handler.apply(info))); - } catch (final RuntimeException ex) { - future.completeExceptionally(ex); - } - } - - @Override - public void onFailure(final Call call, final IOException ex) { - future.completeExceptionally(ex); - } - }); - - return future; - } - - /** - * Create an HTTP client service with a pre-configured {@link OkHttpClient}. - * - * @param client the OkHttpClient - * @return an HTTP client service - */ - public static CustomOkHttpService ofOkHttpClient(final OkHttpClient client) { - return new CustomOkHttpService(client); - } - - static RequestBody prepareBody(final Request request, final MediaType mediaType) { - if (NO_BODY_METHODS.contains(request.method())) { - return null; - } - return RequestBody.Companion.create(request.bodyPublisher() - .orElseGet(Request.BodyPublishers::noBody).getBytes().array(), mediaType); - } - - static okhttp3.Request prepareRequest(final Request request) { - final Headers headers = prepareHeaders(request.headers().asMap()); - final MediaType mediaType = getContentType(headers); - - return new okhttp3.Request.Builder() - .url(request.uri().toString()) - .headers(headers) - .method(request.method(), prepareBody(request, mediaType)) - .build(); - } - - static Headers prepareHeaders(final Map> headers) { - final Headers.Builder builder = Headers.of().newBuilder(); - for (final Map.Entry> entry : headers.entrySet()) { - for (final String value : entry.getValue()) { - builder.add(entry.getKey(), value); - } - } - return builder.build(); - } - - static MediaType getContentType(final Headers headers) { - final String ct = headers.get("Content-Type"); - if (ct != null) { - return MediaType.parse(ct); - } - return MediaType.parse("application/octet-stream"); - } - - OkHttpClient trustAllCertsClient() { - // setup trust all certificates - final TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - @Override - public void checkClientTrusted(final X509Certificate[] chain, - final String authType) { - } - - @Override - public void checkServerTrusted(final X509Certificate[] chain, - final String authType) { - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[]{}; - } - } - }; - - final SSLContext sslContext; - try { - sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustAllCerts, new SecureRandom()); - } catch (KeyManagementException e) { - throw new RuntimeException("There was a key management exception in the CustomOkHttp client."); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("There was a no such algorithm exception in the CustomOkHttp client."); - } - - final OkHttpClient.Builder newBuilder = new OkHttpClient.Builder(); - newBuilder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]); - newBuilder.hostnameVerifier((hostname, session) -> true); - - return newBuilder.build(); - } - -} diff --git a/integration/customokhttp/src/main/resources/META-INF/services/com.inrupt.client.spi.HttpService b/integration/customokhttp/src/main/resources/META-INF/services/com.inrupt.client.spi.HttpService deleted file mode 100644 index ef9b7676367..00000000000 --- a/integration/customokhttp/src/main/resources/META-INF/services/com.inrupt.client.spi.HttpService +++ /dev/null @@ -1 +0,0 @@ -com.inrupt.client.integration.customokhttp.CustomOkHttpService diff --git a/integration/customokhttp/src/site/resources/css/site.css b/integration/customokhttp/src/site/resources/css/site.css deleted file mode 100644 index 97fdf4abca6..00000000000 --- a/integration/customokhttp/src/site/resources/css/site.css +++ /dev/null @@ -1,22 +0,0 @@ -#banner img { - width: 200px; -} - -table.bodyTable tr.a { - background-color: #ddd; -} - -table.bodyTable tr.b { - background-color: #eee; -} - -div.sidebar-nav { - display: none; -} - -a.externalLink, -a.externalLink[href^=http], -a.externalLink[href^=https] { - background: none; - padding-right: 0px; -} diff --git a/integration/customokhttp/src/site/resources/images/inrupt_logo-2020.svg b/integration/customokhttp/src/site/resources/images/inrupt_logo-2020.svg deleted file mode 100644 index 05e59ed4a5d..00000000000 --- a/integration/customokhttp/src/site/resources/images/inrupt_logo-2020.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/integration/customokhttp/src/site/site.xml b/integration/customokhttp/src/site/site.xml deleted file mode 100644 index ccc9e51f451..00000000000 --- a/integration/customokhttp/src/site/site.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com - - - - - - - - - - diff --git a/integration/openid/pom.xml b/integration/openid/pom.xml index cde1d6d1cb1..617b180997d 100644 --- a/integration/openid/pom.xml +++ b/integration/openid/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client-integration-tests - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-integration-openid-tests diff --git a/integration/openid/src/site/site.xml b/integration/openid/src/site/site.xml index 1d4cfb39518..bbb4982f3ce 100644 --- a/integration/openid/src/site/site.xml +++ b/integration/openid/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/integration/pom.xml b/integration/pom.xml index 6874484dfb7..fc4d1deb64f 100644 --- a/integration/pom.xml +++ b/integration/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-integration-tests @@ -16,30 +16,11 @@ base - customokhttp openid uma - - com.inrupt.client - inrupt-client-httpclient - ${project.version} - test - - - com.inrupt.client - inrupt-client-jsonb - ${project.version} - test - - - org.eclipse - yasson - ${yasson.version} - test - com.inrupt.client inrupt-client-rdf4j @@ -67,22 +48,6 @@ - com.inrupt.client:inrupt-client-httpclient - com.inrupt.client:inrupt-client-jsonb - com.inrupt.client:inrupt-client-rdf4j - - - - - okhttp-jsonb-jena-test - test - - test - - - - com.inrupt.client:inrupt-client-httpclient - com.inrupt.client:inrupt-client-jackson com.inrupt.client:inrupt-client-rdf4j @@ -98,25 +63,6 @@ **/*JenaTest.java - com.inrupt.client:inrupt-client-httpclient - com.inrupt.client:inrupt-client-jsonb - com.inrupt.client:inrupt-client-jena - - - - - okhttp-jsonb-rdf4j-test - test - - test - - - - **/*JenaTest.java - - - com.inrupt.client:inrupt-client-httpclient - com.inrupt.client:inrupt-client-jackson com.inrupt.client:inrupt-client-jena @@ -133,6 +79,20 @@ true + + org.apache.maven.plugins + maven-pmd-plugin + + false + + + + org.apache.maven.plugins + maven-javadoc-plugin + + true + + diff --git a/integration/src/site/site.xml b/integration/src/site/site.xml index bbae83f8248..55f7825a037 100644 --- a/integration/src/site/site.xml +++ b/integration/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/integration/uma/pom.xml b/integration/uma/pom.xml index 168d2a96d84..5b50b639836 100644 --- a/integration/uma/pom.xml +++ b/integration/uma/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client-integration-tests - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-integration-uma-tests diff --git a/integration/uma/src/site/site.xml b/integration/uma/src/site/site.xml index 2e7996bc7ce..a5ef709f4e2 100644 --- a/integration/uma/src/site/site.xml +++ b/integration/uma/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/jackson/pom.xml b/jackson/pom.xml index 155cdccd65a..ae0908c5f5c 100644 --- a/jackson/pom.xml +++ b/jackson/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-jackson diff --git a/jackson/src/site/site.xml b/jackson/src/site/site.xml index fd45de0ae89..5da0b2c7a75 100644 --- a/jackson/src/site/site.xml +++ b/jackson/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/jena/pom.xml b/jena/pom.xml index 5fe41539dde..09e30407246 100644 --- a/jena/pom.xml +++ b/jena/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-jena @@ -13,6 +13,18 @@ Jena integration for the Inrupt Java Client Libraries. + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + com.inrupt.client @@ -98,7 +110,7 @@ - okhhttp-test + okhttp-test test test diff --git a/jena/src/site/site.xml b/jena/src/site/site.xml index 839332d8a2b..a12bfaab340 100644 --- a/jena/src/site/site.xml +++ b/jena/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/jena/src/test/java/com/inrupt/client/jena/JenaTestModel.java b/jena/src/test/java/com/inrupt/client/jena/JenaTestModel.java index 25c540eb293..d52eb6999e5 100644 --- a/jena/src/test/java/com/inrupt/client/jena/JenaTestModel.java +++ b/jena/src/test/java/com/inrupt/client/jena/JenaTestModel.java @@ -35,12 +35,12 @@ final class JenaTestModel { //JenaNode properties public static final Node S_NODE = NodeFactory.createURI(RdfTestModel.S_VALUE); public static final Node P_NODE = NodeFactory.createURI(RdfTestModel.P_VALUE); - public static final Node O_NODE = NodeFactory.createLiteral(RdfTestModel.O_VALUE); + public static final Node O_NODE = NodeFactory.createLiteral(RdfTestModel.O_VALUE, "", null); public static final Node G_NODE = NodeFactory.createURI(RdfTestModel.G_VALUE); public static final Node S1_NODE = NodeFactory.createURI(RdfTestModel.S1_VALUE); public static final Node P1_NODE = NodeFactory.createURI(RdfTestModel.P1_VALUE); - public static final Node O1_NODE = NodeFactory.createLiteral(RdfTestModel.O1_VALUE); + public static final Node O1_NODE = NodeFactory.createLiteral(RdfTestModel.O1_VALUE, "", null); //Jena Resources public static Model model = ModelFactory.createDefaultModel(); diff --git a/jsonb/pom.xml b/jsonb/pom.xml index 14fc7e3303d..7c8cce83fe5 100644 --- a/jsonb/pom.xml +++ b/jsonb/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-jsonb diff --git a/jsonb/src/site/site.xml b/jsonb/src/site/site.xml index 7ef33e4495b..1ed74045d47 100644 --- a/jsonb/src/site/site.xml +++ b/jsonb/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/mvnw b/mvnw index 8d937f4c14f..bd8896bf221 100755 --- a/mvnw +++ b/mvnw @@ -19,290 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.2.0 -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.4 # # Optional ENV vars # ----------------- -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /usr/local/etc/mavenrc ] ; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x -fi - -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false +# OS specific support. +native_path() { printf %s\\n "$1"; } case "$(uname)" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME - else - JAVA_HOME="/Library/Java/Home"; export JAVA_HOME - fi - fi - ;; +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; esac -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=$(java-config --jre-home) - fi -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --unix "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --unix "$CLASSPATH") -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && - JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="$(which javac)" - if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=$(which readlink) - if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then - if $darwin ; then - javaHome="$(dirname "\"$javaExecutable\"")" - javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" - else - javaExecutable="$(readlink -f "\"$javaExecutable\"")" - fi - javaHome="$(dirname "\"$javaExecutable\"")" - javaHome=$(expr "$javaHome" : '\(.*\)/bin') - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" else JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi fi else - JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" - fi -fi + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi +} - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=$(cd "$wdir/.." || exit 1; pwd) - fi - # end of workaround +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done - printf '%s' "$(cd "$basedir" || exit 1; pwd)" + printf %x\\n $h } -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - # Remove \r in case we run on Windows within Git Bash - # and check out the repository with auto CRLF management - # enabled. Otherwise, we may read lines that are delimited with - # \r\n and produce $'-Xarg\r' rather than -Xarg due to word - # splitting rules. - tr -s '\r\n' ' ' < "$1" - fi +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 } -log() { - if [ "$MVNW_VERBOSE" = true ]; then - printf '%s\n' "$1" - fi +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" } -BASE_DIR=$(find_maven_basedir "$(dirname "$0")") -if [ -z "$BASE_DIR" ]; then - exit 1; +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR -log "$MAVEN_PROJECTBASEDIR" +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" -if [ -r "$wrapperJarPath" ]; then - log "Found $wrapperJarPath" +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT else - log "Couldn't find $wrapperJarPath, downloading it ..." + die "cannot create temp dir" +fi - if [ -n "$MVNW_REPOURL" ]; then - wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - else - wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - fi - while IFS="=" read -r key value; do - # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) - safeValue=$(echo "$value" | tr -d '\r') - case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; - esac - done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" - log "Downloading from: $wrapperUrl" - - if $cygwin; then - wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") - fi +mkdir -p -- "${MAVEN_HOME%/*}" - if command -v wget > /dev/null; then - log "Found wget ... using wget" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - log "Found curl ... using curl" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - else - curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - fi - else - log "Falling back to using Java to download" - javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" - javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaSource=$(cygpath --path --windows "$javaSource") - javaClass=$(cygpath --path --windows "$javaClass") - fi - if [ -e "$javaSource" ]; then - if [ ! -e "$javaClass" ]; then - log " - Compiling MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/javac" "$javaSource") - fi - if [ -e "$javaClass" ]; then - log " - Running MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" - fi - fi - fi +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -########################################################################################## -# End of extension -########################################################################################## -# If specified, validate the SHA-256 sum of the Maven wrapper jar file -wrapperSha256Sum="" -while IFS="=" read -r key value; do - case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; - esac -done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" -if [ -n "$wrapperSha256Sum" ]; then - wrapperSha256Result=false - if command -v sha256sum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then - wrapperSha256Result=true +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true fi - elif command -v shasum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then - wrapperSha256Result=true + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." - echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 fi - if [ $wrapperSha256Result = false ]; then - echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 - echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 - echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 exit 1 fi fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --windows "$CLASSPATH") - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi fi -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" -export MAVEN_CMD_LINE_ARGS +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -# shellcheck disable=SC2086 # safe args -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index f80fbad3e76..5761d948924 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,3 +1,4 @@ +<# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @@ -18,188 +19,171 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.2.0 -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir +@REM Apache Maven Wrapper startup batch script, version 3.3.4 @REM @REM Optional ENV vars -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %WRAPPER_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file -SET WRAPPER_SHA_256_SUM="" -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) -IF NOT %WRAPPER_SHA_256_SUM%=="" ( - powershell -Command "&{"^ - "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ - "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ - " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ - " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ - " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ - " exit 1;"^ - "}"^ - "}" - if ERRORLEVEL 1 goto error -) - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/okhttp/pom.xml b/okhttp/pom.xml index c00dfa844ae..27ca9d96d08 100644 --- a/okhttp/pom.xml +++ b/okhttp/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-okhttp @@ -13,11 +13,6 @@ The OkHttp bindings for the Inrupt Java Client Libraries. - - 1.8 - 1.8 - - com.inrupt.client @@ -26,7 +21,7 @@ com.squareup.okhttp3 - okhttp + okhttp-jvm ${okhttp.version} @@ -40,7 +35,7 @@ com.squareup.okio okio - 3.7.0 + 3.17.0 diff --git a/okhttp/src/main/java/com/inrupt/client/okhttp/OkHttpService.java b/okhttp/src/main/java/com/inrupt/client/okhttp/OkHttpService.java index 411fb740901..3346de7973f 100644 --- a/okhttp/src/main/java/com/inrupt/client/okhttp/OkHttpService.java +++ b/okhttp/src/main/java/com/inrupt/client/okhttp/OkHttpService.java @@ -25,6 +25,7 @@ import com.inrupt.client.spi.HttpService; import java.io.IOException; +import java.time.Duration; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -58,7 +59,7 @@ public class OkHttpService implements HttpService { * Create an HTTP client service with a default {@link OkHttpClient}. */ public OkHttpService() { - this(new OkHttpClient()); + this(new OkHttpClient().newBuilder().callTimeout(Duration.ofSeconds(30)).build()); } private OkHttpService(final OkHttpClient client) { @@ -76,7 +77,7 @@ public CompletionStage> send(final Request request, final Respon LOGGER.trace("Request Headers: {}", req.headers()); } } - client.newCall(req).enqueue(new Callback() { + getClient(request).newCall(req).enqueue(new Callback() { @Override public void onResponse(final Call call, final okhttp3.Response res) throws IOException { if (LOGGER.isDebugEnabled()) { @@ -112,6 +113,14 @@ public static OkHttpService ofOkHttpClient(final OkHttpClient client) { return new OkHttpService(client); } + OkHttpClient getClient(final Request request) { + if (request.timeout().isPresent()) { + return client.newBuilder().readTimeout(request.timeout().get()) + .writeTimeout(request.timeout().get()).build(); + } + return client; + } + static RequestBody prepareBody(final Request request, final MediaType mediaType) { if (NO_BODY_METHODS.contains(request.method())) { return null; diff --git a/okhttp/src/site/site.xml b/okhttp/src/site/site.xml index 24627183214..69455be002f 100644 --- a/okhttp/src/site/site.xml +++ b/okhttp/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/openid/pom.xml b/openid/pom.xml index 89841e15834..5d7769124ff 100644 --- a/openid/pom.xml +++ b/openid/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-openid @@ -13,11 +13,6 @@ An OpenID library for the Inrupt Java Client Libraries. - - 1.8 - 1.8 - - com.inrupt.client diff --git a/openid/src/main/java/com/inrupt/client/openid/Metadata.java b/openid/src/main/java/com/inrupt/client/openid/Metadata.java index 2b6c806aec1..7ab2149cb00 100644 --- a/openid/src/main/java/com/inrupt/client/openid/Metadata.java +++ b/openid/src/main/java/com/inrupt/client/openid/Metadata.java @@ -112,4 +112,9 @@ public class Metadata { * A list of DPoP signing algorithm values supported by the given OpenID Connect provider. */ public List dpopSigningAlgValuesSupported; + + /** + * Indication of whether the OpenID Connect provider supports RFC-9207. + */ + public boolean authorizationResponseIssParameterSupported; } diff --git a/openid/src/main/java/com/inrupt/client/openid/OpenIdAuthenticationProvider.java b/openid/src/main/java/com/inrupt/client/openid/OpenIdAuthenticationProvider.java index 21af14ef9ca..a94e4d35d50 100644 --- a/openid/src/main/java/com/inrupt/client/openid/OpenIdAuthenticationProvider.java +++ b/openid/src/main/java/com/inrupt/client/openid/OpenIdAuthenticationProvider.java @@ -108,6 +108,11 @@ public CompletionStage authenticate(final Session session, final Req return CompletableFuture.completedFuture(session .getCredential(OpenIdSession.ID_TOKEN, request.uri()).orElse(null)); } + + @Override + public String toString() { + return getName(); + } } } diff --git a/openid/src/main/java/com/inrupt/client/openid/OpenIdProvider.java b/openid/src/main/java/com/inrupt/client/openid/OpenIdProvider.java index c71e9fafcd3..c1c5287a6ad 100644 --- a/openid/src/main/java/com/inrupt/client/openid/OpenIdProvider.java +++ b/openid/src/main/java/com/inrupt/client/openid/OpenIdProvider.java @@ -54,8 +54,18 @@ */ public class OpenIdProvider { + // OAuth 2 and OpenID request parameters + private static final String CLIENT_SECRET_BASIC = "client_secret_basic"; + private static final String CLIENT_SECRET_POST = "client_secret_post"; private static final String CLIENT_ID = "client_id"; + private static final String CODE_CHALLENGE = "code_challenge"; + private static final String CODE_CHALLENGE_METHOD = "code_challenge_method"; + private static final String NONCE = "nonce"; private static final String REDIRECT_URI = "redirect_uri"; + private static final String RESPONSE_TYPE = "response_type"; + private static final String SCOPE = "scope"; + private static final String STATE = "state"; + private static final String EQUALS = "="; private static final String ETC = "&"; @@ -155,11 +165,20 @@ private URI authorize(final URI authorizationEndpoint, final AuthorizationReques final URIBuilder builder = URIBuilder.newBuilder(authorizationEndpoint) .queryParam(CLIENT_ID, request.getClientId()) .queryParam(REDIRECT_URI, request.getRedirectUri().toString()) - .queryParam("response_type", request.getResponseType()); + .queryParam(RESPONSE_TYPE, request.getResponseType()) + .queryParam(SCOPE, request.getScope()); + + if (request.getState() != null) { + builder.queryParam(STATE, request.getState()); + } + + if (request.getNonce() != null) { + builder.queryParam(NONCE, request.getNonce()); + } if (request.getCodeChallenge() != null && request.getCodeChallengeMethod() != null) { - builder.queryParam("code_challenge", request.getCodeChallenge()); - builder.queryParam("code_challenge_method", request.getCodeChallengeMethod()); + builder.queryParam(CODE_CHALLENGE, request.getCodeChallenge()); + builder.queryParam(CODE_CHALLENGE_METHOD, request.getCodeChallengeMethod()); } return builder.build(); @@ -211,6 +230,15 @@ ErrorResponse tryParseError(final InputStream input) { } private Request tokenRequest(final Metadata metadata, final TokenRequest request) { + // RFC 9207 describes this behavior as a SHOULD but recognizes use cases that vary; + // this would be good to consider when adding broader configuration support to the libraries. + if (metadata.authorizationResponseIssParameterSupported) { + if (!Objects.equals(request.getIssuer(), metadata.issuer)) { + throw new OpenIdException("Issuer mismatch. " + + "Please verify that the designated OpenID issuer is correct"); + } + } + if (!metadata.grantTypesSupported.contains(request.getGrantType())) { throw new OpenIdException("Grant type [" + request.getGrantType() + "] is not supported by this provider."); } @@ -230,10 +258,10 @@ private Request tokenRequest(final Metadata metadata, final TokenRequest request final Optional authHeader; if (request.getClientSecret() != null) { - if ("client_secret_basic".equals(request.getAuthMethod())) { + if (CLIENT_SECRET_BASIC.equals(request.getAuthMethod())) { authHeader = getBasicAuthHeader(request.getClientId(), request.getClientSecret()); } else { - if ("client_secret_post".equals(request.getAuthMethod())) { + if (CLIENT_SECRET_POST.equals(request.getAuthMethod())) { data.put(CLIENT_ID, request.getClientId()); data.put("client_secret", request.getClientSecret()); } diff --git a/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java b/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java index 4c3188dd9ce..15609da0397 100644 --- a/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java +++ b/openid/src/main/java/com/inrupt/client/openid/OpenIdSession.java @@ -142,6 +142,7 @@ public static Session ofClientCredentials(final URI issuer, final String clientI return new OpenIdSession(id, dpop, () -> provider.token(TokenRequest.newBuilder() .clientSecret(clientSecret) .authMethod(authMethod) + .issuer(issuer) .scopes(config.getScopes().toArray(new String[0])) .build("client_credentials", clientId)) .thenApply(response -> { @@ -166,11 +167,13 @@ public static Session ofClientCredentials(final OpenIdProvider provider, final OpenIdConfig config) { final String id = UUID.randomUUID().toString(); final DPoP dpop = DPoP.of(config.getProofKeyPairs()); - return new OpenIdSession(id, dpop, () -> provider.token(TokenRequest.newBuilder() + return new OpenIdSession(id, dpop, () -> provider.metadata() + .thenCompose(metadata -> provider.token(TokenRequest.newBuilder() .clientSecret(clientSecret) .authMethod(authMethod) .scopes(config.getScopes().toArray(new String[0])) - .build("client_credentials", clientId)) + .issuer(metadata.issuer) + .build("client_credentials", clientId))) .thenApply(response -> { final JwtClaims claims = parseIdToken(response.idToken, config); return new Credential(response.tokenType, getIssuer(claims), response.idToken, @@ -289,7 +292,7 @@ static String getSessionIdentifier(final JwtClaims claims) { static String sha256(final String value) { final MessageDigest md = DigestUtils.getDigest("SHA-256"); - return new String(Hex.encodeHex(md.digest(value.getBytes(UTF_8)))); + return String.valueOf(Hex.encodeHex(md.digest(value.getBytes(UTF_8)))); } static Instant getExpiration(final JwtClaims claims) { diff --git a/openid/src/main/java/com/inrupt/client/openid/TokenRequest.java b/openid/src/main/java/com/inrupt/client/openid/TokenRequest.java index d36df366610..ecbaf03b23b 100644 --- a/openid/src/main/java/com/inrupt/client/openid/TokenRequest.java +++ b/openid/src/main/java/com/inrupt/client/openid/TokenRequest.java @@ -38,6 +38,7 @@ public final class TokenRequest { private final String clientSecret; private final String authMethod; private final URI redirectUri; + private final URI issuer; private final List scopes; /** @@ -58,6 +59,15 @@ public List getScopes() { return scopes; } + /** + * Get the issuer. + * + * @return the issuer, may be {@code null} + */ + public URI getIssuer() { + return issuer; + } + /** * Get the authentication method. * @@ -123,7 +133,8 @@ public static Builder newBuilder() { /* package-private */ TokenRequest(final String clientId, final String clientSecret, final URI redirectUri, final String grantType, - final String authMethod, final String code, final String codeVerifier, final List scopes) { + final String authMethod, final String code, final String codeVerifier, final URI issuer, + final List scopes) { this.clientId = clientId; this.clientSecret = clientSecret; this.redirectUri = redirectUri; @@ -132,6 +143,7 @@ public static Builder newBuilder() { this.code = code; this.codeVerifier = codeVerifier; this.scopes = scopes; + this.issuer = issuer; } /** @@ -140,12 +152,14 @@ public static Builder newBuilder() { public static class Builder { private static final String CLIENT_CREDENTIALS = "client_credentials"; + private static final String AUTHORIZATION_CODE = "authorization_code"; private String builderClientSecret; private String builderAuthMethod; private String builderCode; private String builderCodeVerifier; private URI builderRedirectUri; + private URI builderIssuer; private List builderScopes = new ArrayList<>(); /** @@ -181,6 +195,17 @@ public Builder scopes(final String... scopes) { return this; } + /** + * Set the issuer URI. + * + * @param issuer the issuer value + * @return this builder + */ + public Builder issuer(final URI issuer) { + builderIssuer = issuer; + return this; + } + /** * Set the authentication method for the token endpoint. * @@ -226,7 +251,7 @@ public TokenRequest build(final String grantType, final String clientId) { Objects.requireNonNull(clientId, "Client ID may not be null!"); final String grant = Objects.requireNonNull(grantType, "Grant type may not be null!"); - if ("authorization_code".equals(grantType)) { + if (AUTHORIZATION_CODE.equals(grantType)) { if (builderCode == null) { throw new IllegalArgumentException( "Missing code parameter for authorization_code grant type"); @@ -240,7 +265,7 @@ public TokenRequest build(final String grantType, final String clientId) { } return new TokenRequest(clientId, builderClientSecret, builderRedirectUri, grant, builderAuthMethod, - builderCode, builderCodeVerifier, builderScopes); + builderCode, builderCodeVerifier, builderIssuer, builderScopes); } } } diff --git a/openid/src/site/site.xml b/openid/src/site/site.xml index 96d728144d1..ba29346d524 100644 --- a/openid/src/site/site.xml +++ b/openid/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/openid/src/test/java/com/inrupt/client/openid/OpenIdMockHttpService.java b/openid/src/test/java/com/inrupt/client/openid/OpenIdMockHttpService.java index 18a0591584e..2a5a6d690a0 100644 --- a/openid/src/test/java/com/inrupt/client/openid/OpenIdMockHttpService.java +++ b/openid/src/test/java/com/inrupt/client/openid/OpenIdMockHttpService.java @@ -75,19 +75,16 @@ private void setupMocks() { .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") - .withBody(getMetadataJSON() - .replace( - "/oauth/oauth20/token", - wireMockServer.baseUrl() + "/oauth/oauth20/token")))); + .withBody(getMetadataJSON()))); - wireMockServer.stubFor(post(urlPathMatching("/oauth/oauth20/token")) + wireMockServer.stubFor(post(urlPathMatching("/token")) .withHeader("Content-Type", containing("application/x-www-form-urlencoded")) .withRequestBody(containing("myCodeverifier")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(getTokenResponseJSON()))); - wireMockServer.stubFor(post(urlPathMatching("/oauth/oauth20/token")) + wireMockServer.stubFor(post(urlPathMatching("/token")) .withHeader("Content-Type", containing("application/x-www-form-urlencoded")) .atPriority(1) .withRequestBody(containing("code=none")) @@ -96,7 +93,7 @@ private void setupMocks() { .withHeader("Content-Type", "application/json") .withBodyFile("token-error.json"))); - wireMockServer.stubFor(post(urlPathMatching("/oauth/oauth20/token")) + wireMockServer.stubFor(post(urlPathMatching("/token")) .withHeader("Content-Type", containing("application/x-www-form-urlencoded")) .atPriority(2) .withRequestBody(containing("grant_type=client_credentials")) @@ -108,7 +105,8 @@ private void setupMocks() { private String getMetadataJSON() { try (final InputStream res = OpenIdMockHttpService.class.getResourceAsStream("/metadata.json")) { - return new String(IOUtils.toByteArray(res), UTF_8); + return new String(IOUtils.toByteArray(res), UTF_8) + .replace("http://example.test", wireMockServer.baseUrl()); } catch (final IOException ex) { throw new UncheckedIOException("Could not read class resource", ex); } @@ -130,9 +128,6 @@ private String getTokenResponseJSON() { "}"; } - - - public String start() { wireMockServer.start(); diff --git a/openid/src/test/java/com/inrupt/client/openid/OpenIdProviderTest.java b/openid/src/test/java/com/inrupt/client/openid/OpenIdProviderTest.java index 2b1827b8493..7fb152c3532 100644 --- a/openid/src/test/java/com/inrupt/client/openid/OpenIdProviderTest.java +++ b/openid/src/test/java/com/inrupt/client/openid/OpenIdProviderTest.java @@ -25,6 +25,7 @@ import com.inrupt.client.auth.DPoP; import com.inrupt.client.openid.TokenRequest.Builder; +import com.inrupt.client.util.URIBuilder; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -33,8 +34,6 @@ import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.OptionalInt; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -49,14 +48,14 @@ class OpenIdProviderTest { private static OpenIdProvider openIdProvider; private static final OpenIdMockHttpService mockHttpService = new OpenIdMockHttpService(); - private static final Map config = new HashMap<>(); private static final DPoP dpop = DPoP.of(); + private static URI issuer; @BeforeAll static void setup() throws NoSuchAlgorithmException { - config.put("openid_uri", mockHttpService.start()); - openIdProvider = new OpenIdProvider(URI.create(config.get("openid_uri")), dpop); + issuer = URI.create(mockHttpService.start()); + openIdProvider = new OpenIdProvider(issuer, dpop); } @AfterAll @@ -66,15 +65,16 @@ static void teardown() { @Test void metadataAsyncTest() { - assertEquals("http://example.test", - openIdProvider.metadata().toCompletableFuture().join().issuer.toString()); - assertEquals("http://example.test/oauth/jwks", - openIdProvider.metadata().toCompletableFuture().join().jwksUri.toString()); + assertEquals(issuer, + openIdProvider.metadata().toCompletableFuture().join().issuer); + assertEquals(URIBuilder.newBuilder(issuer).path("jwks").build(), + openIdProvider.metadata().toCompletableFuture().join().jwksUri); } @Test void unknownMetadata() { - final OpenIdProvider provider = new OpenIdProvider(URI.create(config.get("openid_uri") + "/not-found"), dpop); + final OpenIdProvider provider = new OpenIdProvider(URIBuilder.newBuilder(issuer).path("not-found").build(), + dpop); final CompletionException err = assertThrows(CompletionException.class, provider.metadata().toCompletableFuture()::join); assertTrue(err.getCause() instanceof OpenIdException); @@ -83,6 +83,8 @@ void unknownMetadata() { @Test void authorizeAsyncTest() { final AuthorizationRequest authReq = AuthorizationRequest.newBuilder() + .scope("openid") + .scope("webid") .codeChallenge("myCodeChallenge") .codeChallengeMethod("method") .build( @@ -90,8 +92,28 @@ void authorizeAsyncTest() { URI.create("myRedirectUri") ); assertEquals( - "http://example.test/auth?client_id=myClientId&redirect_uri=myRedirectUri&" + - "response_type=code&code_challenge=myCodeChallenge&code_challenge_method=method", + issuer + "/auth?client_id=myClientId&redirect_uri=myRedirectUri&" + + "response_type=code&scope=openid%20webid&code_challenge=myCodeChallenge&code_challenge_method=method", + openIdProvider.authorize(authReq).toCompletableFuture().join().toString() + ); + } + + @Test + void authorizeAsyncStateNonceTest() { + final String state = UUID.randomUUID().toString(); + final String nonce = UUID.randomUUID().toString(); + final AuthorizationRequest authReq = AuthorizationRequest.newBuilder() + .scope("openid") + .scope("webid") + .state(state) + .nonce(nonce) + .build( + "myClientId", + URI.create("myRedirectUri") + ); + assertEquals( + issuer + "/auth?client_id=myClientId&redirect_uri=myRedirectUri&" + + "response_type=code&scope=openid%20webid&state=" + state + "&nonce=" + nonce, openIdProvider.authorize(authReq).toCompletableFuture().join().toString() ); } @@ -114,11 +136,67 @@ void tokenRequestIllegalArgumentsTest() { () -> builder.build("myGrantType", null)); } + @Test + void tokenIssuerMismatch() { + final TokenRequest tokenReq = TokenRequest.newBuilder() + .code("someCode") + .codeVerifier("myCodeverifier") + .issuer(URI.create("https://not.an.issuer.test")) + .redirectUri(URI.create("https://example.test/redirectUri")) + .build( + "authorization_code", + "myClientId" + ); + + final CompletionException ex = assertThrows(CompletionException.class, openIdProvider.token(tokenReq) + .toCompletableFuture()::join); + assertTrue(ex.getCause() instanceof OpenIdException); + final OpenIdException cause = (OpenIdException) ex.getCause(); + assertTrue(cause.getMessage().contains("Issuer mismatch")); + } + + @Test + void tokenIssuerMissing() { + final TokenRequest tokenReq = TokenRequest.newBuilder() + .code("someCode") + .codeVerifier("myCodeverifier") + .redirectUri(URI.create("https://example.test/redirectUri")) + .build( + "authorization_code", + "myClientId" + ); + + final CompletionException ex = assertThrows(CompletionException.class, openIdProvider.token(tokenReq) + .toCompletableFuture()::join); + assertTrue(ex.getCause() instanceof OpenIdException); + final OpenIdException cause = (OpenIdException) ex.getCause(); + assertTrue(cause.getMessage().contains("Issuer mismatch")); + } + + @Test + void tokenIssuerMatch() { + final TokenRequest tokenReq = TokenRequest.newBuilder() + .code("someCode") + .codeVerifier("myCodeverifier") + .issuer(issuer) + .redirectUri(URI.create("https://example.test/redirectUri")) + .build( + "authorization_code", + "myClientId" + ); + final TokenResponse token = openIdProvider.token(tokenReq) + .toCompletableFuture().join(); + assertEquals("123456", token.accessToken); + assertNotNull(token.idToken); + assertEquals("Bearer", token.tokenType); + } + @Test void tokenNoClientSecretTest() { final TokenRequest tokenReq = TokenRequest.newBuilder() .code("someCode") .codeVerifier("myCodeverifier") + .issuer(issuer) .redirectUri(URI.create("https://example.test/redirectUri")) .build( "authorization_code", @@ -137,6 +215,7 @@ void tokenWithClientSecretBasicTest() { .code("someCode") .codeVerifier("myCodeverifier") .clientSecret("myClientSecret") + .issuer(issuer) .authMethod("client_secret_basic") .redirectUri(URI.create("https://example.test/redirectUri")) .build( @@ -156,6 +235,7 @@ void tokenWithClientSecretePostTest() { .code("someCode") .codeVerifier("myCodeverifier") .clientSecret("myClientSecret") + .issuer(issuer) .authMethod("client_secret_post") .redirectUri(URI.create("https://example.test/redirectUri")) .build( @@ -174,6 +254,7 @@ void tokenAsyncTest() { final TokenRequest tokenReq = TokenRequest.newBuilder() .code("someCode") .codeVerifier("myCodeverifier") + .issuer(issuer) .redirectUri(URI.create("https://example.test/redirectUri")) .build("authorization_code", "myClientId"); final TokenResponse token = openIdProvider.token(tokenReq).toCompletableFuture().join(); @@ -187,6 +268,7 @@ void tokenAsyncStatusCodesTest() { final TokenRequest tokenReq = TokenRequest.newBuilder() .code("none") .codeVerifier("none") + .issuer(issuer) .redirectUri(URI.create("none")) .build("authorization_code", "none"); @@ -217,7 +299,7 @@ void endSessionAsyncTest() { .build(); final URI uri = openIdProvider.endSession(endReq).toCompletableFuture().join(); assertEquals( - "http://example.test/endSession?" + + issuer + "/endSession?" + "client_id=myClientId&post_logout_redirect_uri=https://example.test/redirectUri&id_token_hint=&state=solid", uri.toString() ); diff --git a/openid/src/test/resources/metadata.json b/openid/src/test/resources/metadata.json index 74c905732ee..034c33e55ca 100644 --- a/openid/src/test/resources/metadata.json +++ b/openid/src/test/resources/metadata.json @@ -1,34 +1,20 @@ { "issuer":"http://example.test", - "token_endpoint":"/oauth/oauth20/token", + "token_endpoint":"http://example.test/token", "end_session_endpoint":"http://example.test/endSession", - "userinfo_endpoint":"http://example.test/oauth/userinfo", - "jwks_uri":"http://example.test/oauth/jwks", + "userinfo_endpoint":"http://example.test/userinfo", + "jwks_uri":"http://example.test/jwks", "authorization_endpoint":"http://example.test/auth", "scopes_supported":[ - "READ", - "WRITE", - "DELETE", "openid", - "scope", "profile", - "email", - "address", - "phone" + "email" ], "response_types_supported":[ - "code", - "code id_token", - "code token", - "code id_token token", - "token", - "id_token", - "id_token token" + "code" ], "grant_types_supported":[ "authorization_code", - "implicit", - "password", "client_credentials", "urn:ietf:params:oauth:grant-type:jwt-bearer" ], @@ -36,30 +22,8 @@ "public" ], "id_token_signing_alg_values_supported":[ - "HS256", - "HS384", - "HS512", "RS256", - "RS384", - "RS512", - "ES256", - "ES384", - "ES512", - "PS256", - "PS384", - "PS512" - ], - "id_token_encryption_alg_values_supported":[ - "RSA1_5", - "RSA-OAEP", - "RSA-OAEP-256", - "A128KW", - "A192KW", - "A256KW", - "A128GCMKW", - "A192GCMKW", - "A256GCMKW", - "dir" + "ES256" ], "token_endpoint_auth_methods_supported":[ "client_secret_post", @@ -69,5 +33,6 @@ ], "dpop_signing_alg_values_supported":[ "RS256", "ES256" - ] + ], + "authorization_response_iss_parameter_supported": true } diff --git a/parser/pom.xml b/parser/pom.xml index cd15afa28ef..827a0516331 100644 --- a/parser/pom.xml +++ b/parser/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-parser @@ -13,11 +13,6 @@ Parsing utilities for the Inrupt Java Client Libraries. - - 1.8 - 1.8 - - org.antlr diff --git a/parser/src/site/site.xml b/parser/src/site/site.xml index 4e77c763e6c..0959c5d56e7 100644 --- a/parser/src/site/site.xml +++ b/parser/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/performance/base/pom.xml b/performance/base/pom.xml index 7e85254565f..c4e320ac938 100644 --- a/performance/base/pom.xml +++ b/performance/base/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client-performance-tests - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-performance-base-tests Inrupt Java Client Libraries - Performance Base Tests @@ -14,9 +14,20 @@ 2.1.1 - 3.5.2 + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + + com.inrupt.client @@ -76,7 +87,6 @@ com.fasterxml.jackson.core jackson-annotations - ${jackson.version} io.smallrye.config @@ -108,7 +118,7 @@ org.wiremock - wiremock + wiremock-standalone ${wiremock.version} provided diff --git a/performance/base/src/site/site.xml b/performance/base/src/site/site.xml index 97788bf6322..fb09ed709be 100644 --- a/performance/base/src/site/site.xml +++ b/performance/base/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/performance/pom.xml b/performance/pom.xml index b664da4e51c..39406ba0a71 100644 --- a/performance/pom.xml +++ b/performance/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-performance-tests @@ -193,6 +193,20 @@ true + + org.apache.maven.plugins + maven-pmd-plugin + + false + + + + org.apache.maven.plugins + maven-javadoc-plugin + + true + + diff --git a/performance/src/site/site.xml b/performance/src/site/site.xml index 054d92355a1..7822737836a 100644 --- a/performance/src/site/site.xml +++ b/performance/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/performance/uma/pom.xml b/performance/uma/pom.xml index 55971a8afa2..9190fd0e8a8 100644 --- a/performance/uma/pom.xml +++ b/performance/uma/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client-performance-tests - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-performance-uma-tests diff --git a/performance/uma/src/site/site.xml b/performance/uma/src/site/site.xml index d4a0cd965ab..1d7c2084357 100644 --- a/performance/uma/src/site/site.xml +++ b/performance/uma/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/pom.xml b/pom.xml index 95e9918fcd2..eb9696b2062 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT Inrupt Java Client Libraries https://docs.inrupt.com/developer-tools/java/client-libraries/ @@ -14,88 +14,86 @@ true - 11 - 11 + 17 UTF-8 UTF-8 - 4.13.1 - 3.1.8 - 1.16.0 - 1.6.0 - 2.15.1 + 4.13.2 + 3.2.4 + 1.22.0 + 1.11.0 + 2.22.0 0.5.0 - 33.0.0-jre - 2.16.1 + 33.6.0-jre + 2.22.0 2.1.3 - 4.10.0 - 0.9.4 - 3.0.0 - 4.12.0 - 3.6.5 - 2.0.11 - - 5.8.8 + 6.1.0 + 0.9.6 + 3.0.2 + 5.4.0 + 3.37.0 + 5.3.2 + 2.0.18 + 7.1.0 0.6.0 - 1.0.0 + 1.1.1 - 3.1.0 - 3.2.1 - 3.6.0 - 3.3.1 - 3.3.2 - 3.12.1 - 3.6.1 - 3.1.1 - 3.4.1 - 3.1.1 - 3.1.1 - 3.1.0 - 0.8.11 - 3.3.0 - 3.6.3 - 4.3 - 1.6.13 - 9.0.8 - 3.5.0 - 3.21.2 - 3.12.1 - 3.0.1 - 3.3.1 - 3.10.0.2594 - 3.3.0 - 3.2.5 + 3.2.0 + 3.4.1 + 3.8.0 + 3.6.0 + 3.5.0 + 3.15.0 + 3.11.0 + 3.1.4 + 3.6.3 + 3.5.0 + 3.1.4 + 3.2.8 + 0.8.15 + 3.5.0 + 3.12.0 + 5.0.0 + 3.9.0 + 3.28.0 + 3.22.0 + 3.3.1 + 3.5.0 + 5.7.0.6970 + 0.11.0 + 3.4.0 + 3.5.6 - 10.12.7 + 13.7.0 - 4.2.0 - 3.15.6 + 4.3.0 + 4.5 2.0.1 - 5.10.1 - 4.4.0 - 3.5.2 - 3.0.3 - 3.3.1 + 6.1.1 + 4.6.3 + 3.17.2 + 3.0.4 + 3.13.2 + 3.0 - true true - - ${maven.multiModuleProjectDirectory}/reports/target/site/jacoco-merged/jacoco.xml - https://sonarqube.dev.inrupt.com + https://sonarcloud.io + inrupt solid-client-java ${project.artifactId} access-grant + acp api bom caffeine @@ -111,31 +109,20 @@ parser quarkus rdf4j - rdf-legacy solid + spring test uma webid vocabulary runtime - spring integration performance - reports archetypes + reports - - Snapshot Repository - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots - - - Staging Repository - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2 - ${project.artifactId} ${project.baseUri} @@ -215,7 +202,6 @@ https://jena.apache.org/documentation/javadoc/arq/ https://www.antlr.org/api/Java/ https://rdf4j.org/javadoc/latest/ - http://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/
org.codehaus.mojo extra-enforcer-rules - 1.7.0 + 1.12.0 @@ -297,6 +283,7 @@ + org.apache.maven.plugins maven-source-plugin ${source.plugin.version} @@ -309,6 +296,7 @@ + org.apache.maven.plugins maven-compiler-plugin ${compiler.plugin.version} @@ -319,6 +307,7 @@ + org.apache.maven.plugins maven-surefire-plugin ${surefire.plugin.version} @@ -329,6 +318,7 @@ + org.apache.maven.plugins maven-failsafe-plugin ${surefire.plugin.version} @@ -484,15 +474,9 @@ ${sonar.plugin.version} - org.sonatype.plugins - nexus-staging-maven-plugin - ${nexus.plugin.version} - true - - ossrh - https://s01.oss.sonatype.org/ - true - + org.sonatype.central + central-publishing-maven-plugin + ${sonatype.plugin.version} org.antlr @@ -506,11 +490,6 @@ - - org.owasp - dependency-check-maven - ${owasp.plugin.version} - @@ -586,18 +565,22 @@ com.mycila license-maven-plugin -
./build-tools/license/HEADER.txt
SLASHSTAR_STYLE - - **/src/main/resources/** - **/src/test/resources/** - - - **/src/main/java/** - **/src/test/java/** - + + +
./build-tools/license/HEADER.txt
+ + **/src/main/resources/** + **/src/test/resources/** + + + **/src/main/java/** + **/src/test/java/** + +
+
@@ -608,28 +591,28 @@ - org.owasp - dependency-check-maven - - - - check - - - + org.sonatype.central + central-publishing-maven-plugin + true - 7 - true - - HTML - JSON - CSV - - - ./build-tools/owasp/suppressions.xml - - ${nvd.api.key} - ${nvd.api.datafeed} + central + true + + inrupt-client-examples-cli + inrupt-client-examples-parent + inrupt-client-examples-spring-web + inrupt-client-examples-springboot + inrupt-client-examples-webapp + inrupt-client-integration-base-tests + inrupt-client-integration-openid-tests + inrupt-client-integration-tests + inrupt-client-integration-uma-tests + inrupt-client-performance-base-tests + inrupt-client-performance-tests + inrupt-client-performance-uma-tests + inrupt-client-report + inrupt-client-test + @@ -730,27 +713,20 @@ - - org.owasp - dependency-check-maven - ${owasp.plugin.version} - - - - aggregate - - - - - - ./build-tools/owasp/suppressions.xml - - - + + jena5 + + (,21) + + + 5.6.0 + 12.3.1 + + publish @@ -800,27 +776,6 @@ true - - dependencies - - true - true - true - false - true - true - true - - - - java-21 - - [11,21) - - - gradle - - @@ -845,7 +800,7 @@ scm:git:https://github.com/inrupt/solid-client-java.git scm:git:git@github.com:inrupt/solid-client-java.git https://github.com/inrupt/solid-client-java - HEAD + inrupt-client-1.2.0 diff --git a/quarkus/pom.xml b/quarkus/pom.xml index ba9d86f8120..60e4f1f1af8 100644 --- a/quarkus/pom.xml +++ b/quarkus/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-quarkus @@ -13,15 +13,17 @@ Integration utilities for Quarkus support. - - 11 - 11 - - - io.quarkus + org.junit + junit-bom + ${junit.version} + pom + import + + + io.quarkus.platform quarkus-bom ${quarkus.version} pom @@ -57,13 +59,11 @@ org.junit.jupiter junit-jupiter-engine - ${junit.version} test org.junit.jupiter junit-jupiter-api - ${junit.version} test diff --git a/quarkus/src/main/java/com/inrupt/client/quarkus/SessionUtils.java b/quarkus/src/main/java/com/inrupt/client/quarkus/SessionUtils.java index fd646dfed69..0f49984f25b 100644 --- a/quarkus/src/main/java/com/inrupt/client/quarkus/SessionUtils.java +++ b/quarkus/src/main/java/com/inrupt/client/quarkus/SessionUtils.java @@ -28,6 +28,10 @@ import org.eclipse.microprofile.jwt.JsonWebToken; +/** + * A utility class for converting a Quarkus (Microprofile) {@link JsonWebToken} to session objects + * for use with the Java Client libraries. + */ public final class SessionUtils { /** diff --git a/solid/src/test/java/com/inrupt/client/solid/DeprecatedBinary.java b/quarkus/src/main/java/com/inrupt/client/quarkus/package-info.java similarity index 76% rename from solid/src/test/java/com/inrupt/client/solid/DeprecatedBinary.java rename to quarkus/src/main/java/com/inrupt/client/quarkus/package-info.java index 08d46762a8c..825ce37efb8 100644 --- a/solid/src/test/java/com/inrupt/client/solid/DeprecatedBinary.java +++ b/quarkus/src/main/java/com/inrupt/client/quarkus/package-info.java @@ -18,14 +18,14 @@ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.inrupt.client.solid; - -import java.io.InputStream; -import java.net.URI; - -public class DeprecatedBinary extends SolidNonRDFSource { - public DeprecatedBinary(final URI identifier, final String contentType, final InputStream entity, - final Metadata metadata) { - super(identifier, contentType, entity, metadata); +/** + *

Integration tooling for Quarkus applications.

+ * + *

When integrating with Quarkus applications, one can convert a Microprofile JsonWebToken into a Session object: + * + *

{@code
+    Optional session = SessionUtils.asSession(jwt);
     }
-}
+ * 
+ */ +package com.inrupt.client.quarkus; diff --git a/quarkus/src/test/java/com/inrupt/client/quarkus/SessionUtilsTest.java b/quarkus/src/test/java/com/inrupt/client/quarkus/SessionUtilsTest.java index f8d8c2e22e5..253155aa1a6 100644 --- a/quarkus/src/test/java/com/inrupt/client/quarkus/SessionUtilsTest.java +++ b/quarkus/src/test/java/com/inrupt/client/quarkus/SessionUtilsTest.java @@ -36,6 +36,7 @@ import java.net.URI; import java.time.Instant; import java.util.Optional; +import java.util.Set; import org.eclipse.microprofile.jwt.JsonWebToken; import org.jose4j.jwk.PublicJsonWebKey; @@ -81,7 +82,7 @@ static JsonWebToken generateIdToken(final String sub, final String issuer, final .newPublicJwk(ResourceUtils.readResource("testKey.json")); final var authContext = new JWTAuthContextInfo(jwk.getPublicKey(), issuer); - authContext.setSignatureAlgorithm(SignatureAlgorithm.ES256); + authContext.setSignatureAlgorithm(Set.of(SignatureAlgorithm.ES256)); final var parser = new DefaultJWTParser(authContext); final var token = Jwt.claims() diff --git a/rdf-legacy/pom.xml b/rdf-legacy/pom.xml deleted file mode 100644 index ee95325b3ac..00000000000 --- a/rdf-legacy/pom.xml +++ /dev/null @@ -1,210 +0,0 @@ - - - 4.0.0 - - com.inrupt.client - inrupt-client - 1.2.0-SNAPSHOT - - - inrupt-client-rdf-legacy - Inrupt Java Client Libraries - Java 8 RDF Support - - Java 8 RDF support for the Inrupt Java Client Libraries. - - - - 1.8 - 1.8 - - - 1.11.0 - - 3.7.7 - - - - - - org.junit - junit-bom - ${junit.version} - pom - import - - - - com.fasterxml.jackson - jackson-bom - ${jackson.version} - pom - import - - - - org.eclipse.rdf4j - rdf4j-bom - ${legacy.rdf4j.version} - pom - import - - - - - - - com.inrupt.client - inrupt-client-api - ${project.version} - - - com.inrupt.rdf - inrupt-commons-rdf4j - ${inrupt.commons.rdf4j.version} - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.eclipse.rdf4j - rdf4j-rio-api - - - org.eclipse.rdf4j - rdf4j-model - - - org.eclipse.rdf4j - rdf4j-query - - - org.eclipse.rdf4j - rdf4j-repository-api - - - org.eclipse.rdf4j - rdf4j-repository-sparql - - - org.eclipse.rdf4j - rdf4j-rio-turtle - - - org.eclipse.rdf4j - rdf4j-rio-trig - - - org.eclipse.rdf4j - rdf4j-rio-ntriples - - - org.eclipse.rdf4j - rdf4j-rio-nquads - - - org.eclipse.rdf4j - rdf4j-repository-sail - - - org.eclipse.rdf4j - rdf4j-sail-memory - - - - - org.apache.commons - commons-text - ${commons.text.version} - - - - - org.wiremock - wiremock - ${wiremock.version} - test - - - org.slf4j - slf4j-simple - ${slf4j.version} - test - - - com.inrupt.client - inrupt-client-httpclient - ${project.version} - test - - - com.inrupt.client - inrupt-client-okhttp - ${project.version} - test - - - com.inrupt.client - inrupt-client-test - ${project.version} - test - - - org.junit.jupiter - junit-jupiter-api - ${junit.version} - test - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - default-test - none - - - httpclient-test - test - - test - - - - com.inrupt.client:inrupt-client-okhttp - - - - - okhttp-test - test - - test - - - - com.inrupt.client:inrupt-client-httpclient - - - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - - - - - org.jacoco - jacoco-maven-plugin - - - - diff --git a/rdf-legacy/src/main/java/com/inrupt/client/rdf/legacy/RDFLegacyService.java b/rdf-legacy/src/main/java/com/inrupt/client/rdf/legacy/RDFLegacyService.java deleted file mode 100644 index ade9a0249f0..00000000000 --- a/rdf-legacy/src/main/java/com/inrupt/client/rdf/legacy/RDFLegacyService.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Inrupt Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.inrupt.client.rdf.legacy; - -import com.inrupt.client.spi.RdfService; -import com.inrupt.commons.rdf4j.RDF4J; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Stream; - -import org.apache.commons.rdf.api.Dataset; -import org.apache.commons.rdf.api.Graph; -import org.apache.commons.rdf.api.RDFSyntax; -import org.eclipse.rdf4j.model.Model; -import org.eclipse.rdf4j.model.Statement; -import org.eclipse.rdf4j.repository.Repository; -import org.eclipse.rdf4j.repository.RepositoryConnection; -import org.eclipse.rdf4j.repository.sail.SailRepository; -import org.eclipse.rdf4j.rio.RDFFormat; -import org.eclipse.rdf4j.rio.RDFWriter; -import org.eclipse.rdf4j.rio.Rio; -import org.eclipse.rdf4j.sail.memory.MemoryStore; - -/** - * An {@link RdfService} that uses a Java 8-compatible version of the RDF4J library. - */ -public class RDFLegacyService implements RdfService { - - private static final Map SYNTAX_TO_FORMAT = buildSyntaxMapping(); - private static final RDF4J rdf = new RDF4J(); - - @Override - public void fromDataset(final Dataset dataset, final RDFSyntax syntax, final OutputStream output) - throws IOException { - final RDFFormat format = Objects.requireNonNull(SYNTAX_TO_FORMAT.get(syntax)); - final RDFWriter writer = Rio.createWriter(format, output); - try { - writer.startRDF(); - try (final Stream statements = dataset.stream().map(rdf::asStatement)) { - statements.sequential().forEach(writer::handleStatement); - } - writer.endRDF(); - } catch (final org.eclipse.rdf4j.RDF4JException ex) { - throw new IOException("Error serializing dataset", ex); - } - } - - @Override - public void fromGraph(final Graph graph, final RDFSyntax syntax, final OutputStream output) throws IOException { - final RDFFormat format = Objects.requireNonNull(SYNTAX_TO_FORMAT.get(syntax)); - final RDFWriter writer = Rio.createWriter(format, output); - try { - writer.startRDF(); - try (final Stream statements = graph.stream().map(rdf::asStatement)) { - statements.sequential().forEach(writer::handleStatement); - } - writer.endRDF(); - } catch (final org.eclipse.rdf4j.RDF4JException ex) { - throw new IOException("Error serializing graph", ex); - } - } - - @Override - public Dataset toDataset(final RDFSyntax syntax, final InputStream input, final String baseUri) throws IOException { - final RDFFormat format = Objects.requireNonNull(SYNTAX_TO_FORMAT.get(syntax)); - final Repository repository = new SailRepository(new MemoryStore()); - try { - try (final RepositoryConnection conn = repository.getConnection()) { - conn.add(input, baseUri, format); - } - return rdf.asDataset(repository); - } catch (final org.eclipse.rdf4j.RDF4JException ex) { - throw new IOException("Error parsing dataset", ex); - } - } - - @Override - public Graph toGraph(final RDFSyntax syntax, final InputStream input, final String baseUri) throws IOException { - final RDFFormat format = Objects.requireNonNull(SYNTAX_TO_FORMAT.get(syntax)); - - try { - final Model model = Rio.parse(input, baseUri, format); - return rdf.asGraph(model); - } catch (final org.eclipse.rdf4j.RDF4JException ex) { - throw new IOException("Error parsing graph", ex); - } - } - - static Map buildSyntaxMapping() { - final Map mapping = new HashMap<>(); - mapping.put(RDFSyntax.TURTLE, RDFFormat.TURTLE); - mapping.put(RDFSyntax.TRIG, RDFFormat.TRIG); - mapping.put(RDFSyntax.JSONLD, RDFFormat.JSONLD); - mapping.put(RDFSyntax.NTRIPLES, RDFFormat.NTRIPLES); - mapping.put(RDFSyntax.NQUADS, RDFFormat.NQUADS); - return Collections.unmodifiableMap(mapping); - } -} diff --git a/rdf-legacy/src/main/resources/META-INF/services/com.inrupt.client.spi.RdfService b/rdf-legacy/src/main/resources/META-INF/services/com.inrupt.client.spi.RdfService deleted file mode 100644 index 093a00615d2..00000000000 --- a/rdf-legacy/src/main/resources/META-INF/services/com.inrupt.client.spi.RdfService +++ /dev/null @@ -1 +0,0 @@ -com.inrupt.client.rdf.legacy.RDFLegacyService diff --git a/rdf-legacy/src/site/resources/css/site.css b/rdf-legacy/src/site/resources/css/site.css deleted file mode 100644 index 97fdf4abca6..00000000000 --- a/rdf-legacy/src/site/resources/css/site.css +++ /dev/null @@ -1,22 +0,0 @@ -#banner img { - width: 200px; -} - -table.bodyTable tr.a { - background-color: #ddd; -} - -table.bodyTable tr.b { - background-color: #eee; -} - -div.sidebar-nav { - display: none; -} - -a.externalLink, -a.externalLink[href^=http], -a.externalLink[href^=https] { - background: none; - padding-right: 0px; -} diff --git a/rdf-legacy/src/site/resources/images/inrupt_logo-2020.svg b/rdf-legacy/src/site/resources/images/inrupt_logo-2020.svg deleted file mode 100644 index 05e59ed4a5d..00000000000 --- a/rdf-legacy/src/site/resources/images/inrupt_logo-2020.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/rdf-legacy/src/site/site.xml b/rdf-legacy/src/site/site.xml deleted file mode 100644 index 8a700326611..00000000000 --- a/rdf-legacy/src/site/site.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com - - - - - - - - - - diff --git a/rdf-legacy/src/test/java/com/inrupt/client/rdf/legacy/RDF4JTestModel.java b/rdf-legacy/src/test/java/com/inrupt/client/rdf/legacy/RDF4JTestModel.java deleted file mode 100644 index 12f603dbbb6..00000000000 --- a/rdf-legacy/src/test/java/com/inrupt/client/rdf/legacy/RDF4JTestModel.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Inrupt Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.inrupt.client.rdf.legacy; - -import com.inrupt.client.test.RdfTestModel; - -import org.eclipse.rdf4j.model.IRI; -import org.eclipse.rdf4j.model.Literal; -import org.eclipse.rdf4j.model.Resource; -import org.eclipse.rdf4j.model.ValueFactory; -import org.eclipse.rdf4j.model.impl.SimpleValueFactory; - -final class RDF4JTestModel { - - //RDF4J properties - public static final ValueFactory VF = SimpleValueFactory.getInstance(); - - public static final Resource S_RDF4J = VF.createIRI(RdfTestModel.S_VALUE); - public static final IRI P_RDF4J = VF.createIRI(RdfTestModel.P_VALUE); - public static final Literal O_RDF4J = VF.createLiteral(RdfTestModel.O_VALUE); - public static final Resource G_RDF4J = VF.createIRI(RdfTestModel.G_VALUE); - - public static final Resource S1_RDF4J = VF.createIRI(RdfTestModel.S1_VALUE); - public static final IRI P1_RDF4J = VF.createIRI(RdfTestModel.P1_VALUE); - public static final Literal O1_RDF4J = VF.createLiteral(RdfTestModel.O1_VALUE); - - public static final Resource S2_RDF4J = VF.createIRI(RdfTestModel.S2_VALUE); - public static final IRI P2_RDF4J = VF.createIRI(RdfTestModel.P2_VALUE); - public static final Literal O2_RDF4J = VF.createLiteral(RdfTestModel.O2_VALUE); - - private RDF4JTestModel() { - // Prevent instantiation - } -} diff --git a/rdf-legacy/src/test/java/com/inrupt/client/rdf/legacy/RDFLegacyServiceTest.java b/rdf-legacy/src/test/java/com/inrupt/client/rdf/legacy/RDFLegacyServiceTest.java deleted file mode 100644 index 16cc5a6986f..00000000000 --- a/rdf-legacy/src/test/java/com/inrupt/client/rdf/legacy/RDFLegacyServiceTest.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright Inrupt Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.inrupt.client.rdf.legacy; - -import static org.junit.jupiter.api.Assertions.*; - -import com.inrupt.client.spi.RdfService; -import com.inrupt.client.spi.ServiceProvider; -import com.inrupt.client.test.RdfServices; -import com.inrupt.client.test.RdfTestModel; -import com.inrupt.commons.rdf4j.RDF4J; -import com.inrupt.commons.rdf4j.RDF4JDataset; -import com.inrupt.commons.rdf4j.RDF4JGraph; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.util.Optional; -import java.util.stream.Stream; - -import org.apache.commons.rdf.api.Dataset; -import org.apache.commons.rdf.api.Graph; -import org.apache.commons.rdf.api.IRI; -import org.apache.commons.rdf.api.Quad; -import org.apache.commons.rdf.api.RDFSyntax; -import org.apache.commons.rdf.api.Triple; -import org.eclipse.rdf4j.model.Model; -import org.eclipse.rdf4j.model.Statement; -import org.eclipse.rdf4j.model.ValueFactory; -import org.eclipse.rdf4j.model.impl.SimpleValueFactory; -import org.eclipse.rdf4j.model.util.ModelBuilder; -import org.eclipse.rdf4j.repository.Repository; -import org.eclipse.rdf4j.repository.RepositoryConnection; -import org.eclipse.rdf4j.repository.sail.SailRepository; -import org.eclipse.rdf4j.sail.memory.MemoryStore; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -class RDFLegacyServiceTest extends RdfServices { - - private final RdfService service = ServiceProvider.getRdfService(); - private static RDF4JDataset rdf4jDataset; - private static RDF4JGraph rdf4jGraph; - private static RDF4J rdf = new RDF4J(); - - public static final ValueFactory VF = SimpleValueFactory.getInstance(); - - @BeforeAll - static void setup() { - // create a RDF4JDataset - final Statement st = VF.createStatement( - RDF4JTestModel.S_RDF4J, - RDF4JTestModel.P_RDF4J, - RDF4JTestModel.O_RDF4J, - RDF4JTestModel.G_RDF4J - ); - final Statement st1 = VF.createStatement( - RDF4JTestModel.S1_RDF4J, - RDF4JTestModel.P1_RDF4J, - RDF4JTestModel.O1_RDF4J - ); - final Repository repository = new SailRepository(new MemoryStore()); - try (final RepositoryConnection conn = repository.getConnection()) { - conn.add(st); - conn.add(st1); - } - rdf4jDataset = rdf.asDataset(repository); - - // create a RDF4JGraph - final ModelBuilder builder = new ModelBuilder(); - builder.namedGraph(RDF4JTestModel.G_RDF4J) - .subject(RdfTestModel.S_VALUE) - .add(RdfTestModel.P_VALUE, RdfTestModel.O_VALUE); - builder.defaultGraph().subject(RdfTestModel.S1_VALUE).add(RdfTestModel.P_VALUE, RdfTestModel.O1_VALUE); - final Model m = builder.build(); - rdf4jGraph = rdf.asGraph(m); - } - - @Test - void checkInstance() { - assertTrue(service instanceof RDFLegacyService); - } - - @Test - void parseToDatasetRelativeURIsButNoBaseURI() throws IOException { - try (final InputStream input = RdfServices.class.getResourceAsStream("/relativeURIs.ttl")) { - assertThrows(IOException.class, () -> service.toDataset(RDFSyntax.TURTLE, input, null)); - } - } - - @Test - void serializeFromDatasetTRIGRoundtrip() throws Exception { - try (final ByteArrayOutputStream output = new ByteArrayOutputStream()) { - service.fromDataset(rdf4jDataset, RDFSyntax.TRIG, output); - try (final InputStream input = new ByteArrayInputStream(output.toByteArray()); - final Dataset roundtrip = service.toDataset(RDFSyntax.TRIG, input, null)) { - assertEquals(2, roundtrip.size()); - assertEquals(rdf4jDataset.size(), roundtrip.size()); - String st1 = null; - try (final Stream stream = rdf4jDataset - .stream(Optional.of(RdfTestModel.G_RDFNode), null, null, null).map(Quad.class::cast)) { - st1 = stream.map(Quad::getSubject).filter(IRI.class::isInstance).map(IRI.class::cast) - .map(IRI::getIRIString).findFirst().orElse(null); - } - - String st2 = null; - try (final Stream stream = roundtrip.stream(Optional.of(RdfTestModel.G_RDFNode), - RdfTestModel.S_RDFNode, RdfTestModel.P_RDFNode, RdfTestModel.O_RDFNode).map(Quad.class::cast)) { - st2 = stream.map(Quad::getSubject).filter(IRI.class::isInstance).map(IRI.class::cast) - .map(IRI::getIRIString).findFirst().orElse(null); - } - assertEquals(st1, st2); - } - } - } - - @Test - void serializeFromDatasetTURTLERoundtrip() throws Exception { - try (final ByteArrayOutputStream output = new ByteArrayOutputStream()) { - service.fromDataset(rdf4jDataset, RDFSyntax.TURTLE, output); - try (final InputStream input = new ByteArrayInputStream(output.toByteArray()); - final Dataset roundtrip = service.toDataset(RDFSyntax.TURTLE, input, null)) { - - assertEquals(2, roundtrip.size()); - assertEquals(rdf4jDataset.size(), roundtrip.size()); - - String st1 = null; - try (final Stream stream = rdf4jDataset - .stream(Optional.of(RdfTestModel.G_RDFNode), null, null, null).map(Quad.class::cast)) { - st1 = stream.map(Quad::getSubject).filter(IRI.class::isInstance).map(IRI.class::cast) - .map(IRI::getIRIString).findFirst().orElse(null); - } - String st2 = null; - try (final Stream stream = roundtrip.stream(null, RdfTestModel.S_RDFNode, RdfTestModel.P_RDFNode, - RdfTestModel.O_RDFNode).map(Quad.class::cast)) { - st2 = stream.map(Quad::getSubject).filter(IRI.class::isInstance).map(IRI.class::cast) - .map(IRI::getIRIString).findFirst().orElse(null); - } - assertEquals(st1, st2); - } - } - } - - @Test - void serializeFromGraphRoundtrip() throws Exception { - try (final ByteArrayOutputStream output = new ByteArrayOutputStream()) { - service.fromGraph(rdf4jGraph, RDFSyntax.TURTLE, output); - try (final InputStream input = new ByteArrayInputStream(output.toByteArray()); - final Graph roundtrip = service.toGraph(RDFSyntax.TURTLE, input, null)) { - assertEquals(2, roundtrip.size()); - assertEquals(rdf4jGraph.size(), roundtrip.size()); - - String st1 = null; - try (final Stream stream = rdf4jGraph.stream(RdfTestModel.S_RDFNode, null, null) - .map(Triple.class::cast)) { - st1 = stream.map(Triple::getSubject).filter(IRI.class::isInstance).map(IRI.class::cast) - .map(IRI::getIRIString).findFirst().orElse(null); - } - - String st2 = null; - try (final Stream stream = roundtrip - .stream(RdfTestModel.S_RDFNode, RdfTestModel.P_RDFNode, RdfTestModel.O_RDFNode) - .map(Triple.class::cast)) { - st2 = stream.map(Triple::getSubject).filter(IRI.class::isInstance).map(IRI.class::cast) - .map(IRI::getIRIString).findFirst().orElse(null); - } - assertEquals(st1, st2); - } - } - } - - @Test - void serializeFromDatasetException() throws IOException { - final File tmp = Files.createTempFile(null, null).toFile(); - try (final OutputStream output = new FileOutputStream(tmp)) { - output.close(); - assertThrows(IOException.class, () -> service.fromDataset(rdf4jDataset, RDFSyntax.TRIG, output)); - } - } - - @Test - void serializeFromGraphException() throws IOException { - final File tmp = Files.createTempFile(null, null).toFile(); - try (final OutputStream output = new FileOutputStream(tmp)) { - output.close(); - assertThrows(IOException.class, - () -> service.fromGraph(rdf4jGraph, RDFSyntax.TURTLE, output)); - } - } -} diff --git a/rdf-legacy/src/test/resources/relativeURIs.ttl b/rdf-legacy/src/test/resources/relativeURIs.ttl deleted file mode 100644 index 283e62e4bf5..00000000000 --- a/rdf-legacy/src/test/resources/relativeURIs.ttl +++ /dev/null @@ -1,7 +0,0 @@ -#use of relative URIs -@prefix : <#>. -@prefix foaf: . -@prefix schema: . - -:me - a schema:Person, foaf:Person. \ No newline at end of file diff --git a/rdf4j/pom.xml b/rdf4j/pom.xml index 6dd307f53a6..e8e7ffc5371 100644 --- a/rdf4j/pom.xml +++ b/rdf4j/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-rdf4j @@ -13,12 +13,6 @@ RDF4J integration for the Inrupt Java Client Libraries. - - 1.8 - 1.8 - 4.3.8 - - @@ -44,6 +38,12 @@ pom import + + + commons-io + commons-io + ${commons.io.version} + diff --git a/rdf4j/src/site/site.xml b/rdf4j/src/site/site.xml index 1fc408f274b..37b4c289e80 100644 --- a/rdf4j/src/site/site.xml +++ b/rdf4j/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/reports/pom.xml b/reports/pom.xml index ab0e53cbd1d..f5066b84d69 100644 --- a/reports/pom.xml +++ b/reports/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-report @@ -22,6 +22,11 @@ inrupt-client-accessgrant ${project.version}
+ + com.inrupt.client + inrupt-client-acp + ${project.version} + com.inrupt.client inrupt-client-caffeine @@ -82,11 +87,6 @@ inrupt-client-rdf4j ${project.version} - - com.inrupt.client - inrupt-client-rdf-legacy - ${project.version} - com.inrupt.client inrupt-client-runtime @@ -112,6 +112,11 @@ inrupt-client-webid ${project.version} + + com.inrupt.client + inrupt-client-vocabulary + ${project.version} +
diff --git a/runtime/pom.xml b/runtime/pom.xml index 68be3e37349..35dab867842 100644 --- a/runtime/pom.xml +++ b/runtime/pom.xml @@ -5,7 +5,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT .. @@ -26,6 +26,11 @@ inrupt-client-accessgrant ${project.version} + + com.inrupt.client + inrupt-client-acp + ${project.version} + com.inrupt.client inrupt-client-caffeine diff --git a/solid/pom.xml b/solid/pom.xml index 15163ab2e65..3012d58c4d4 100644 --- a/solid/pom.xml +++ b/solid/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-solid @@ -13,11 +13,6 @@ Solid resource body handlers for the Inrupt Java Client Libraries. - - 1.8 - 1.8 - - com.inrupt.client @@ -29,6 +24,11 @@ inrupt-client-vocabulary ${project.version} + + org.slf4j + slf4j-api + ${slf4j.version} + @@ -80,9 +80,9 @@ test - org.slf4j - slf4j-api - ${slf4j.version} + com.inrupt.client + inrupt-client-jackson + ${project.version} test diff --git a/solid/src/main/java/com/inrupt/client/solid/Metadata.java b/solid/src/main/java/com/inrupt/client/solid/Metadata.java index f3875cf18f6..9da65e820b7 100644 --- a/solid/src/main/java/com/inrupt/client/solid/Metadata.java +++ b/solid/src/main/java/com/inrupt/client/solid/Metadata.java @@ -36,18 +36,17 @@ */ public class Metadata { - private static final URI STORAGE = URI.create(PIM.getNamespace() + "Storage"); private static final String CONTENT_TYPE = "Content-Type"; private final URI acl; private final URI storage; - private final Set types = new HashSet<>(); - private final Map> wacAllow = new HashMap<>(); - private final Set allowedMethods = new HashSet<>(); - private final Set allowedPatchSyntaxes = new HashSet<>(); - private final Set allowedPostSyntaxes = new HashSet<>(); - private final Set allowedPutSyntaxes = new HashSet<>(); private final String contentType; + final Set types = new HashSet<>(); + final Map> wacAllow = new HashMap<>(); + final Set allowedMethods = new HashSet<>(); + final Set allowedPatchSyntaxes = new HashSet<>(); + final Set allowedPostSyntaxes = new HashSet<>(); + final Set allowedPutSyntaxes = new HashSet<>(); /** * The Solid Storage location. @@ -309,7 +308,7 @@ public static Metadata of(final URI identifier, final Headers headers) { .flatMap(l -> Headers.Link.parse(l).stream()) .forEach(link -> { if (link.getParameter("rel").contains("type")) { - if ((link.getUri().equals(STORAGE))) { + if ((link.getUri().equals(PIM.Storage))) { metadata.storage(identifier); } metadata.type(link.getUri()); diff --git a/solid/src/main/java/com/inrupt/client/solid/SolidClient.java b/solid/src/main/java/com/inrupt/client/solid/SolidClient.java index 2b20c3a5105..736f1f39a11 100644 --- a/solid/src/main/java/com/inrupt/client/solid/SolidClient.java +++ b/solid/src/main/java/com/inrupt/client/solid/SolidClient.java @@ -20,22 +20,14 @@ */ package com.inrupt.client.solid; -import static java.nio.charset.StandardCharsets.UTF_8; - -import com.inrupt.client.Client; -import com.inrupt.client.ClientProvider; -import com.inrupt.client.Headers; -import com.inrupt.client.RDFSource; -import com.inrupt.client.Request; -import com.inrupt.client.Resource; -import com.inrupt.client.Response; -import com.inrupt.client.ValidationResult; +import com.inrupt.client.*; import com.inrupt.client.auth.Session; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Map; @@ -59,7 +51,6 @@ public class SolidClient { private static final String IF_NONE_MATCH = "If-None-Match"; private static final String TEXT_TURTLE = "text/turtle"; private static final String WILDCARD = "*"; - private static final int ERROR_STATUS = 400; private final Client client; private final Headers defaultHeaders; @@ -131,36 +122,43 @@ public CompletionStage read(final URI identifier, final headers.firstValue(USER_AGENT).ifPresent(agent -> builder.setHeader(USER_AGENT, agent)); final Request request = builder.build(); - return client.send(request, Response.BodyHandlers.ofByteArray()) - .thenApply(response -> { - if (response.statusCode() >= ERROR_STATUS) { - throw SolidClientException.handle("Unable to read resource at " + request.uri(), request.uri(), - response.statusCode(), response.headers(), new String(response.body())); - } else { - final String contentType = response.headers().firstValue(CONTENT_TYPE) - .orElse("application/octet-stream"); - try { - // Check that this is an RDFSoure - if (RDFSource.class.isAssignableFrom(clazz)) { - final Dataset dataset = SolidResourceHandlers.buildDataset(contentType, response.body(), - request.uri().toString()).orElse(null); - final T obj = construct(request.uri(), clazz, dataset, response.headers()); - final ValidationResult res = RDFSource.class.cast(obj).validate(); - if (!res.isValid()) { - throw new DataMappingException( - "Unable to map resource into type: [" + clazz.getSimpleName() + "] ", - res.getResults()); - } - return obj; - // Otherwise, create a non-RDF-bearing resource - } else { - return construct(request.uri(), clazz, contentType, - new ByteArrayInputStream(response.body()), response.headers()); + return client.send( + request, + Response.BodyHandlers.ofByteArray() + ).thenApply(response -> { + if (!isSuccess(response.statusCode())) { + throw SolidClientException.handle( + "Reading resource failed.", + response.uri(), + response.statusCode(), + response.headers(), + new String(response.body(), StandardCharsets.UTF_8) + ); + } + + final String contentType = response.headers().firstValue(CONTENT_TYPE) + .orElse("application/octet-stream"); + try { + // Check that this is an RDFSoure + if (RDFSource.class.isAssignableFrom(clazz)) { + final Dataset dataset = SolidResourceHandlers.buildDataset(contentType, response.body(), + request.uri().toString()).orElse(null); + final T obj = construct(request.uri(), clazz, dataset, response.headers()); + final ValidationResult res = RDFSource.class.cast(obj).validate(); + if (!res.isValid()) { + throw new DataMappingException( + "Unable to map resource into type: [" + clazz.getSimpleName() + "] ", + res.getResults()); } - } catch (final ReflectiveOperationException ex) { - throw new SolidResourceException("Unable to read resource into type " + clazz.getName(), - ex); + return obj; + // Otherwise, create a non-RDF-bearing resource + } else { + return construct(request.uri(), clazz, contentType, + new ByteArrayInputStream(response.body()), response.headers()); } + } catch (final ReflectiveOperationException ex) { + throw new SolidResourceException("Unable to read resource into type " + clazz.getName(), + ex); } }); } @@ -280,13 +278,21 @@ public CompletionStage delete(final T resource, final defaultHeaders.firstValue(USER_AGENT).ifPresent(agent -> builder.setHeader(USER_AGENT, agent)); headers.firstValue(USER_AGENT).ifPresent(agent -> builder.setHeader(USER_AGENT, agent)); - return client.send(builder.build(), Response.BodyHandlers.ofByteArray()).thenApply(res -> { - if (isSuccess(res.statusCode())) { - return null; - } else { - throw SolidClientException.handle("Unable to delete resource", resource.getIdentifier(), - res.statusCode(), res.headers(), new String(res.body(), UTF_8)); + + return client.send( + builder.build(), + Response.BodyHandlers.ofByteArray() + ).thenApply(response -> { + if (!isSuccess(response.statusCode())) { + throw SolidClientException.handle( + "Deleting resource failed.", + response.uri(), + response.statusCode(), + response.headers(), + new String(response.body(), StandardCharsets.UTF_8) + ); } + return null; }); } @@ -371,8 +377,13 @@ Function, CompletionStage> handleRespon final Headers headers, final String message) { return res -> { if (!isSuccess(res.statusCode())) { - throw SolidClientException.handle(message, resource.getIdentifier(), - res.statusCode(), res.headers(), new String(res.body(), UTF_8)); + throw SolidClientException.handle( + message, + resource.getIdentifier(), + res.statusCode(), + res.headers(), + new String(res.body(), StandardCharsets.UTF_8) + ); } if (!fetchAfterWrite) { @@ -382,7 +393,6 @@ Function, CompletionStage> handleRespon @SuppressWarnings("unchecked") final Class clazz = (Class) resource.getClass(); return read(resource.getIdentifier(), headers, clazz); - }; } @@ -396,16 +406,6 @@ static T construct(final URI identifier, final Class cla // no-op } - // Next, try an arity-3 ctor with metadata - // TODO: this construct is deprecated and can be removed in a future version - try { - final Metadata metadata = Metadata.of(identifier, headers); - return clazz.getConstructor(URI.class, Dataset.class, Metadata.class) - .newInstance(identifier, dataset, metadata); - } catch (final NoSuchMethodException ex) { - // no-op - } - // Fall back to an arity-2 ctor return clazz.getConstructor(URI.class, Dataset.class) .newInstance(identifier, dataset); @@ -422,16 +422,6 @@ static T construct(final URI identifier, final Class cla // no-op } - // Next try an arity-4 ctor with metadata - // TODO: this construct is deprecated and can be removed in a future version - try { - final Metadata metadata = Metadata.of(identifier, headers); - return clazz.getConstructor(URI.class, String.class, InputStream.class, Metadata.class) - .newInstance(identifier, contentType, entity, metadata); - } catch (final NoSuchMethodException ex) { - // no-op - } - // Fall back to an arity-3 ctor return clazz.getConstructor(URI.class, String.class, InputStream.class) .newInstance(identifier, contentType, entity); diff --git a/solid/src/main/java/com/inrupt/client/solid/SolidClientException.java b/solid/src/main/java/com/inrupt/client/solid/SolidClientException.java index 668df265d3d..731280df350 100644 --- a/solid/src/main/java/com/inrupt/client/solid/SolidClientException.java +++ b/solid/src/main/java/com/inrupt/client/solid/SolidClientException.java @@ -22,6 +22,7 @@ import com.inrupt.client.Headers; import com.inrupt.client.InruptClientException; +import com.inrupt.client.ProblemDetails; import java.net.URI; @@ -35,7 +36,7 @@ public class SolidClientException extends InruptClientException { private final URI uri; private final int statusCode; private final String body; - private final transient Headers headers; + private final Headers headers; /** * Create a SolidClient exception. @@ -91,6 +92,23 @@ public String getBody() { return body; } + /** + * Retrieve the {@link ProblemDetails} instance describing the HTTP error response. + * @return the problem details object + */ + public ProblemDetails getProblemDetails() { + return SolidProblemDetails.fromErrorResponse(uri, statusCode, headers, body.getBytes()); + } + + /** + * + * @param message the resulting exception message + * @param uri the request URL + * @param statusCode the response status code + * @param headers the response {@link Headers} + * @param body the response body + * @return an appropriate exception based on the status code. + */ public static SolidClientException handle( final String message, final URI uri, diff --git a/solid/src/main/java/com/inrupt/client/solid/SolidContainer.java b/solid/src/main/java/com/inrupt/client/solid/SolidContainer.java index ac2ff46ce8e..58343e6c18d 100644 --- a/solid/src/main/java/com/inrupt/client/solid/SolidContainer.java +++ b/solid/src/main/java/com/inrupt/client/solid/SolidContainer.java @@ -66,19 +66,6 @@ public SolidContainer(final URI identifier, final Dataset dataset) { this(identifier, dataset, (Headers) null); } - /** - * Create a new SolidContainer. - * - * @param identifier the container's unique identifier - * @param dataset the dataset for this container, may be {@code null} - * @param metadata the container's metadata, may be {@code null} - * @deprecated use {@link #SolidContainer(URI, Dataset, Headers)} instead - */ - @Deprecated - public SolidContainer(final URI identifier, final Dataset dataset, final Metadata metadata) { - super(identifier, dataset, metadata); - } - /** * Create a new SolidContainer. * @@ -195,11 +182,8 @@ static boolean verifyContainmentIri(final String container, final IRI object) { relativePath.substring(0, relativePath.length() - 1) : relativePath; // Containment cannot skip intermediate nodes - if (normalizedPath.contains("/")) { - return false; - } - - return true; + // Return true if the normalized path does not contain '/' + return !normalizedPath.contains("/"); } @SuppressWarnings("java:S2160") // Wrapper equality is correctly delegated to underlying node diff --git a/solid/src/main/java/com/inrupt/client/solid/SolidNonRDFSource.java b/solid/src/main/java/com/inrupt/client/solid/SolidNonRDFSource.java index 66b805bbf5c..58e4759786c 100644 --- a/solid/src/main/java/com/inrupt/client/solid/SolidNonRDFSource.java +++ b/solid/src/main/java/com/inrupt/client/solid/SolidNonRDFSource.java @@ -44,26 +44,6 @@ public SolidNonRDFSource(final URI identifier, final String contentType, final I this(identifier, contentType, entity, (Headers) null); } - /** - * Create a non-RDF-bearing Solid Resource. - * - * @param identifier the resource identifier - * @param contentType the content type - * @param entity the entity - * @param metadata the metadata, may be {@code null} - * @deprecated use {@link #SolidNonRDFSource(URI, String, InputStream, Headers)} instead - */ - @Deprecated - public SolidNonRDFSource(final URI identifier, final String contentType, final InputStream entity, - final Metadata metadata) { - super(identifier, contentType, entity); - if (metadata == null) { - this.metadata = Metadata.newBuilder().build(); - } else { - this.metadata = metadata; - } - } - /** * Create a non-RDF-bearing Solid Resource. * diff --git a/solid/src/main/java/com/inrupt/client/solid/SolidProblemDetails.java b/solid/src/main/java/com/inrupt/client/solid/SolidProblemDetails.java new file mode 100644 index 00000000000..9a393d403bd --- /dev/null +++ b/solid/src/main/java/com/inrupt/client/solid/SolidProblemDetails.java @@ -0,0 +1,167 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.solid; + +import com.inrupt.client.Headers; +import com.inrupt.client.ProblemDetails; +import com.inrupt.client.spi.JsonService; +import com.inrupt.client.spi.ServiceProvider; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SolidProblemDetails implements ProblemDetails { + + private static final Logger LOGGER = LoggerFactory.getLogger(SolidProblemDetails.class); + private static final long serialVersionUID = -4597170432270957765L; + + private final URI type; + private final String title; + private final String detail; + private final int status; + private final URI instance; + private static JsonService jsonService; + private static boolean isJsonServiceInitialized; + + /** + * Build a ProblemDetails instance providing the expected fields as described in + * RFC9457. + * @param type the problem type + * @param title the problem title + * @param detail the problem details + * @param status the error response status code + * @param instance the problem instance + */ + public SolidProblemDetails( + final URI type, + final String title, + final String detail, + final int status, + final URI instance + ) { + this.type = type; + this.title = title; + this.detail = detail; + this.status = status; + this.instance = instance; + } + + @Override + public URI getType() { + return this.type; + } + + @Override + public String getTitle() { + return this.title; + } + + @Override + public String getDetail() { + return this.detail; + } + + @Override + public int getStatus() { + return this.status; + } + + @Override + public URI getInstance() { + return this.instance; + } + + /** + * This inner class is only ever used for JSON deserialization. Please do not use in any other context. + */ + static class Data { + /** + * The problem type. + */ + public URI type; + /** + * The problem title. + */ + public String title; + /** + * The problem detail. + */ + public String detail; + /** + * The problem status code. + */ + public int status; + /** + * The problem instance. + */ + public URI instance; + } + + private static JsonService getJsonService() { + if (SolidProblemDetails.isJsonServiceInitialized) { + return SolidProblemDetails.jsonService; + } + // It is a legitimate use case that this is loaded in a context where no implementation of the JSON service is + // available. On SPI lookup failure, the ProblemDetails exceptions will fall back to default and not be parsed. + try { + SolidProblemDetails.jsonService = ServiceProvider.getJsonService(); + } catch (IllegalStateException e) { + SolidProblemDetails.jsonService = null; + } + SolidProblemDetails.isJsonServiceInitialized = true; + return SolidProblemDetails.jsonService; + } + + /** + * Builds a {@link ProblemDetails} instance from an HTTP error response. + * @param uri the original URI + * @param statusCode the HTTP error response status code + * @param headers the HTTP error response headers + * @param body the HTTP error response body + * @return a {@link ProblemDetails} instance + */ + static SolidProblemDetails fromErrorResponse(final URI uri, final int statusCode, final Headers headers, + final byte[] body) { + final JsonService jsonService = getJsonService(); + if (jsonService != null + && (headers != null && headers.allValues("Content-Type").contains(ProblemDetails.MIME_TYPE))) { + try (final InputStream input = new ByteArrayInputStream(body)) { + final Data pdData = jsonService.fromJson(input, Data.class); + final URI type = Optional.ofNullable(pdData.type) + .map(uri::resolve) + .orElse(ProblemDetails.DEFAULT_TYPE); + final URI instance = pdData.instance != null ? uri.resolve(pdData.instance) : null; + // JSON mappers map invalid integers to 0, which is an invalid value in our case anyway. + final int status = Optional.of(pdData.status).filter(s -> s != 0).orElse(statusCode); + return new SolidProblemDetails(type, pdData.title, pdData.detail, status, instance); + } catch (IOException e) { + LOGGER.debug("Unable to parse ProblemDetails response from server", e); + } + } + return new SolidProblemDetails(ProblemDetails.DEFAULT_TYPE, null, null, statusCode, null); + } +} diff --git a/solid/src/main/java/com/inrupt/client/solid/SolidRDFSource.java b/solid/src/main/java/com/inrupt/client/solid/SolidRDFSource.java index 2d2503ea7fa..b10b9cd3795 100644 --- a/solid/src/main/java/com/inrupt/client/solid/SolidRDFSource.java +++ b/solid/src/main/java/com/inrupt/client/solid/SolidRDFSource.java @@ -53,24 +53,6 @@ public SolidRDFSource(final URI identifier, final Dataset dataset) { this(identifier, dataset, (Headers) null); } - /** - * Create a Solid resource. - * - * @param identifier the Solid Resource identifier - * @param dataset the resource dataset, may be {@code null} - * @param metadata metadata associated with this resource, may be {@code null} - * @deprecated use {@link #SolidRDFSource(URI, Dataset, Headers)} instead - */ - @Deprecated - public SolidRDFSource(final URI identifier, final Dataset dataset, final Metadata metadata) { - super(identifier, dataset); - if (metadata == null) { - this.metadata = Metadata.newBuilder().build(); - } else { - this.metadata = metadata; - } - } - /** * Create a Solid resource. * diff --git a/solid/src/main/java/com/inrupt/client/solid/SolidResourceHandlers.java b/solid/src/main/java/com/inrupt/client/solid/SolidResourceHandlers.java index 5b7ea7acf14..3efa0fdd010 100644 --- a/solid/src/main/java/com/inrupt/client/solid/SolidResourceHandlers.java +++ b/solid/src/main/java/com/inrupt/client/solid/SolidResourceHandlers.java @@ -20,6 +20,7 @@ */ package com.inrupt.client.solid; +import com.inrupt.client.Headers; import com.inrupt.client.Response; import com.inrupt.client.spi.RdfService; import com.inrupt.client.spi.ServiceProvider; @@ -47,13 +48,13 @@ public final class SolidResourceHandlers { */ public static Response.BodyHandler ofSolidRDFSource() { return responseInfo -> { - final Metadata metadata = Metadata.of(responseInfo.uri(), responseInfo.headers()); + final Headers headers = responseInfo.headers(); - return responseInfo.headers().firstValue(CONTENT_TYPE) + return headers.firstValue(CONTENT_TYPE) .flatMap(contentType -> buildDataset(contentType, responseInfo.body().array(), responseInfo.uri().toString())) - .map(dataset -> new SolidRDFSource(responseInfo.uri(), dataset, metadata)) - .orElseGet(() -> new SolidRDFSource(responseInfo.uri(), null, metadata)); + .map(dataset -> new SolidRDFSource(responseInfo.uri(), dataset, headers)) + .orElseGet(() -> new SolidRDFSource(responseInfo.uri(), null, headers)); }; } @@ -64,13 +65,12 @@ public static Response.BodyHandler ofSolidRDFSource() { */ public static Response.BodyHandler ofSolidContainer() { return responseInfo -> { - final Metadata metadata = Metadata.of(responseInfo.uri(), responseInfo.headers()); - - return responseInfo.headers().firstValue(CONTENT_TYPE) + final Headers headers = responseInfo.headers(); + return headers.firstValue(CONTENT_TYPE) .flatMap(contentType -> buildDataset(contentType, responseInfo.body().array(), responseInfo.uri().toString())) - .map(dataset -> new SolidContainer(responseInfo.uri(), dataset, metadata)) - .orElseGet(() -> new SolidContainer(responseInfo.uri(), null, metadata)); + .map(dataset -> new SolidContainer(responseInfo.uri(), dataset, headers)) + .orElseGet(() -> new SolidContainer(responseInfo.uri(), null, headers)); }; } diff --git a/solid/src/site/site.xml b/solid/src/site/site.xml index 9004a2cf3ff..60ed0bbc156 100644 --- a/solid/src/site/site.xml +++ b/solid/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/solid/src/test/java/com/inrupt/client/solid/DeprecatedType.java b/solid/src/test/java/com/inrupt/client/solid/DeprecatedType.java deleted file mode 100644 index 2390f5ddb8b..00000000000 --- a/solid/src/test/java/com/inrupt/client/solid/DeprecatedType.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Inrupt Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, - * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A - * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.inrupt.client.solid; - -import com.inrupt.rdf.wrapping.commons.TermMappings; -import com.inrupt.rdf.wrapping.commons.ValueMappings; -import com.inrupt.rdf.wrapping.commons.WrapperIRI; - -import java.net.URI; -import java.util.Set; - -import org.apache.commons.rdf.api.Dataset; -import org.apache.commons.rdf.api.Graph; -import org.apache.commons.rdf.api.IRI; -import org.apache.commons.rdf.api.RDFTerm; - -public class DeprecatedType extends SolidRDFSource { - - private final IRI dcTitle; - private final IRI exIngredient; - private final IRI exStep; - private final Node subject; - - public DeprecatedType(final URI identifier, final Dataset dataset, final Metadata metadata) { - super(identifier, dataset, metadata); - - this.subject = new Node(rdf.createIRI(identifier.toString()), getGraph()); - this.dcTitle = rdf.createIRI("http://purl.org/dc/terms/title"); - this.exStep = rdf.createIRI("https://example.com/step"); - this.exIngredient = rdf.createIRI("https://example.com/ingredient"); - } - - public String getTitle() { - return subject.getTitle(); - } - - public void setTitle(final String value) { - subject.setTitle(value); - } - - public Set getIngredients() { - return subject.getIngredients(); - } - - public Set getSteps() { - return subject.getSteps(); - } - - class Node extends WrapperIRI { - - Node(final RDFTerm original, final Graph graph) { - super(original, graph); - } - - String getTitle() { - return anyOrNull(dcTitle, ValueMappings::literalAsString); - } - - void setTitle(final String value) { - overwriteNullable(dcTitle, value, TermMappings::asStringLiteral); - } - - Set getIngredients() { - return objects(exIngredient, TermMappings::asStringLiteral, ValueMappings::literalAsString); - } - - Set getSteps() { - return objects(exStep, TermMappings::asStringLiteral, ValueMappings::literalAsString); - } - } -} - diff --git a/solid/src/test/java/com/inrupt/client/solid/SolidClientTest.java b/solid/src/test/java/com/inrupt/client/solid/SolidClientTest.java index fab400019f5..52e096b4537 100644 --- a/solid/src/test/java/com/inrupt/client/solid/SolidClientTest.java +++ b/solid/src/test/java/com/inrupt/client/solid/SolidClientTest.java @@ -22,17 +22,18 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; -import com.inrupt.client.ClientProvider; -import com.inrupt.client.Headers; -import com.inrupt.client.Request; -import com.inrupt.client.Response; +import com.inrupt.client.*; import com.inrupt.client.auth.Session; +import com.inrupt.client.jackson.JacksonService; +import com.inrupt.client.spi.JsonService; import com.inrupt.client.spi.RDFFactory; import com.inrupt.client.util.URIBuilder; import com.inrupt.client.vocabulary.PIM; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -57,6 +58,7 @@ class SolidClientTest { private static final Map config = new HashMap<>(); private static final RDF rdf = RDFFactory.getInstance(); private static final SolidClient client = SolidClient.getClient().session(Session.anonymous()); + private static final JsonService jsonService = new JacksonService(); @BeforeAll static void setup() { @@ -127,6 +129,24 @@ void testCustomHeaders() throws Exception { }).toCompletableFuture().join(); } + @Test + void testJsonResource() { + final URI uri = URI.create(config.get("solid_resource_uri") + "/transaction"); + client.read(uri, TransactionResource.class).thenAccept(res -> { + try (final TransactionResource tr = res) { + final var transaction = tr.getTransaction(); + assertEquals("sample description", transaction.description()); + assertEquals(TransactionResource.TransactionType.CREDIT, transaction.type()); + final var err = assertThrows(CompletionException.class, client.update(tr).toCompletableFuture()::join); + assertTrue(err.getCause() instanceof ForbiddenException); + tr.setTransaction(TransactionResource.Transaction.newBuilder(transaction) + .type(TransactionResource.TransactionType.DEBIT) + .description("different description") + .build()); + assertDoesNotThrow(client.update(tr).toCompletableFuture()::join); + } + }).toCompletableFuture().join(); + } @Test void testGetPlaylist() throws IOException, InterruptedException { @@ -199,20 +219,6 @@ void testGetContainer() throws IOException, InterruptedException { } - @Test - void testGetDeprecatedBinaryFetch() { - final URI uri = URI.create(config.get("solid_resource_uri") + "/binary"); - - client.read(uri, DeprecatedBinary.class).thenAccept(binary -> { - try (final DeprecatedBinary b = binary) { - assertEquals(uri, b.getIdentifier()); - assertEquals(TEXT_PLAIN, b.getContentType()); - - assertTrue(b.getHeaders().asMap().isEmpty()); - } - }).toCompletableFuture().join(); - } - @Test void testGetBinaryFetch() { final URI uri = URI.create(config.get("solid_resource_uri") + "/binary"); @@ -327,22 +333,6 @@ void testGetRecipeType() { .toCompletableFuture().join(); } - @Test - void testGetDeprecatedType() { - final URI uri = URI.create(config.get("solid_resource_uri") + "/recipe"); - - client.read(uri, DeprecatedType.class).thenAccept(recipe -> { - try (final DeprecatedType r = recipe) { - assertEquals(uri, r.getIdentifier()); - assertEquals("Molasses Cookies", r.getTitle()); - assertEquals(11, r.getIngredients().size()); - assertEquals(7, r.getSteps().size()); - assertFalse(r.getHeaders().firstValue("allow").isPresent()); - } - }) - .toCompletableFuture().join(); - } - @ParameterizedTest @MethodSource void testExceptionalResources( @@ -366,20 +356,15 @@ void testExceptionalResources( private static Stream testExceptionalResources() { return Stream.of( - Arguments.of( - URI.create(config.get("solid_resource_uri") + "/unauthorized"), 401, - UnauthorizedException.class), - Arguments.of( - URI.create(config.get("solid_resource_uri") + "/forbidden"), 403, - ForbiddenException.class), - Arguments.of( - URI.create(config.get("solid_resource_uri") + "/missing"), 404, - NotFoundException.class)); + arguments(URI.create(config.get("solid_resource_uri") + "/unauthorized"), 401, UnauthorizedException.class), + arguments(URI.create(config.get("solid_resource_uri") + "/forbidden"), 403, ForbiddenException.class), + arguments(URI.create(config.get("solid_resource_uri") + "/missing"), 404, NotFoundException.class) + ); } @ParameterizedTest @MethodSource - void testSpecialisedExceptions( + void testLegacyExceptions( final Class clazz, final int statusCode ) { @@ -387,20 +372,215 @@ void testSpecialisedExceptions( final SolidClient solidClient = new SolidClient(ClientProvider.getClient(), headers, false); final SolidContainer resource = new SolidContainer(URI.create("http://example.com")); + final SolidClientException exception = assertThrows( + clazz, + () -> solidClient.handleResponse(resource, headers, "message") + .apply(new Response() { + @Override + public byte[] body() { + return new byte[0]; + } + + @Override + public Headers headers() { + return null; + } + + @Override + public URI uri() { + return null; + } + + @Override + public int statusCode() { + return statusCode; + } + }) + ); + assertEquals(statusCode, exception.getStatusCode()); + // The following assertions check that in absence of an RFC9457 compliant response, we properly initialize the + // default values for the attached Problem Details. + assertEquals(ProblemDetails.DEFAULT_TYPE, exception.getProblemDetails().getType()); + assertEquals(statusCode, exception.getProblemDetails().getStatus()); + assertNull(exception.getProblemDetails().getTitle()); + assertNull(exception.getProblemDetails().getDetail()); + assertNull(exception.getProblemDetails().getInstance()); + } + + private static Stream testLegacyExceptions() { + return Stream.of( + arguments(BadRequestException.class, 400), + arguments(UnauthorizedException.class, 401), + arguments(ForbiddenException.class, 403), + arguments(NotFoundException.class, 404), + arguments(MethodNotAllowedException.class, 405), + arguments(NotAcceptableException.class, 406), + arguments(ConflictException.class, 409), + arguments(GoneException.class, 410), + arguments(PreconditionFailedException.class, 412), + arguments(UnsupportedMediaTypeException.class, 415), + arguments(TooManyRequestsException.class, 429), + arguments(InternalServerErrorException.class, 500), + arguments(SolidClientException.class, 418), + arguments(SolidClientException.class,599), + arguments(SolidClientException.class,600) + ); + } + + @ParameterizedTest + @MethodSource + void testRfc9457Exceptions( + final Class clazz, + final ProblemDetails problemDetails + ) { + final Headers headers = Headers.of(Collections.singletonMap("x-key", Arrays.asList("value"))); + final SolidClient solidClient = new SolidClient(ClientProvider.getClient(), headers, false); + final SolidContainer resource = new SolidContainer(URI.create("http://example.com")); + final SolidClientException exception = assertThrows( clazz, () -> solidClient.handleResponse(resource, headers, "message") .apply(new Response() { @Override public byte[] body() { - return new byte[0]; + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + jsonService.toJson(problemDetails, bos); + return bos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } } @Override public Headers headers() { + final List headerValues = new ArrayList<>(); + headerValues.add("application/problem+json"); + final Map> headerMap = new HashMap<>(); + headerMap.put("Content-Type", headerValues); + return Headers.of(headerMap); + } + + @Override + public URI uri() { return null; } + @Override + public int statusCode() { + return problemDetails.getStatus(); + } + }) + ); + assertEquals(problemDetails.getStatus(), exception.getStatusCode()); + assertEquals(problemDetails.getType(), exception.getProblemDetails().getType()); + assertEquals(problemDetails.getTitle(), exception.getProblemDetails().getTitle()); + assertEquals(problemDetails.getStatus(), exception.getProblemDetails().getStatus()); + assertEquals(problemDetails.getDetail(), exception.getProblemDetails().getDetail()); + assertEquals(problemDetails.getInstance(), exception.getProblemDetails().getInstance()); + } + + private static ProblemDetails mockProblemDetails(final String title, final String details, final int status) { + return new SolidProblemDetails(URI.create("https://example.org/type"), + title, + details, + status, + URI.create("https://example.org/instance") + ); + } + + private static Stream testRfc9457Exceptions() { + return Stream.of( + arguments( + BadRequestException.class, + mockProblemDetails("Bad Request", "Some details", 400) + ), + arguments( + UnauthorizedException.class, + mockProblemDetails("Unauthorized", "Some details", 401) + ), + arguments( + ForbiddenException.class, + mockProblemDetails("Forbidden", "Some details", 403) + ), + arguments( + NotFoundException.class, + mockProblemDetails("Not Found", "Some details", 404) + ), + arguments( + MethodNotAllowedException.class, + mockProblemDetails("Method Not Allowed", "Some details", 405) + ), + arguments( + NotAcceptableException.class, + mockProblemDetails("Not Acceptable", "Some details", 406) + ), + arguments( + ConflictException.class, + mockProblemDetails("Conflict", "Some details", 409) + ), + arguments( + GoneException.class, + mockProblemDetails("Gone", "Some details", 410) + ), + arguments( + PreconditionFailedException.class, + mockProblemDetails("Precondition Failed", "Some details", 412) + ), + arguments( + UnsupportedMediaTypeException.class, + mockProblemDetails("Unsupported Media Type", "Some details", 415) + ), + arguments( + TooManyRequestsException.class, + mockProblemDetails("Too Many Requests", "Some details", 429) + ), + arguments( + InternalServerErrorException.class, + mockProblemDetails("Internal Server Error", "Some details", 500) + ), + arguments( + // Custom errors that do not map to a predefined Exception class + // default to the generic SolidClientException + SolidClientException.class, + mockProblemDetails("I'm a Teapot", "Some details", 418) + ), + arguments( + // Custom errors that do not map to a predefined Exception class + // default to the generic SolidClientException. + SolidClientException.class, + mockProblemDetails("Custom server error", "Some details", 599) + ) + ); + } + + @Test + void testMalformedProblemDetails() { + // The specific error code is irrelevant to this test. + final int statusCode = 400; + final Headers headers = Headers.of(Collections.singletonMap("x-key", Arrays.asList("value"))); + final SolidClient solidClient = new SolidClient(ClientProvider.getClient(), headers, false); + final SolidContainer resource = new SolidContainer(URI.create("http://example.com")); + + final SolidClientException exception = assertThrows( + BadRequestException.class, + () -> solidClient.handleResponse(resource, headers, "message") + .apply(new Response() { + // Pretend we return RFC9457 content... + @Override + public Headers headers() { + final List headerValues = new ArrayList<>(); + headerValues.add("application/problem+json"); + final Map> headerMap = new HashMap<>(); + headerMap.put("Content-Type", headerValues); + return Headers.of(headerMap); + } + + // ... but actually return malformed JSON. + @Override + public byte[] body() { + return "This isn't valid application/problem+json.".getBytes(); + } + @Override public URI uri() { return null; @@ -413,23 +593,58 @@ public int statusCode() { }) ); assertEquals(statusCode, exception.getStatusCode()); + // On malformed response, the ProblemDetails should fall back to defaults. + assertEquals(ProblemDetails.DEFAULT_TYPE, exception.getProblemDetails().getType()); + assertNull(exception.getProblemDetails().getTitle()); + assertEquals(statusCode, exception.getProblemDetails().getStatus()); + assertNull(exception.getProblemDetails().getDetail()); + assertNull(exception.getProblemDetails().getInstance()); } - private static Stream testSpecialisedExceptions() { - return Stream.of( - Arguments.of(BadRequestException.class, 400), - Arguments.of(UnauthorizedException.class, 401), - Arguments.of(ForbiddenException.class, 403), - Arguments.of(NotFoundException.class, 404), - Arguments.of(MethodNotAllowedException.class, 405), - Arguments.of(NotAcceptableException.class, 406), - Arguments.of(ConflictException.class, 409), - Arguments.of(GoneException.class, 410), - Arguments.of(PreconditionFailedException.class, 412), - Arguments.of(UnsupportedMediaTypeException.class, 415), - Arguments.of(TooManyRequestsException.class, 429), - Arguments.of(InternalServerErrorException.class, 500), - Arguments.of(SolidClientException.class, 418) + @Test + void testMinimalProblemDetails() { + // The specific error code is irrelevant to this test. + final int statusCode = 400; + final Headers headers = Headers.of(Collections.singletonMap("x-key", Arrays.asList("value"))); + final SolidClient solidClient = new SolidClient(ClientProvider.getClient(), headers, false); + final SolidContainer resource = new SolidContainer(URI.create("http://example.com")); + + final SolidClientException exception = assertThrows( + BadRequestException.class, + () -> solidClient.handleResponse(resource, headers, "message") + .apply(new Response() { + @Override + public Headers headers() { + final List headerValues = new ArrayList<>(); + headerValues.add("application/problem+json"); + final Map> headerMap = new HashMap<>(); + headerMap.put("Content-Type", headerValues); + return Headers.of(headerMap); + } + + // Return minimal problem details.. + @Override + public byte[] body() { + return "{\"status\":400}".getBytes(); + } + + @Override + public URI uri() { + return null; + } + + @Override + public int statusCode() { + return statusCode; + } + }) ); + assertEquals(statusCode, exception.getStatusCode()); + // On malformed response, the ProblemDetails should fall back to defaults. + assertEquals(ProblemDetails.DEFAULT_TYPE, exception.getProblemDetails().getType()); + assertNull(exception.getProblemDetails().getTitle()); + assertEquals(statusCode, exception.getProblemDetails().getStatus()); + assertNull(exception.getProblemDetails().getDetail()); + assertNull(exception.getProblemDetails().getInstance()); } } diff --git a/solid/src/test/java/com/inrupt/client/solid/SolidExceptionTest.java b/solid/src/test/java/com/inrupt/client/solid/SolidExceptionTest.java index 15220345d68..147b48fb094 100644 --- a/solid/src/test/java/com/inrupt/client/solid/SolidExceptionTest.java +++ b/solid/src/test/java/com/inrupt/client/solid/SolidExceptionTest.java @@ -22,6 +22,8 @@ import static org.junit.jupiter.api.Assertions.*; +import java.net.URI; + import org.junit.jupiter.api.Test; class SolidExceptionTest { @@ -41,4 +43,14 @@ void checkSolidWrappedException() { assertEquals(upstream, err.getCause()); assertEquals(msg, err.getMessage()); } + + @Test + void checkSolidClientException() { + final String msg = "Error"; + final SolidClientException err = new SolidClientException( + msg, URI.create("https://example.org/request"), 123, null, "some body" + ); + assertEquals(msg, err.getMessage()); + assertEquals(123, err.getProblemDetails().getStatus()); + } } diff --git a/solid/src/test/java/com/inrupt/client/solid/SolidMockHttpService.java b/solid/src/test/java/com/inrupt/client/solid/SolidMockHttpService.java index 6871eff5053..7c22febfd2f 100644 --- a/solid/src/test/java/com/inrupt/client/solid/SolidMockHttpService.java +++ b/solid/src/test/java/com/inrupt/client/solid/SolidMockHttpService.java @@ -216,6 +216,26 @@ private void setupMocks() { .willReturn(aResponse() .withStatus(204))); + wireMockServer.stubFor(get(urlEqualTo("/transaction")) + .withHeader("User-Agent", equalTo(USER_AGENT)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBodyFile("transaction.json"))); + + wireMockServer.stubFor(put(urlEqualTo("/transaction")) + .withHeader("User-Agent", equalTo(USER_AGENT)) + .withRequestBody(containing("DEBIT")) + .withRequestBody(containing("different description")) + .willReturn(aResponse() + .withStatus(204))); + + wireMockServer.stubFor(put(urlEqualTo("/transaction")) + .withHeader("User-Agent", equalTo(USER_AGENT)) + .withRequestBody(containing("CREDIT")) + .willReturn(aResponse() + .withStatus(403))); + wireMockServer.stubFor(get(urlEqualTo("/binary")) .atPriority(1) .withHeader("User-Agent", equalTo(USER_AGENT)) diff --git a/solid/src/test/java/com/inrupt/client/solid/SolidProblemDetailsTest.java b/solid/src/test/java/com/inrupt/client/solid/SolidProblemDetailsTest.java new file mode 100644 index 00000000000..9502088b2f7 --- /dev/null +++ b/solid/src/test/java/com/inrupt/client/solid/SolidProblemDetailsTest.java @@ -0,0 +1,206 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.solid; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.inrupt.client.Headers; +import com.inrupt.client.ProblemDetails; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +// Ideally, this class should be in the api module, but it creates +// a circular dependency with the JSON module implementation. +class SolidProblemDetailsTest { + private static final URI POD = URI.create("https://storage.test/pod/"); + + Headers mockProblemDetailsHeader() { + final List headerValues = new ArrayList<>(); + headerValues.add("application/problem+json"); + final Map> headerMap = new HashMap<>(); + headerMap.put("Content-Type", headerValues); + return Headers.of(headerMap); + } + + @Test + void testEmptyProblemDetails() { + final int statusCode = 400; + final ProblemDetails pd = SolidProblemDetails.fromErrorResponse( + POD, + statusCode, + mockProblemDetailsHeader(), + "{}".getBytes() + ); + assertEquals(ProblemDetails.DEFAULT_TYPE, pd.getType()); + assertEquals(statusCode, pd.getStatus()); + assertNull(pd.getTitle()); + assertNull(pd.getDetail()); + assertNull(pd.getInstance()); + } + + @Test + void testRelativeUriProblemDetails() { + final int statusCode = 400; + final UUID instance = UUID.randomUUID(); + final ProblemDetails pd = SolidProblemDetails.fromErrorResponse( + POD, + statusCode, + mockProblemDetailsHeader(), + ("{" + + "\"title\":\"Some title\"," + + "\"status\":400," + + "\"detail\":\"Some details\"," + + "\"instance\":\"Instance" + instance + "\"," + + "\"type\":\"SomeType\"" + + "}").getBytes() + ); + assertEquals(URI.create("https://storage.test/pod/SomeType"), pd.getType()); + assertEquals(statusCode, pd.getStatus()); + Assertions.assertEquals("Some title", pd.getTitle()); + assertEquals("Some details", pd.getDetail()); + assertEquals("https://storage.test/pod/Instance" + instance, pd.getInstance().toString()); + } + + @Test + void testCompleteProblemDetails() { + final int statusCode = 400; + final UUID instance = UUID.randomUUID(); + final ProblemDetails pd = SolidProblemDetails.fromErrorResponse( + POD, + statusCode, + mockProblemDetailsHeader(), + ("{" + + "\"title\":\"Some title\"," + + "\"status\":400," + + "\"detail\":\"Some details\"," + + "\"instance\":\"urn:uuid:" + instance + "\"," + + "\"type\":\"https://example.org/type\"" + + "}").getBytes() + ); + assertEquals("https://example.org/type", pd.getType().toString()); + assertEquals(statusCode, pd.getStatus()); + Assertions.assertEquals("Some title", pd.getTitle()); + assertEquals("Some details", pd.getDetail()); + assertEquals("urn:uuid:" + instance, pd.getInstance().toString()); + } + + @Test + void testIgnoreUnknownProblemDetails() { + final int statusCode = 400; + final ProblemDetails pd = SolidProblemDetails.fromErrorResponse( + POD, + statusCode, + mockProblemDetailsHeader(), + ("{" + + "\"title\":\"Some title\"," + + "\"status\":400," + + "\"detail\":\"Some details\"," + + "\"instance\":\"https://example.org/instance\"," + + "\"type\":\"https://example.org/type\"," + + "\"unknown\":\"Some unknown property\"" + + "}").getBytes() + ); + assertEquals("https://example.org/type", pd.getType().toString()); + assertEquals(statusCode, pd.getStatus()); + Assertions.assertEquals("Some title", pd.getTitle()); + assertEquals("Some details", pd.getDetail()); + assertEquals("https://example.org/instance", pd.getInstance().toString()); + } + + @Test + void testInvalidStatusProblemDetails() { + final int statusCode = 400; + final ProblemDetails pd = SolidProblemDetails.fromErrorResponse( + POD, + statusCode, + mockProblemDetailsHeader(), + ("{" + + "\"status\":\"Some invalid status\"" + + "}").getBytes() + ); + assertEquals(statusCode, pd.getStatus()); + } + + @Test + void testMismatchingStatusProblemDetails() { + final int statusCode = 400; + final ProblemDetails pd = SolidProblemDetails.fromErrorResponse( + POD, + statusCode, + mockProblemDetailsHeader(), + ("{" + + "\"status\":500" + + "}").getBytes() + ); + assertEquals(500, pd.getStatus()); + } + + @Test + void testInvalidTypeProblemDetails() { + final ProblemDetails pd = SolidProblemDetails.fromErrorResponse( + POD, + 400, + mockProblemDetailsHeader(), + ("{" + + "\"type\":\"Some invalid type\"" + + "}").getBytes() + ); + assertEquals(ProblemDetails.DEFAULT_TYPE, pd.getType()); + } + + @Test + void testInvalidInstanceProblemDetails() { + final ProblemDetails pd = SolidProblemDetails.fromErrorResponse( + POD, + 400, + mockProblemDetailsHeader(), + ("{" + + "\"instance\":\"Some invalid instance\"" + + "}").getBytes() + ); + assertNull(pd.getInstance()); + } + + @Test + void testInvalidProblemDetails() { + final int statusCode = 400; + final ProblemDetails pd = SolidProblemDetails.fromErrorResponse( + POD, + statusCode, + mockProblemDetailsHeader(), + "Not valid application/problem+json".getBytes() + ); + assertEquals(ProblemDetails.DEFAULT_TYPE, pd.getType()); + assertEquals(statusCode, pd.getStatus()); + assertNull(pd.getTitle()); + assertNull(pd.getDetail()); + assertNull(pd.getInstance()); + } +} diff --git a/solid/src/test/java/com/inrupt/client/solid/TransactionResource.java b/solid/src/test/java/com/inrupt/client/solid/TransactionResource.java new file mode 100644 index 00000000000..0e5eef7905a --- /dev/null +++ b/solid/src/test/java/com/inrupt/client/solid/TransactionResource.java @@ -0,0 +1,132 @@ +/* + * Copyright Inrupt Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.inrupt.client.solid; + +import com.inrupt.client.spi.JsonService; +import com.inrupt.client.spi.ServiceProvider; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URI; +import java.time.Instant; +import java.util.Objects; + +class TransactionResource extends SolidNonRDFSource { + + private final JsonService jsonService; + + private Transaction transaction; + + public TransactionResource(final URI identifier, final String contentType, final InputStream entity) { + super(identifier, contentType, entity); + + this.jsonService = ServiceProvider.getJsonService(); + try { + this.transaction = jsonService.fromJson(super.getEntity(), Transaction.class); + } catch (IOException ex) { + throw new UncheckedIOException("Unable to parse Transaction data", ex); + } + } + + public Transaction getTransaction() { + return transaction; + } + + public void setTransaction(final Transaction transaction) { + this.transaction = Objects.requireNonNull(transaction, "transaction may not be null!"); + } + + @Override + public InputStream getEntity() throws IOException { + try (final ByteArrayOutputStream output = new ByteArrayOutputStream()) { + jsonService.toJson(transaction, output); + return new ByteArrayInputStream(output.toByteArray()); + } + } + + record Transaction(String id, String description, Instant date, double amount, TransactionType type, + String category) { + static class Builder { + private String transactionId; + private String transactionDescription; + private Instant transactionDate; + private double transactionAmount; + private TransactionType transactionType; + private String transactionCategory; + + public Builder id(final String id) { + this.transactionId = id; + return this; + } + + public Builder description(final String description) { + this.transactionDescription = description; + return this; + } + + public Builder date(final Instant date) { + this.transactionDate = date; + return this; + } + + public Builder amount(final double amount) { + this.transactionAmount = amount; + return this; + } + + public Builder type(final TransactionType type) { + this.transactionType = type; + return this; + } + + public Builder category(final String category) { + this.transactionCategory = category; + return this; + } + + public Transaction build() { + return new Transaction(this.transactionId, this.transactionDescription, this.transactionDate, + this.transactionAmount, this.transactionType, this.transactionCategory); + } + } + + static Builder newBuilder() { + return new Builder(); + } + + static Builder newBuilder(final Transaction transaction) { + return new Builder() + .id(transaction.id()) + .description(transaction.description()) + .date(transaction.date()) + .amount(transaction.amount()) + .type(transaction.type()) + .category(transaction.category()); + } + } + + enum TransactionType { + CREDIT, DEBIT + } +} diff --git a/solid/src/test/resources/__files/transaction.json b/solid/src/test/resources/__files/transaction.json new file mode 100644 index 00000000000..39841a6cfe9 --- /dev/null +++ b/solid/src/test/resources/__files/transaction.json @@ -0,0 +1,8 @@ +{ + "id": "testid", + "date": "2025-09-28T15:37:00Z", + "description": "sample description", + "amount": 20.12, + "type": "CREDIT", + "category": "groceries" +} diff --git a/spring/pom.xml b/spring/pom.xml index 16876434128..d3e0f7c634b 100644 --- a/spring/pom.xml +++ b/spring/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-spring @@ -13,11 +13,6 @@ Integration utilities for Spring support. - - 11 - 11 - - @@ -95,19 +90,15 @@ ${glassfish.json.version} test - - - - java-17 - - [17,) - - - 6.2.1 - - - + + + org.springframework + spring-web + 7.0.8 + provided + + diff --git a/integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/package-info.java b/spring/src/main/java/com/inrupt/client/spring/package-info.java similarity index 79% rename from integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/package-info.java rename to spring/src/main/java/com/inrupt/client/spring/package-info.java index b6e1f848616..85ac3220525 100644 --- a/integration/customokhttp/src/main/java/com/inrupt/client/integration/customokhttp/package-info.java +++ b/spring/src/main/java/com/inrupt/client/spring/package-info.java @@ -19,6 +19,13 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /** - *

Custom OkHttp Client for Integration Inrupt Java Client Libraries tests.

+ *

Integration tooling for Spring applications.

+ * + *

When integrating with Spring applications, one can convert an OAuth2User into a Session object: + * + *

{@code
+    Optional session = SessionUtils.asSession(user);
+    }
+ * 
*/ -package com.inrupt.client.integration.customokhttp; +package com.inrupt.client.spring; diff --git a/src/site/site.xml b/src/site/site.xml index 91f90eb39b3..f725f39a13a 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - index.html + + @@ -21,4 +19,4 @@ 1.11.1 - + diff --git a/test/pom.xml b/test/pom.xml index 4a0072a6a81..1573799a695 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-test @@ -39,7 +39,7 @@
org.wiremock - wiremock + wiremock-standalone ${wiremock.version} provided diff --git a/test/src/site/site.xml b/test/src/site/site.xml index 22e78304832..b68ba31c85f 100644 --- a/test/src/site/site.xml +++ b/test/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/uma/pom.xml b/uma/pom.xml index e4092b069f9..9325e84139f 100644 --- a/uma/pom.xml +++ b/uma/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-uma @@ -13,11 +13,6 @@ UMA abstractions for the Inrupt Java Client Libraries. - - 1.8 - 1.8 - - com.inrupt.client diff --git a/uma/src/main/java/com/inrupt/client/uma/UmaAuthenticationProvider.java b/uma/src/main/java/com/inrupt/client/uma/UmaAuthenticationProvider.java index 9f9fa1f8fc4..73a7bd3e534 100644 --- a/uma/src/main/java/com/inrupt/client/uma/UmaAuthenticationProvider.java +++ b/uma/src/main/java/com/inrupt/client/uma/UmaAuthenticationProvider.java @@ -207,6 +207,11 @@ public CompletionStage authenticate(final Session session, final Req return CompletableFuture.completedFuture(null); }); } + + @Override + public String toString() { + return getName(); + } } static boolean supportsProfile(final Metadata metadata, final URI profile) { diff --git a/uma/src/site/site.xml b/uma/src/site/site.xml index f8fcc94121d..1d29911e3f6 100644 --- a/uma/src/site/site.xml +++ b/uma/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/vocabulary/pom.xml b/vocabulary/pom.xml index 2c4c6e9d57d..7e2cd5d4b5b 100644 --- a/vocabulary/pom.xml +++ b/vocabulary/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-vocabulary @@ -46,6 +46,10 @@ + + org.jacoco + jacoco-maven-plugin +
diff --git a/vocabulary/src/main/java/com/inrupt/client/vocabulary/ACP.java b/vocabulary/src/main/java/com/inrupt/client/vocabulary/ACP.java index 60b01bac0b1..c5e5dbad9de 100644 --- a/vocabulary/src/main/java/com/inrupt/client/vocabulary/ACP.java +++ b/vocabulary/src/main/java/com/inrupt/client/vocabulary/ACP.java @@ -31,6 +31,19 @@ public final class ACP { private static String namespace = "http://www.w3.org/ns/solid/acp#"; + // Named Individuals + /** The acp:AuthenticatedAgent URI. */ + public static final URI AuthenticatedAgent = URI.create(namespace + "AuthenticatedAgent"); + + /** The acp:PublicAgent URI. */ + public static final URI PublicAgent = URI.create(namespace + "PublicAgent"); + + /** The acp:PublicClient URI. */ + public static final URI PublicClient = URI.create(namespace + "PublicClient"); + + /** The acp:PublicIssuer URI. */ + public static final URI PublicIssuer = URI.create(namespace + "PublicIssuer"); + // Properties /** * The acp:resource URI. @@ -76,6 +89,18 @@ public final class ACP { * The acp:vc URI. */ public static final URI vc = URI.create(namespace + "vc"); + /** + * The acp:client URI. + */ + public static final URI client = URI.create(namespace + "client"); + /** + * The acp:agent URI. + */ + public static final URI agent = URI.create(namespace + "agent"); + /** + * The acp:issuer URI. + */ + public static final URI issuer = URI.create(namespace + "issuer"); // Classes /** diff --git a/vocabulary/src/main/java/com/inrupt/client/vocabulary/PIM.java b/vocabulary/src/main/java/com/inrupt/client/vocabulary/PIM.java index fc670eccc51..1976067e512 100644 --- a/vocabulary/src/main/java/com/inrupt/client/vocabulary/PIM.java +++ b/vocabulary/src/main/java/com/inrupt/client/vocabulary/PIM.java @@ -37,6 +37,11 @@ public final class PIM { */ public static final URI storage = URI.create(namespace + "storage"); + /** + * The pim:Storage URI. + */ + public static final URI Storage = URI.create(namespace + "Storage"); + /** * Get the PIM namespace URI. * diff --git a/vocabulary/src/site/site.xml b/vocabulary/src/site/site.xml index 35df9cd2fa8..8283f52c2e7 100644 --- a/vocabulary/src/site/site.xml +++ b/vocabulary/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - + diff --git a/webid/pom.xml b/webid/pom.xml index 3dae094ab29..1c79738504a 100644 --- a/webid/pom.xml +++ b/webid/pom.xml @@ -4,7 +4,7 @@ com.inrupt.client inrupt-client - 1.2.0-SNAPSHOT + 2.0.1-SNAPSHOT inrupt-client-webid diff --git a/webid/src/site/site.xml b/webid/src/site/site.xml index 643f2337e8b..7319434ab2f 100644 --- a/webid/src/site/site.xml +++ b/webid/src/site/site.xml @@ -1,9 +1,7 @@ - + - - Inrupt - images/inrupt_logo-2020.svg - https://inrupt.com + + @@ -13,4 +11,4 @@ - +