diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index e9ecf12d..ce3d6572 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -22,22 +22,22 @@ on: jobs: build: - runs-on: ubuntu-latest - + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up JDK 11 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: + distribution: 'temurin' java-version: 11 - name: Cache Maven packages - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - name: Build with Maven - run: mvn -B clean package -s maven/cnaf-mirror-settings.xml + run: mvn -B clean package - name: Checkstyle with Maven - run: mvn clean compile -s maven/cnaf-mirror-settings.xml -U -Dmaven.test.failure.ignore -DfailIfNoTests=false checkstyle:check -Dcheckstyle.config.location=google_checks.xml + run: mvn clean compile -U -Dmaven.test.failure.ignore -DfailIfNoTests=false checkstyle:check -Dcheckstyle.config.location=google_checks.xml diff --git a/.github/workflows/run-testsuite.yml b/.github/workflows/run-testsuite.yml new file mode 100644 index 00000000..bad5c3ad --- /dev/null +++ b/.github/workflows/run-testsuite.yml @@ -0,0 +1,53 @@ +name: Run testsuite + +on: + push: + +jobs: + run-testsuite: + name: WebDAV test suite + + runs-on: ubuntu-latest + + env: + ARTIFACTS: ${HOME}/artifacts + ROBOT_ARGS: -L DEBUG --variable dav.host:storm.test.example --variable remote.dav.host:storm-alias.test.example --variable remote.davs.port:443 --exclude known-issue + OIDC_AGENT_SECRET: ${{ secrets.OIDC_AGENT_SECRET }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Provide trustanchors + working-directory: compose + run: docker compose up trust + + - name: Start services + working-directory: compose + run: docker compose up --build -d storage-setup webdav nginx + + - name: Run testsuite + if: ${{ always() }} + working-directory: compose + run: | + docker compose up -d ts + docker compose exec -T ts bash -c '/scripts/ci-run-testsuite.sh' + + - name: Create artifacts directory + if: ${{ always() }} + run: mkdir -p ${ARTIFACTS} + + - name: Collect test reports + if: ${{ always() }} + run: docker cp storm-webdav-ts-1:/home/test/robot/reports ${ARTIFACTS} + + - name: Collect service log + if: ${{ always() }} + run: docker logs storm-webdav-webdav-1 > ${ARTIFACTS}/storm-webdav-server.log 2>&1 + + - name: Archive reports + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: logs-and-reports + path: ${{ env.ARTIFACTS }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 23af7abd..d231dc4d 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -10,21 +10,22 @@ jobs: name: Build and analyze runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Set up JDK 11 - uses: actions/setup-java@v1 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - java-version: 11 + distribution: 'temurin' + java-version: 17 - name: Cache SonarCloud packages - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar restore-keys: ${{ runner.os }}-sonar - name: Cache Maven packages - uses: actions/cache@v1 + uses: actions/cache@v4 with: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} @@ -33,4 +34,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -s maven/cnaf-mirror-settings.xml -B -U install org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=italiangrid_storm-webdav \ No newline at end of file + run: mvn -B -U install org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=italiangrid_storm-webdav diff --git a/.gitignore b/.gitignore index c8aa1829..2a738a16 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ .springBeans .idea storm-webdav-server.iml +/robot/reports +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fb6334e..435384f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,36 +1,65 @@ # Changelog +## 1.4.2 (2023-06-27) + +## Description + +This release: + +* upgrades significant dependencies (spring-boot, canl, bouncycastle, jQuery) +* removes the support for TRACE method +* tunes some default values (default TPC timeout, default heap size, etc.) +* and fixes other minor bugs/issues. + +### fixes + +* [[STOR-1396](https://issues.infn.it/jira/browse/STOR-1396)] - Ensure adler32 checksums are always 8 chars long +* [[STOR-1450](https://issues.infn.it/jira/browse/STOR-1450)] - Increase default timeout for TPC to 30 seconds +* [[STOR-1500](https://issues.infn.it/jira/browse/STOR-1500)] - When redis is disabled the health indicator for redis should be disabled +* [[STOR-1574](https://issues.infn.it/jira/browse/STOR-1574)] - Old java/canl creates problems with encoding of subject/issuer names in self-signed certificates +* [[STOR-1440](https://issues.infn.it/jira/browse/STOR-1440)] - StoRM WebDAV should configure a bigger heap by default +* [[STOR-1497](https://issues.infn.it/jira/browse/STOR-1497)] - Upgrade canl-java to v2.6.0 +* [[STOR-1515](https://issues.infn.it/jira/browse/STOR-1515)] - StoRM WebDAV metrics on TPC.pull/push.throughput +* [[STOR-1555](https://issues.infn.it/jira/browse/STOR-1555)] - Upgrade jQuery version +* [[STOR-1556](https://issues.infn.it/jira/browse/STOR-1556)] - Remove TRACE from allowed methods +* [[STOR-1557](https://issues.infn.it/jira/browse/STOR-1557)] - Upgrade Spring Boot version to the latest +* [[STOR-1558](https://issues.infn.it/jira/browse/STOR-1558)] - Update bouncycastle version to 1.67 +* [[STOR-1576](https://issues.infn.it/jira/browse/STOR-1576)] - Add .well-known endpoint for StoRM WebDAV to point to the Tape REST endpoint + + +## 1.4.1 (2021-05-12) + +This release fixes the failed state shown on stop/restart of the service due to a misunderstood exit code meaning. + +### Fixed + +- [[STOR-1400](https://issues.infn.it/jira/browse/STOR-1400)] - StoRM WebDAV service enters failed state when stopped + ## 1.4.0 (2021-04-01) ### Added -- [Add support for externalized session management](https://issues.infn.it/jira/browse/STOR-1336) +- [[STOR-1336](https://issues.infn.it/jira/browse/STOR-1336)] - Add support for externalized session management ### Fixed -- [Login with OIDC button not shown for error - pages](https://issues.infn.it/jira/browse/STOR-1335) -- [StoRM WebDAV: Login with OIDC button displayed only on storage area index - page]( https://issues.infn.it/jira/browse/STOR-1332) -- [StoRM WebDAV rpm doesn't set the proper ownership on - /var/log/storm](https://issues.infn.it/jira/browse/STOR-1298) -- [StoRM WebDAV package should install Java - 11](https://issues.infn.it/jira/browse/STOR-1358) +- [[STOR-1335](https://issues.infn.it/jira/browse/STOR-1335)] - Login with OIDC button not shown for error + pages +- [[STOR-1332](https://issues.infn.it/jira/browse/STOR-1332)] - Login with OIDC button displayed only on storage area index page +- [[STOR-1298](https://issues.infn.it/jira/browse/STOR-1298)] - StoRM WebDAV RPM doesn't set the proper ownership on `/var/log/storm` +- [[STOR-1358](https://issues.infn.it/jira/browse/STOR-1358)] - StoRM WebDAV package should install Java 11 ## 1.2.0 (2019-08-??) ### Added -- [Spring boot updated to 2.1.4.RELEASE][STOR-1098] -- [Introduced support for Conscrypt JSSE provider to improve TLS - performace][STOR-1097] +- [[STOR-1098](https://issues.infn.it/jira/browse/STOR-1098)] - Spring boot updated to 2.1.4.RELEASE +- [[STOR-1097](https://issues.infn.it/jira/browse/STOR-1097)] - Introduced support for Conscrypt JSSE provider to improve TLS performance ### Fixed -- [StoRM WebDAV default configuration does not depend anymore on - iam-test.indigo-datacloud.eu][STOR-1095] -- [Unreachable OpenID Connect provider causes StoRM WebDAV startup - failure][STOR-1096] +- [[STOR-1095](https://issues.infn.it/jira/browse/STOR-1095)] - StoRM WebDAV default configuration does not depend anymore on `iam-test.indigo-datacloud.eu` +- [[STOR-1096](https://issues.infn.it/jira/browse/STOR-1096)] - Unreachable OpenID Connect provider causes StoRM WebDAV startup failure ## 1.1.0 (2019-02-28) @@ -46,8 +75,3 @@ - POST handled as GET fixed - -[STOR-1095]: https://issues.infn.it/jira/browse/STOR-1095 -[STOR-1096]: https://issues.infn.it/jira/browse/STOR-1096 -[STOR-1097]: https://issues.infn.it/jira/browse/STOR-1097 -[STOR-1098]: https://issues.infn.it/jira/browse/STOR-1098 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..9d3b7ce3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# https://spring.io/guides/topicals/spring-boot-docker#_multi_stage_build +FROM eclipse-temurin:11-jdk-alpine as build +WORKDIR /workspace/app +RUN apk add maven +COPY pom.xml . +RUN mvn dependency:resolve +RUN mvn dependency:resolve-plugins +COPY .git .git +COPY etc etc +COPY src src +RUN mvn package -Dmaven.test.skip +RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar) + +FROM eclipse-temurin:11-centos7 +ENV STORM_WEBDAV_JVM_OPTS="-Dspring.profiles.active=dev" +ARG DEPENDENCY=/workspace/app/target/dependency + +#WORKDIR /app +COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib +COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF +COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app +COPY src src + +ARG USERNAME=storm +ARG USER_UID=1000 +ARG USER_GID=${USER_UID} + +RUN groupadd --gid ${USER_GID} ${USERNAME} +RUN useradd --uid ${USER_UID} --gid ${USER_GID} -m ${USERNAME} +RUN echo ${USERNAME} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers +RUN chmod 0440 /etc/sudoers +USER ${USERNAME} + +ENTRYPOINT java ${STORM_WEBDAV_JVM_OPTS} -cp app:app/lib/* org.italiangrid.storm.webdav.WebdavService diff --git a/cnaf-mirror-settings.xml b/cnaf-mirror-settings.xml deleted file mode 100644 index 3280e381..00000000 --- a/cnaf-mirror-settings.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - false - - - nexus - CNAF maven mirror - https://repo.cloud.cnaf.infn.it/repository/maven-public - * - - - - - nexus - - - central - http://central - true - true - - - - - - nexus - - diff --git a/compose/.env b/compose/.env index 80bf19fb..27d31cbe 100644 --- a/compose/.env +++ b/compose/.env @@ -1 +1,9 @@ COMPOSE_PROJECT_NAME=storm-webdav +TRUST_IMAGE=indigoiam/egi-trustanchors +TRUST_IMAGE_TAG=igi-test-ca +WEBDAV_IMAGE=italiangrid/storm-webdav-centos7 +WEBDAV_IMAGE_TAG=latest +TS_IMAGE=indigoiam/robot-framework +TS_IMAGE_TAG=latest +NGINX_IMAGE=baltig.infn.it:4567/cnafsd/ngx_http_voms_module/nginx-httpg-voms +NGINX_IMAGE_TAG=latest \ No newline at end of file diff --git a/compose/README.md b/compose/README.md new file mode 100644 index 00000000..057b2dc2 --- /dev/null +++ b/compose/README.md @@ -0,0 +1,25 @@ +# Docker compose for StoRM WebDAV + +Run the services with + +``` +$ docker-compose up -d +``` + +The docker-compose contains several services: + +* `trust`: docker image for the GRID CA certificates, mounted in the `/etc/grid-security/certificates` path of the other services. The _igi-test-ca_ used in this deployment is also present in that path +* `storage-setup`: sidecar container, used to allocate proper volumes (i.e. storage areas) owned by _storm_ +* `webdav`: is the main service, also known as StoRM WebDAV. The StoRM WebDAV base URL is https://storm.test.example:8443. It serves the following storage areas: + * `test.vo` for users presenting a proxy issued by a _test.vo_ VO + * `noauth`: which allows read/write mode also to anonymous users + * `fga`: for a fined grained authorization storage area. Its access policies are set in the [application](./assets/etc/storm/webdav/config/application-policies.yml) file + * `oauth-authz`: for users presenting a token issued by the [IAM DEV](https://iam-dev.cloud.cnaf.infn.it) +* `ts`: used for running the StoRM WebDAV testsuite. It shares the storage with the `webdav` service, to run local tests +* `nginx`: is the NGINX service supporting VOMS authentication, used as remote StoRM server for WebDAV calls. It does not forward requests to StoRM WebDAV, but just serves local resources in a separate storage. URL of this service is https://storm-alias.test.example. In the testsuite, the local resources are served by an `oauth-authz` endpoint, that does not require authentication. + +To resolve the hostname of the service, add a line in your `/etc/hosts` file with + +``` +127.0.0.1 storm.test.example storm-alias.test.example +``` \ No newline at end of file diff --git a/compose/assets/certs/hostcert.pem b/compose/assets/certs/hostcert.pem index c97b8a7b..3cbd4774 100644 --- a/compose/assets/certs/hostcert.pem +++ b/compose/assets/certs/hostcert.pem @@ -1,86 +1,85 @@ Certificate: Data: Version: 3 (0x2) - Serial Number: 801 (0x321) + Serial Number: 19 (0x13) Signature Algorithm: sha512WithRSAEncryption Issuer: C=IT, O=IGI, CN=Test CA Validity - Not Before: Oct 15 15:57:05 2018 GMT - Not After : Oct 12 15:57:05 2028 GMT - Subject: C=IT, O=IGI, CN=storm dev + Not Before: Oct 19 08:55:57 2022 GMT + Not After : Oct 16 08:55:57 2032 GMT + Subject: C=IT, O=IGI, CN=*.test.example Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: - 00:cb:98:91:d4:9f:f5:a7:0a:1c:cf:b8:51:7d:2e: - fa:a9:c7:df:74:75:bb:81:1a:52:e0:a5:1e:48:56: - c5:85:39:bf:90:4a:2b:be:c5:ef:83:0a:4a:e0:86: - 84:81:79:14:4f:8e:70:ba:8a:a3:68:07:a3:2c:be: - 76:d6:fc:28:bf:91:31:67:45:eb:2e:b6:ce:31:bd: - 32:d1:f4:a0:88:0c:e9:2f:a0:ee:77:8f:da:c1:1b: - 50:ba:0d:09:05:29:12:b1:4c:98:28:fd:6a:c0:fc: - 9b:d1:40:cd:5c:59:c4:7d:49:bf:c1:0f:a5:3a:42: - 7c:41:0d:1e:25:2e:2e:2e:3d:0c:23:fb:9f:1f:46: - ec:f3:62:aa:a6:ca:85:a9:ea:ec:51:98:26:6e:1a: - bd:cd:0e:eb:22:49:b2:e6:c4:99:2f:6b:3c:ba:82: - 09:46:74:b3:19:a6:dc:b9:a1:83:6d:d5:28:62:43: - ba:1b:f3:e1:1d:61:61:87:b2:cb:1b:14:49:02:de: - d9:10:ca:d7:0c:da:c6:c3:1c:f2:ab:48:27:8d:10: - 17:8b:56:cb:5d:d4:f6:19:65:4c:78:25:cb:3d:be: - a5:93:77:ce:a2:77:97:de:b4:24:8e:aa:3b:dc:c6: - f8:57:d9:a9:ba:42:d9:7a:77:a4:4a:dc:76:07:2b: - 43:c3 + 00:e7:3a:01:a8:93:12:08:f4:a6:c9:89:10:a2:f6: + 6a:6a:d3:93:98:c7:31:c0:e5:8a:3a:44:9b:cf:ef: + b9:3d:05:86:03:61:0e:6e:fc:c6:f9:9a:9e:35:d6: + 3d:38:27:48:cb:77:26:97:15:34:a0:0b:1d:97:31: + dd:18:ec:bf:78:d9:32:9e:00:1a:44:6a:78:15:1f: + ac:7b:3e:bb:ad:b2:b4:32:75:8c:11:d8:31:ec:19: + 7d:bf:ba:5d:1e:70:38:62:10:cf:3a:8a:a4:98:83: + b4:df:e0:50:3b:e5:ec:24:a0:89:14:2c:19:27:48: + 66:c3:d4:1d:74:63:be:63:38:95:3f:64:d0:91:ac: + 95:f7:d9:ca:96:b5:1b:e7:71:70:7b:5f:3b:12:30: + 2c:b8:3a:28:79:84:9c:81:12:db:38:31:6d:2d:2a: + e2:80:05:5c:29:77:53:58:10:19:ee:f9:50:e1:8d: + 3b:2b:e2:c0:0b:d2:9f:3c:a0:95:33:f8:33:17:ce: + 23:0e:31:e8:1e:3d:7e:6a:c9:6d:83:9e:0b:fa:43: + d2:4a:3f:be:d3:19:07:1e:8c:e4:f6:dc:8f:c3:3e: + 3a:8e:66:4a:87:ef:0b:39:db:e8:3e:30:1c:91:9e: + b3:1e:d3:a0:1e:1b:9a:b1:58:99:de:a5:bb:53:3b: + 3b:5d Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Basic Constraints: critical CA:FALSE X509v3 Subject Key Identifier: - AF:52:EA:AC:22:88:70:E5:C6:AA:AE:CC:AD:FB:CA:95:EB:17:3B:15 + 60:FA:21:CE:1C:B5:31:8D:9B:01:F6:08:5B:72:4D:59:5A:F8:71:8C X509v3 Key Usage: critical Digital Signature, Non Repudiation, Key Encipherment X509v3 Extended Key Usage: TLS Web Server Authentication, TLS Web Client Authentication, Microsoft Server Gated Crypto, Netscape Server Gated Crypto, E-mail Protection X509v3 Authority Key Identifier: - keyid:91:77:36:7B:2E:B4:69:F3:27:EA:B7:F6:08:8B:4A:23:A2:11:49:C6 + keyid:50:9B:6F:74:01:E3:1A:03:57:AB:D9:D5:7D:15:64:4C:25:F3:F8:F4 X509v3 Subject Alternative Name: - DNS:storm.example, DNS:storm-alias.example, DNS:other.example, DNS:localhost + DNS:*.test.example Signature Algorithm: sha512WithRSAEncryption - b5:36:9a:2d:e4:79:56:1a:1c:d0:34:e4:d8:06:2a:03:94:65: - cc:a7:71:bf:88:c6:f9:1d:bf:20:18:d4:25:6a:8a:a5:5e:97: - 64:8e:23:d2:51:0a:fb:3a:96:68:f6:a3:75:bd:74:6d:3d:4d: - 05:54:1c:b4:43:ee:33:bd:66:80:ee:81:50:f4:9c:ea:38:74: - 22:f3:ab:b1:41:04:7f:f5:64:07:49:78:9e:73:a5:00:0d:8f: - e6:c9:ec:bc:3b:f7:00:7e:9e:09:1a:9b:a4:40:a7:39:90:1c: - fa:ca:ec:31:53:52:27:93:88:db:18:b3:f0:b7:7f:65:4e:06: - c5:f5:b4:9e:6c:af:69:ef:da:ea:4c:e8:50:ed:dc:49:a7:fe: - 69:90:cf:77:69:58:49:0a:1c:50:5e:ab:26:b0:52:31:ca:6f: - 8a:11:78:80:c5:9e:4f:43:40:60:f3:99:46:4d:8d:51:5a:e5: - 04:90:9e:ce:40:4a:c5:35:b1:f1:d1:63:86:8b:42:73:79:7a: - f7:33:d3:69:22:45:a2:82:0c:05:69:7d:00:2b:e5:c9:44:38: - f8:ae:e1:81:71:04:b8:48:bf:51:91:22:4e:90:c6:ad:91:cc: - 30:a5:e8:53:4f:64:b1:3d:7a:c8:cd:ae:b6:b8:7c:dc:c7:98: - 36:eb:a5:e4 + 79:82:f2:54:44:98:96:25:c2:83:c9:0f:19:69:1c:f6:a7:19: + 0d:61:90:f9:96:23:e2:ab:5a:30:db:55:d7:4f:b0:ff:b2:7b: + 41:da:35:97:47:86:e4:85:00:6d:11:64:ee:32:a4:64:ee:fe: + b2:83:a5:24:4a:ce:c3:91:ae:db:3d:5b:af:fa:7e:81:1a:1c: + 69:d0:1a:9e:70:0e:9e:74:85:6b:48:90:6a:1b:62:ff:6e:b3: + 84:30:b7:7f:fa:c0:3e:ee:91:70:0b:f2:13:ea:c8:2c:aa:d8: + cb:3c:60:b1:08:f9:8e:bf:c2:e4:ce:92:6a:7e:0a:41:49:94: + 8f:e5:6e:71:f9:47:04:1a:18:1f:65:47:d6:1c:ea:a9:90:71: + 82:1b:3b:1f:a5:f2:02:ce:5c:d6:2e:5d:1e:05:c4:92:9e:3d: + 8e:ce:fa:00:83:01:d5:c3:c1:cf:e2:e5:fb:08:80:08:f4:6c: + 26:64:96:db:cd:be:4c:e7:bc:8f:af:3d:0e:0c:f7:d2:52:15: + 9c:d5:15:0d:51:b3:95:72:78:1d:8c:ca:37:55:7a:c0:b0:0f: + 18:ae:de:d0:27:6f:1b:e4:5d:1d:4b:f9:4c:5d:44:49:ed:cf: + c2:9e:e7:c6:55:72:ce:2f:43:a7:2f:88:de:b7:da:9f:82:a6: + 54:77:c2:2e -----BEGIN CERTIFICATE----- -MIIDwzCCAqugAwIBAgICAyEwDQYJKoZIhvcNAQENBQAwLTELMAkGA1UEBhMCSVQx -DDAKBgNVBAoMA0lHSTEQMA4GA1UEAwwHVGVzdCBDQTAeFw0xODEwMTUxNTU3MDVa -Fw0yODEwMTIxNTU3MDVaMC8xCzAJBgNVBAYTAklUMQwwCgYDVQQKDANJR0kxEjAQ -BgNVBAMMCXN0b3JtIGRldjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AMuYkdSf9acKHM+4UX0u+qnH33R1u4EaUuClHkhWxYU5v5BKK77F74MKSuCGhIF5 -FE+OcLqKo2gHoyy+dtb8KL+RMWdF6y62zjG9MtH0oIgM6S+g7neP2sEbULoNCQUp -ErFMmCj9asD8m9FAzVxZxH1Jv8EPpTpCfEENHiUuLi49DCP7nx9G7PNiqqbKhanq -7FGYJm4avc0O6yJJsubEmS9rPLqCCUZ0sxmm3Lmhg23VKGJDuhvz4R1hYYeyyxsU -SQLe2RDK1wzaxsMc8qtIJ40QF4tWy13U9hllTHglyz2+pZN3zqJ3l960JI6qO9zG -+FfZqbpC2Xp3pErcdgcrQ8MCAwEAAaOB6jCB5zAMBgNVHRMBAf8EAjAAMB0GA1Ud -DgQWBBSvUuqsIohw5caqrsyt+8qV6xc7FTAOBgNVHQ8BAf8EBAMCBeAwPgYDVR0l -BDcwNQYIKwYBBQUHAwEGCCsGAQUFBwMCBgorBgEEAYI3CgMDBglghkgBhvhCBAEG -CCsGAQUFBwMEMB8GA1UdIwQYMBaAFJF3NnsutGnzJ+q39giLSiOiEUnGMEcGA1Ud -EQRAMD6CDXN0b3JtLmV4YW1wbGWCE3N0b3JtLWFsaWFzLmV4YW1wbGWCDW90aGVy -LmV4YW1wbGWCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQ0FAAOCAQEAtTaaLeR5Vhoc -0DTk2AYqA5RlzKdxv4jG+R2/IBjUJWqKpV6XZI4j0lEK+zqWaPajdb10bT1NBVQc -tEPuM71mgO6BUPSc6jh0IvOrsUEEf/VkB0l4nnOlAA2P5snsvDv3AH6eCRqbpECn -OZAc+srsMVNSJ5OI2xiz8Ld/ZU4GxfW0nmyvae/a6kzoUO3cSaf+aZDPd2lYSQoc -UF6rJrBSMcpvihF4gMWeT0NAYPOZRk2NUVrlBJCezkBKxTWx8dFjhotCc3l69zPT -aSJFooIMBWl9ACvlyUQ4+K7hgXEEuEi/UZEiTpDGrZHMMKXoU09ksT16yM2utrh8 -3MeYNuul5A== +MIIDmTCCAoGgAwIBAgIBEzANBgkqhkiG9w0BAQ0FADAtMQswCQYDVQQGEwJJVDEM +MAoGA1UECgwDSUdJMRAwDgYDVQQDDAdUZXN0IENBMB4XDTIyMTAxOTA4NTU1N1oX +DTMyMTAxNjA4NTU1N1owNDELMAkGA1UEBhMCSVQxDDAKBgNVBAoMA0lHSTEXMBUG +A1UEAwwOKi50ZXN0LmV4YW1wbGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQDnOgGokxII9KbJiRCi9mpq05OYxzHA5Yo6RJvP77k9BYYDYQ5u/Mb5mp41 +1j04J0jLdyaXFTSgCx2XMd0Y7L942TKeABpEangVH6x7PrutsrQydYwR2DHsGX2/ +ul0ecDhiEM86iqSYg7Tf4FA75ewkoIkULBknSGbD1B10Y75jOJU/ZNCRrJX32cqW +tRvncXB7XzsSMCy4Oih5hJyBEts4MW0tKuKABVwpd1NYEBnu+VDhjTsr4sAL0p88 +oJUz+DMXziMOMegePX5qyW2Dngv6Q9JKP77TGQcejOT23I/DPjqOZkqH7ws52+g+ +MByRnrMe06AeG5qxWJnepbtTOztdAgMBAAGjgbwwgbkwDAYDVR0TAQH/BAIwADAd +BgNVHQ4EFgQUYPohzhy1MY2bAfYIW3JNWVr4cYwwDgYDVR0PAQH/BAQDAgXgMD4G +A1UdJQQ3MDUGCCsGAQUFBwMBBggrBgEFBQcDAgYKKwYBBAGCNwoDAwYJYIZIAYb4 +QgQBBggrBgEFBQcDBDAfBgNVHSMEGDAWgBRQm290AeMaA1er2dV9FWRMJfP49DAZ +BgNVHREEEjAQgg4qLnRlc3QuZXhhbXBsZTANBgkqhkiG9w0BAQ0FAAOCAQEAeYLy +VESYliXCg8kPGWkc9qcZDWGQ+ZYj4qtaMNtV10+w/7J7Qdo1l0eG5IUAbRFk7jKk +ZO7+soOlJErOw5Gu2z1br/p+gRocadAannAOnnSFa0iQahti/26zhDC3f/rAPu6R +cAvyE+rILKrYyzxgsQj5jr/C5M6San4KQUmUj+VucflHBBoYH2VH1hzqqZBxghs7 +H6XyAs5c1i5dHgXEkp49js76AIMB1cPBz+Ll+wiACPRsJmSW282+TOe8j689Dgz3 +0lIVnNUVDVGzlXJ4HYzKN1V6wLAPGK7e0CdvG+RdHUv5TF1ESe3Pwp7nxlVyzi9D +py+I3rfan4KmVHfCLg== -----END CERTIFICATE----- diff --git a/compose/assets/certs/hostkey.pem b/compose/assets/certs/hostkey.pem index c0c7d06e..610502b8 100644 --- a/compose/assets/certs/hostkey.pem +++ b/compose/assets/certs/hostkey.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAy5iR1J/1pwocz7hRfS76qcffdHW7gRpS4KUeSFbFhTm/kEor -vsXvgwpK4IaEgXkUT45wuoqjaAejLL521vwov5ExZ0XrLrbOMb0y0fSgiAzpL6Du -d4/awRtQug0JBSkSsUyYKP1qwPyb0UDNXFnEfUm/wQ+lOkJ8QQ0eJS4uLj0MI/uf -H0bs82KqpsqFqersUZgmbhq9zQ7rIkmy5sSZL2s8uoIJRnSzGabcuaGDbdUoYkO6 -G/PhHWFhh7LLGxRJAt7ZEMrXDNrGwxzyq0gnjRAXi1bLXdT2GWVMeCXLPb6lk3fO -oneX3rQkjqo73Mb4V9mpukLZenekStx2BytDwwIDAQABAoIBAHFYwWeEnniekqe6 -T/PHodm/4tGtcfRQOW/DvXY8iL7BBbtI783H2K41nrYdbcu/IuWfwXa5FHwoNFoG -t5a8z9rG9KAwNtzM/UKHuLFW5cCYn4HasKhzuC/mCy1pcGolEbkPkW7QlwxWFlGL -KEmP2GqAEndjRHOI7DAzI2NDsIYgjBARGCWLURcjohr8q5Z1EC9B8ClmzA94f7EZ -RZ61mN3oOZiJtulGRnmn70lIdcJ4sWMlJbrLtKsPK0rHAv8U5Yjs+TSsrz4lYTVa -5sdp9nhr5GpZ3W+JDEq0ZyeiJ5FxyR4krcIj8HVVDVavauW4vRu9CeqnDwDunPl6 -L14O/uECgYEA75RpcfM4bzULJpVbLNHZTkClZtWNyY77rDkfvrOjlsD06QMDBeQh -vFxiNxwO2JqSKoJf1vay8Hn7un9NSm5x8MgRfrMjhsG6MzavycrxESRFtq5Adkdk -3lQyn0WGYsPHFLVs/tx1GtdzCqU5SyBUkeLCqMNaARV1xmD4AjFcaasCgYEA2YzM -ZZ8Z4aAqkv6gJiZTN1gQxMO8nPiCwY8NefI/Mm1U+X6j4ZYRkqTcvdsJzFtnj+ab -rrguS1AOuyDMID2NKjQTrzJLBUhNYzbo7YeMsY2U+k9z0fvM3WGzX0YBvcxtnqXm -BLMKHjbF0YvzEbu0qD1dWj5CZ6e/+DXfK5QlZkkCgYAEAa9hwHeJJJHzKzxDG59O -t7YMajXc0Q9UagAl6EssEj4GR46dYptN0x2xXj7BUJRxMYz4w1dqvh9/lvFr9Tzi -kfX48HX/ou3CPX/jGAnAB6NC0tcxIzCEp1PRZhBBRpTlu8L+4CD1OfUqkGjM4NWJ -OwmWWO4AZqN5ldWP89Nf0QKBgQDUR1RHMNljVRNV/gmtUCZRUaiDJ3ALR17nmjwP -KzdJcG/DSDSHchTRn/cZdvt3ohVK0D5HXccmjAbjx9wG9aiibtBqWsvjaqrAzhq5 -dFPwCPQ+z3p3gpljx+rsY3ZdinXIoZ7yJPYRh2a90y6qthtRMxe9cBUB6iki/QY4 -EsXvqQKBgQCUokN2XeonTeJCIDKU7XKd5JNOuWFWCz/tsBu2lnMSr/2txiL3cCgt -BNJw+rbZ08hMMNeD871lsYKTrPigEXKpMlHlC8RodWK7XEGhTL4nHoZQ/PE8Zq71 -Q6+DM27CV0IU7/78rrWO0YdHii2pE72Fp05i/X16apjTSFi9InL6ZQ== +MIIEowIBAAKCAQEA5zoBqJMSCPSmyYkQovZqatOTmMcxwOWKOkSbz++5PQWGA2EO +bvzG+ZqeNdY9OCdIy3cmlxU0oAsdlzHdGOy/eNkyngAaRGp4FR+sez67rbK0MnWM +Edgx7Bl9v7pdHnA4YhDPOoqkmIO03+BQO+XsJKCJFCwZJ0hmw9QddGO+YziVP2TQ +kayV99nKlrUb53Fwe187EjAsuDooeYScgRLbODFtLSrigAVcKXdTWBAZ7vlQ4Y07 +K+LAC9KfPKCVM/gzF84jDjHoHj1+asltg54L+kPSSj++0xkHHozk9tyPwz46jmZK +h+8LOdvoPjAckZ6zHtOgHhuasViZ3qW7Uzs7XQIDAQABAoIBAAx5xL0jskVpbdZR +3uPsB7Hb2IrVtImD2QFr0jxV4ti4A5MLGYxDdzjgbsjY1lTBSdwwgZSFQGGiN+aA +ej1uCKaskV6VAtXOKMx6+QNtTxMAIVjXnscXsxnaBj7h/0Q1KdWgso2mDVttP8UU +hT+2GBeh0cOU3YaREXpfZ3dwKkWQHbtO/UYwVzu+XVFt8kApPoLMMHoXZfetP6Yp +7YSCuI6id44mwqkP7aY8iGhcUpVTkP3LD7z8nUp4LaG9my6T1Wev8x7hstb/NIsZ +DPiXAzfDUkHWqpMthnoWyOdghGc6JzKGFeJVHqrW4byJ4hNU3WvNIdvZ8tyIEpd1 +56uP/gECgYEA92oUhzHjvw87qfo6tPDai2I8AghXJoPGB6xYYhchlirYMGPx9fU/ +rcVEGbmSBDqXMg9eZUqiXB+E/hukCOrFZJt4kt656Nm/Xy68IDwSifmf5vcUde6q +j0pD6i0vwJFjYWBjjS7gRBK83pr/jHhy8aK1+79lZ0GfbQkLxF/2TxkCgYEA70Af +A387tHDmct7ZH0gAZx9QKYZhtS+WWVCIoZ81028DEeGri0By83KFkU0QZ9RfWKQi +RajBYkB35xJFv4fSX5s4+tcVaTVJKOn7V5YGmUIxrGY3IMuE77+h9SEHd8GY723q +9qgwTF5SQP3cGiVpGFB99M44CBuHbbypFh67iuUCgYEA3Zp6QI2C/AJc4mZqZt7E +IMwgC4IE7U5h9UV89H7banF9qfobIr5EBxUFZjU8f+Uqv3/cgMVUn0bsC94eEo6V +twM5//LWeaVvL4Xgos6rnEGl422zOd5HjohqRDms58JRTUrUYAR4gwB1gr0530uT +SLMAZTiNTusMLNFJZN6+8yECgYAulAY1sRSXmY9T98y/iU4CxZberrnhA2W697HR +/WQGSMuJNK0oDCEVAku8sQsrm64AXNwLQcJ8dV6iju0jT7cGQ/sA4tTZSbV3kK4N +LDkWp0tya+f5q4WzA1Ttm0OP7hHvMzAWW0Ij7A0JeCLcuEHQqQMMoQVJlspz89Hb +a5pJfQKBgFZb6XnLMTSCs/SQe38PQiawIQcA+zXmhG83xkEKspQGm2KqyJL+AdKQ +fXQKoKa/Ubyp7PKRJVZ8raX1/kvtFDQIQ+G3L/hps5rhZgDh5S2n0xd4zlbK/Sw6 +l3RjOUpHSe8oz+X3Jinl/Rwr39I9hrRAW2xj7vkFb84IE98mJu2X -----END RSA PRIVATE KEY----- diff --git a/compose/assets/etc/storm/webdav/README.md b/compose/assets/etc/storm/webdav/README.md index fe79a8f1..a45eefb2 100644 --- a/compose/assets/etc/storm/webdav/README.md +++ b/compose/assets/etc/storm/webdav/README.md @@ -11,22 +11,3 @@ service. Storage area configuration lives in the `sa.d` directory. For more information see the README.md file there. - -## VOMS map files configuration - -VOMS map files contains the list of VO members as obtained by running the -voms-admin list-users command. - -When VOMS mapfiles are enabled, users can authenticate to the StoRM webdav -service using the certificate in their browser and be granted VOMS attributes -if their subject is listed in one of the supported VOMS mapfile. - -For each supported VO, a file having the same name as the VO is put in the -voms-mapfiles directory. - -*Example*: to generate a VOMS mapfile for the `cms` VO, run the following -command - -```bash -voms-admin --host voms.cern.ch --vo cms list-users > cms -``` diff --git a/compose/assets/etc/storm/webdav/config/application-issuers.yml b/compose/assets/etc/storm/webdav/config/application-issuers.yml new file mode 100644 index 00000000..275750b9 --- /dev/null +++ b/compose/assets/etc/storm/webdav/config/application-issuers.yml @@ -0,0 +1,6 @@ +oauth: + issuers: + - name: egi-checkin + issuer: https://egi-checkin.example/ + - name: dev + issuer: https://iam-dev.cloud.cnaf.infn.it/ \ No newline at end of file diff --git a/compose/assets/etc/storm/webdav/config/application-policies.yml b/compose/assets/etc/storm/webdav/config/application-policies.yml new file mode 100644 index 00000000..0ef9a668 --- /dev/null +++ b/compose/assets/etc/storm/webdav/config/application-policies.yml @@ -0,0 +1,54 @@ +storm: + authz: + policies: + - sa: fga + actions: + - all + effect: permit + description: Grant read/write access to test.vo VOMS users + principals: + - type: vo + params: + vo: test.vo + - sa: fga + actions: + - list + - read + effect: permit + description: Grant read access to anyone to the public area + paths: + - /public/** + principals: + - type: anyone + - sa: fga + actions: + - all + effect: permit + description: Grant read/write to the "cms" token (default) group in the cms namespace + paths: + - /cms/** + principals: + - type: jwt-group + params: + iss: https://iam-dev.cloud.cnaf.infn.it/ + group: /cms + - sa: fga + actions: + - all + effect: permit + description: Grant read/write to the "data-manager" token (optional) group + principals: + - type: jwt-group + params: + iss: https://iam-dev.cloud.cnaf.infn.it/ + group: /data-manager + - sa: fga + actions: + - list + - read + effect: permit + description: Grant read access to tokens issued by iam-dev + principals: + - type: jwt-issuer + params: + iss: https://iam-dev.cloud.cnaf.infn.it/ diff --git a/robot/assets/fixtures/default/sa.d/wlcg.properties b/compose/assets/etc/storm/webdav/sa.d/fga.properties similarity index 61% rename from robot/assets/fixtures/default/sa.d/wlcg.properties rename to compose/assets/etc/storm/webdav/sa.d/fga.properties index 0d635e49..7da65f4d 100644 --- a/robot/assets/fixtures/default/sa.d/wlcg.properties +++ b/compose/assets/etc/storm/webdav/sa.d/fga.properties @@ -1,12 +1,11 @@ -name=wlcg -rootPath=/storage/wlcg +name=fga +rootPath=/storage/fga filesystemType=posixfs -accessPoints=/wlcg -orgs=https://wlcg.cloud.cnaf.infn.it/ +accessPoints=/fga authenticatedReadEnabled=false anonymousReadEnabled=false -voMapGrantsWritePermission=false -wlcgScopeAuthzEnabled=true -fineGrainedAuthzEnabled=true +orgs=https://iam-dev.cloud.cnaf.infn.it/,https://egi-checkin.example/ orgsGrantReadPermission=false orgsGrantWritePermission=false +fineGrainedAuthzEnabled=true +wlcgScopeAuthzEnabled=true diff --git a/compose/assets/etc/storm/webdav/sa.d/oauth_authz.properties b/compose/assets/etc/storm/webdav/sa.d/oauth-authz.properties similarity index 91% rename from compose/assets/etc/storm/webdav/sa.d/oauth_authz.properties rename to compose/assets/etc/storm/webdav/sa.d/oauth-authz.properties index 4985fffa..a09cd12f 100644 --- a/compose/assets/etc/storm/webdav/sa.d/oauth_authz.properties +++ b/compose/assets/etc/storm/webdav/sa.d/oauth-authz.properties @@ -18,7 +18,7 @@ name=oauth-authz rootPath=/storage/oauth-authz filesystemType=posixfs accessPoints=/oauth-authz -orgs=https://iam-test.indigo-datacloud.eu/ +orgs=https://iam-dev.cloud.cnaf.infn.it/,https://egi-checkin.example/ authenticatedReadEnabled=false anonymousReadEnabled=false voMapGrantsWritePermission=false diff --git a/compose/assets/etc/storm/webdav/sa.d/sa.properties.template b/compose/assets/etc/storm/webdav/sa.d/sa.properties.template deleted file mode 100644 index 64cc965c..00000000 --- a/compose/assets/etc/storm/webdav/sa.d/sa.properties.template +++ /dev/null @@ -1,29 +0,0 @@ -# This is an example of StoRM WebDAV storage area configuration - -# Name of the storage area -name=sa - -# Root path for the storage area. Files will be served from this path, which must exist and -# must be accessible from the user that runs the storm webdav service -rootPath=/tmp - -# Comma separated list of storage area access points. -accessPoints=/sa - -# Comma separated list of VOMS VOs supported in this storage area -vos=testers.eu-emi.eu - -# Enables read access to users authenticated with an X.509 certificate issued by -# a trusted CA (users without VOMS credentials). -# Defaults to false, which means that all users need to authenticate with a VOMS credential -# authenticatedReadEnabled=false - -# Enables read access to anonymous users. Defaults to false. -# anonymousReadEnabled=false - -# Enables VO map files for this storage area. Defaults to true. -# voMapEnabled=true - -# VO map normally grants read-only access to storage area files. To grant -# write access set this flag to true. Defaults to false. -# voMapGrantsWriteAccess=false diff --git a/compose/assets/etc/storm/webdav/sa.d/test_vo.properties b/compose/assets/etc/storm/webdav/sa.d/test.vo.properties similarity index 100% rename from compose/assets/etc/storm/webdav/sa.d/test_vo.properties rename to compose/assets/etc/storm/webdav/sa.d/test.vo.properties diff --git a/compose/assets/etc/storm/webdav/vo-mapfiles.d/README.md b/compose/assets/etc/storm/webdav/vo-mapfiles.d/README.md deleted file mode 100644 index 922f3c14..00000000 --- a/compose/assets/etc/storm/webdav/vo-mapfiles.d/README.md +++ /dev/null @@ -1,39 +0,0 @@ -## VO map files configuration -VO map files contains the list of the members of a VOMS-managed Virtual Organization (VO). - -## What are VO map files - -When VO map files are enabled, users can authenticate to the StoRM webdav -service using the certificate in their browser and be granted VOMS attributes -if their subject is listed in one of the supported VO mapfile. - -This mechanism is very similar to the traditional Gridmap file but is just used -to know whether a given user is registered as a member in a VOMS managed VO and -not to map his/her certificate subject to a local unix account. - -### How to enable VO map files - -VO map files support is disabled by default in StoRM WebDAV. - -Set `STORM_WEBDAV_VO_MAP_FILES_ENABLE=true`` in /etc/sysconfig/storm-webdav -to enable VO map file support. - -### How to generate VO map files - -VO map files are generated using the voms-admin list-users command. - -For each supported VO, a file named: - -.vomap - -is put in the voms-mapfiles.d directory. - -*Example*: to generate a VO mapfile for the `cms` VO, run the following -command - -```bash -voms-admin --vo cms list-users > /etc/storm/webdav/vo-mapfiles.d/cms.vomap -``` - -*N.B.:* Ensure that vo map files are readable by the user that runs the StORM -WebDAV service (by default, the `storm` user). diff --git a/compose/assets/nginx/nginx.conf b/compose/assets/nginx/nginx.conf index 5b77bc4f..e92b74b3 100644 --- a/compose/assets/nginx/nginx.conf +++ b/compose/assets/nginx/nginx.conf @@ -1,10 +1,4 @@ -user build; -worker_processes 1; - -env OPENSSL_ALLOW_PROXY_CERTS=1; -env X509_VOMS_DIR=/vomsdir; - -error_log /home/build/local/openresty/nginx/logs/error.log warn; +load_module modules/ngx_http_voms_module.so; events { worker_connections 1024; @@ -12,9 +6,11 @@ events { http { - include /home/build/local/openresty/nginx/conf/mime.types; + include mime.types; default_type application/octet-stream; + resolver 127.0.0.11 ipv6=off; + log_format storm '$time_iso8601 [$request_id] $remote_addr - $remote_user "$request" <$upstream_response_time> ' '$ssl_protocol/$ssl_cipher ' '"$ssl_client_s_dn" ' @@ -22,15 +18,14 @@ http { '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /home/build/local/openresty/nginx/logs/access.log storm; + access_log /var/log/nginx/access.log storm; sendfile on; #tcp_nopush on; keepalive_timeout 65; - #gzip on; - client_max_body_size 10000m; + include /etc/nginx/conf.d/*.conf; } diff --git a/compose/assets/nginx/srm.conf b/compose/assets/nginx/srm.conf index 6d58e8b0..69ed1160 100644 --- a/compose/assets/nginx/srm.conf +++ b/compose/assets/nginx/srm.conf @@ -1,27 +1,26 @@ server { - root /tmp/storage; - error_log /home/build/local/openresty/nginx/logs/error.log info; - access_log /home/build/local/openresty/nginx/logs/access.log storm; + root /storage; listen 443 ssl; - server_name storm-alias.example; + server_name storm-alias.test.example; - ssl on; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_certificate /certs/hostcert.pem; ssl_certificate_key /certs/hostkey.pem; - ssl_client_certificate /etc/pki/tls/certs/ca-bundle.crt; + ssl_client_certificate /etc/pki/ca-trust/extracted/pem/tls-ca-bundle-all.pem; ssl_verify_client optional; ssl_verify_depth 100; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; + client_max_body_size 10000m; + location / { autoindex on; - dav_methods PUT DELETE MKCOL; + dav_methods PUT DELETE MKCOL COPY; create_full_put_path on; dav_access group:rw all:r; } diff --git a/compose/assets/oidc-agent/dev-wlcg b/compose/assets/oidc-agent/dev-wlcg new file mode 100644 index 00000000..9d79eca1 --- /dev/null +++ b/compose/assets/oidc-agent/dev-wlcg @@ -0,0 +1,7 @@ +988 +jKnjHytEdw2+IC/vyCA8vFb4TOsCzU5I +V548LMRkzXbmeRhZrXwdkg== +24:16:16:32:1:2:67108864:2 +YAf68/yXEOBM53CwmPXXyR7f2ExOcFQ/gXvW+BKnKJ6S05SzAr3qrYmRBDi33t4IEgHgx83+OyF0W+p+fanl1veg0Wfegr++8BY/c6sToV+xKUMRYZe3dtjJTkcd1x0e7fwxQgsQn2AXb0pMp7AadVaiqW2xPOkR6EiYuHA1gcKc1lekshbMh1IbMS+BH7cmlOqJDlGbPbZpR70HaP3Pd/xx3Rbcyx0RRCLNUSmwdPcud/uLaBLGkjXmz6vq2uf50nyJRABOchWpSnZBf73reb5aMVwXOrnJLSSXvm8caMKRRum7IDEt4bmwa0KAx8xnblYd4AhRtEu9Dy+KVv0C60FxQqV43kOSvjvR4V6QOwcIiCv0fmAavY1BTO3yxgcHsKEx75psbjmsMvjmqTrgoRW+74vv01vgtjHyOsioGTpTl6RhECvy1RCcOt9D9SWCmqiHtSPsrB/6YN/ba2NLOqQotsEwPJnZlgMaOeBQeUb2XYNCV5pCDKige3vPEv69EUhi2ArWNxEryMK7RcGRK4mSNTmb2fAj5SuWsgrRucbzEsjeypIWhaSmdgwbmElAaDY5b0kEOE6jzR1tYIGQ/RfteHJE4n217C9XqN1nnLdIgu+wrxR0cHFgAd8T6qC7hD3TuTRzFZ9p/H2k8DSLZl4qrj+AgVOCVqKSXma0gakpLL0+MdtUxHCaf7G0D4EWmz0c3qDHiIEaJOkeHJ4kJt4sGuqd3zFW9bTAu4sKH9FqXAKi/oKE5jz/joEGKQw66+LWdLFyqSJcUHAG0DWUUKxzOmC1O0GuD3KkfcsPvbjNjNeySfn+e8Y/puSUyyEbvWztHi83MyJ0jF3da9dE9dpfakfezY0RlN8HplIPNPfSdxompoTkzv76j/rKE+FAkVZyQaASYaruaWjmBxDU5HxLbHUrvhhJXN3BG9nQwqE49NmDfghAsVaZmXrft3wgnvT/Qtb9ZjJY8CZ6EJbMmnuZm3V9u4q4o78LLLwyfH83npAfBiGx0KAKLosDV7sGahIXh77hrnOstLU2EoS8EeypCNnp8miKNW0mXYgru9OfFQQAS1osQc/Eqtf/WzGONf2/WsC7N69QWhgxxbMYsEQGpgpCp5pr96FNNciB0U4gqFuVfi2gGMoXahHVCGWYaIiG9i1CTZ6BJ6qZOu1uZLIiSRovS2dK2WVpvJfm1xdAZanMkFKvW4Qzk5siLLGjLbkWlitWlvwEQhemLdeP400MwpFZIWFsVKV48vOVcu5O2iw5xr+q4K+N8Zr8eFXFVVGTAFN2Ir+doZiinnezUDE3zRntEbAzWFxbIQ== +Xlhm5mu9d9cbcXxWzn83HGEx8HjT/535x22xWkP1o70= +Generated using version: 4.5.1 \ No newline at end of file diff --git a/compose/assets/oidc-agent/issuer.config b/compose/assets/oidc-agent/issuer.config new file mode 100644 index 00000000..c935cc99 --- /dev/null +++ b/compose/assets/oidc-agent/issuer.config @@ -0,0 +1 @@ +https://iam-dev.cloud.cnaf.infn.it/ dev-wlcg \ No newline at end of file diff --git a/compose/assets/scripts/ci-run-testsuite.sh b/compose/assets/scripts/ci-run-testsuite.sh new file mode 100755 index 00000000..e79799ff --- /dev/null +++ b/compose/assets/scripts/ci-run-testsuite.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -ex + +OIDC_AGENT_ALIAS=${OIDC_AGENT_ALIAS:-dev-wlcg} + +eval $(oidc-agent --no-autoload) +oidc-add --pw-cmd='echo ${OIDC_AGENT_SECRET}' ${OIDC_AGENT_ALIAS} +export IAM_ACCESS_TOKEN=$(oidc-token -s openid ${OIDC_AGENT_ALIAS}) + +/scripts/init-usercerts.sh +echo "pass123" | voms-proxy-init --cert /tmp/usercerts/test0.p12 -voms test.vo --pwstdin + +cp -r /code/robot . + +pushd robot + +sh run-testsuite.sh diff --git a/compose/assets/scripts/init-certs.sh b/compose/assets/scripts/init-certs.sh deleted file mode 100755 index 63f5451c..00000000 --- a/compose/assets/scripts/init-certs.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -ex - -CERT_DIR=${CERT_DIR:-/certs} - -cp ${CERT_DIR}/* /etc/grid-security/storm-webdav/ -chown -R storm:storm /etc/grid-security/storm-webdav diff --git a/compose/assets/scripts/init-sa-config.sh b/compose/assets/scripts/init-sa-config.sh deleted file mode 100755 index 876bac16..00000000 --- a/compose/assets/scripts/init-sa-config.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -set -ex - -SA_CONFIG_DIR=${SA_CONFIG_DIR:-/sa.d} -VOMAP_CONFIG_DIR=${VOMAP_CONFIG_DIR:-/vo-mapfiles.d} - -cp ${SA_CONFIG_DIR}/* /etc/storm/webdav/sa.d -chown -R storm:storm /etc/storm/webdav/sa.d - -cp ${VOMAP_CONFIG_DIR}/* /etc/storm/webdav/vo-mapfiles.d -chown -R storm:storm /etc/storm/webdav/vo-mapfiles.d diff --git a/compose/assets/scripts/init-storage.sh b/compose/assets/scripts/init-storage.sh index 03431990..a0c63ee7 100755 --- a/compose/assets/scripts/init-storage.sh +++ b/compose/assets/scripts/init-storage.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -ex STORAGE_DIR=${STORAGE_DIR:-/storage} diff --git a/compose/assets/scripts/init-usercerts.sh b/compose/assets/scripts/init-usercerts.sh index 00fc19bb..d56dce68 100755 --- a/compose/assets/scripts/init-usercerts.sh +++ b/compose/assets/scripts/init-usercerts.sh @@ -1,8 +1,7 @@ -#!/bin/bash +#!/usr/bin/env bash set -ex USER_CERTS_DIR=${USER_CERTS_DIR:-/usercerts} mkdir -p /tmp/usercerts cp ${USER_CERTS_DIR}/* /tmp/usercerts -chown -R storm:storm /tmp/usercerts chmod 600 /tmp/usercerts/* diff --git a/compose/assets/scripts/run-service.sh b/compose/assets/scripts/run-service.sh deleted file mode 100755 index 944587b6..00000000 --- a/compose/assets/scripts/run-service.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -set -ex - -JARDIR=/usr/share/java/storm-webdav -JVM_OPTS=${STORM_WEBDAV_JVM_OPTS:-} - -if [ -n "$ENABLE_JREBEL" ]; then - JVM_OPTS="-javaagent:/opt/jrebel/jrebel.jar -Drebel.stats=false -Drebel.usage_reporting=false -Drebel.struts2_plugin=true -Drebel.tiles2_plugin=true -Drebel.license=/home/storm/.jrebel/jrebel.lic $JVM_OPTS" - - mkdir -p /home/storm - cp -r /mnt/.jrebel /home/storm - chown -R storm.storm /home/storm - chmod 755 /home/storm/.jrebel - chmod 644 /home/storm/.jrebel/* -fi - -if [ -z "$DEBUG_PORT" ]; then - DEBUG_PORT=1044 -fi - -if [ -z "$DEBUG_SUSPEND" ]; then - DEBUG_SUSPEND="n" -fi - -if [ ! -z "$DEBUG" ]; then - JVM_OPTS="-Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=$DEBUG_PORT,suspend=$DEBUG_SUSPEND $JVM_OPTS" -fi - -if [ -n "$ENABLE_JMX" ]; then - JVM_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=6002 -Dcom.sun.management.jmxremote.rmi.port=6002 -Djava.rmi.server.hostname=dev.local.io -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false $JVM_OPTS" -fi - -if [ -z "${DONT_START_SERVICE}" ]; then - su storm -s /bin/bash -c "java $JVM_OPTS -jar $JARDIR/storm-webdav-server.jar" -fi diff --git a/compose/assets/scripts/run.sh b/compose/assets/scripts/run.sh deleted file mode 100755 index a8934982..00000000 --- a/compose/assets/scripts/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -set -ex - -/scripts/unpack-tarball.sh -/scripts/run-service.sh diff --git a/compose/assets/scripts/setup.sh b/compose/assets/scripts/setup.sh deleted file mode 100755 index e1ca14ef..00000000 --- a/compose/assets/scripts/setup.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -ex - -/scripts/init-certs.sh -/scripts/init-sa-config.sh -/scripts/init-storage.sh diff --git a/compose/assets/scripts/unpack-tarball.sh b/compose/assets/scripts/unpack-tarball.sh deleted file mode 100755 index cbb73855..00000000 --- a/compose/assets/scripts/unpack-tarball.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -ex - -JARDIR=/usr/share/java/storm-webdav - -if [ -z "${DONT_UNPACK_TARBALL}" ]; then - TARFILE="target/storm-webdav-server.tar.gz" - tar -C / --owner=storm --group=storm -xvzf /code/$TARFILE -fi diff --git a/compose/assets/scripts/wait-and-run-testsuite.sh b/compose/assets/scripts/wait-and-run-testsuite.sh deleted file mode 100755 index 8c6c14b2..00000000 --- a/compose/assets/scripts/wait-and-run-testsuite.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -set -ex - -export X509_USER_PROXY=${X509_USER_PROXY:-/tmp/x509up_u$(id -u)} -DAV_HOST=${DAV_HOST:-storm.example} -WAIT_TIMEOUT=${WAIT_TIMEOUT:-30} - -/scripts/init-usercerts.sh -echo "pass" | voms-proxy-init --cert /tmp/usercerts/test0.p12 -voms test.vo --pwstdin - -/scripts/wait-for-it.sh ${DAV_HOST}:8085 --timeout=${WAIT_TIMEOUT} - -rsync -avhu --exclude='.git/' /code/robot . - -pushd robot - - -sh run-testsuite.sh diff --git a/compose/assets/scripts/wait-for-it.sh b/compose/assets/scripts/wait-for-it.sh deleted file mode 100755 index bbe40432..00000000 --- a/compose/assets/scripts/wait-for-it.sh +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env bash -# Use this script to test if a given TCP host/port are available - -cmdname=$(basename $0) - -echoerr() { if [[ $QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } - -usage() -{ - cat << USAGE >&2 -Usage: - $cmdname host:port [-s] [-t timeout] [-- command args] - -h HOST | --host=HOST Host or IP under test - -p PORT | --port=PORT TCP port under test - Alternatively, you specify the host and port as host:port - -s | --strict Only execute subcommand if the test succeeds - -q | --quiet Don't output any status messages - -t TIMEOUT | --timeout=TIMEOUT - Timeout in seconds, zero for no timeout - -- COMMAND ARGS Execute command with args after the test finishes -USAGE - exit 1 -} - -wait_for() -{ - if [[ $TIMEOUT -gt 0 ]]; then - echoerr "$cmdname: waiting $TIMEOUT seconds for $HOST:$PORT" - else - echoerr "$cmdname: waiting for $HOST:$PORT without a timeout" - fi - start_ts=$(date +%s) - while : - do - if [[ $ISBUSY -eq 1 ]]; then - nc -z $HOST $PORT - result=$? - else - (echo > /dev/tcp/$HOST/$PORT) >/dev/null 2>&1 - result=$? - fi - if [[ $result -eq 0 ]]; then - end_ts=$(date +%s) - echoerr "$cmdname: $HOST:$PORT is available after $((end_ts - start_ts)) seconds" - break - fi - sleep 1 - done - return $result -} - -wait_for_wrapper() -{ - # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 - if [[ $QUIET -eq 1 ]]; then - timeout $BUSYTIMEFLAG $TIMEOUT $0 --quiet --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & - else - timeout $BUSYTIMEFLAG $TIMEOUT $0 --child --host=$HOST --port=$PORT --timeout=$TIMEOUT & - fi - PID=$! - trap "kill -INT -$PID" INT - wait $PID - RESULT=$? - if [[ $RESULT -ne 0 ]]; then - echoerr "$cmdname: timeout occurred after waiting $TIMEOUT seconds for $HOST:$PORT" - fi - return $RESULT -} - -# process arguments -while [[ $# -gt 0 ]] -do - case "$1" in - *:* ) - hostport=(${1//:/ }) - HOST=${hostport[0]} - PORT=${hostport[1]} - shift 1 - ;; - --child) - CHILD=1 - shift 1 - ;; - -q | --quiet) - QUIET=1 - shift 1 - ;; - -s | --strict) - STRICT=1 - shift 1 - ;; - -h) - HOST="$2" - if [[ $HOST == "" ]]; then break; fi - shift 2 - ;; - --host=*) - HOST="${1#*=}" - shift 1 - ;; - -p) - PORT="$2" - if [[ $PORT == "" ]]; then break; fi - shift 2 - ;; - --port=*) - PORT="${1#*=}" - shift 1 - ;; - -t) - TIMEOUT="$2" - if [[ $TIMEOUT == "" ]]; then break; fi - shift 2 - ;; - --timeout=*) - TIMEOUT="${1#*=}" - shift 1 - ;; - --) - shift - CLI=("$@") - break - ;; - --help) - usage - ;; - *) - echoerr "Unknown argument: $1" - usage - ;; - esac -done - -if [[ "$HOST" == "" || "$PORT" == "" ]]; then - echoerr "Error: you need to provide a host and port to test." - usage -fi - -TIMEOUT=${TIMEOUT:-15} -STRICT=${STRICT:-0} -CHILD=${CHILD:-0} -QUIET=${QUIET:-0} - -# check to see if timeout is from busybox? -# check to see if timeout is from busybox? -TIMEOUT_PATH=$(realpath $(which timeout)) -if [[ $TIMEOUT_PATH =~ "busybox" ]]; then - ISBUSY=1 - BUSYTIMEFLAG="-t" -else - ISBUSY=0 - BUSYTIMEFLAG="" -fi - -if [[ $CHILD -gt 0 ]]; then - wait_for - RESULT=$? - exit $RESULT -else - if [[ $TIMEOUT -gt 0 ]]; then - wait_for_wrapper - RESULT=$? - else - wait_for - RESULT=$? - fi -fi - -if [[ $CLI != "" ]]; then - if [[ $RESULT -ne 0 && $STRICT -eq 1 ]]; then - echoerr "$cmdname: strict mode, refusing to execute subprocess" - exit $RESULT - fi - exec "${CLI[@]}" -else - exit $RESULT -fi diff --git a/compose/assets/usercerts/test0.p12 b/compose/assets/usercerts/test0.p12 index 1c97d372..3ce35f01 100644 Binary files a/compose/assets/usercerts/test0.p12 and b/compose/assets/usercerts/test0.p12 differ diff --git a/compose/assets/vomsdir/test.vo.2/vgrid02.cnaf.infn.it.lsc b/compose/assets/vomsdir/test.vo.2/vgrid02.cnaf.infn.it.lsc deleted file mode 100644 index 11316e3d..00000000 --- a/compose/assets/vomsdir/test.vo.2/vgrid02.cnaf.infn.it.lsc +++ /dev/null @@ -1,2 +0,0 @@ -/C=IT/O=INFN/OU=Host/L=CNAF/CN=vgrid02.cnaf.infn.it -/C=IT/O=INFN/CN=INFN Certification Authority diff --git a/compose/assets/vomsdir/test.vo.2/voms.example.lsc b/compose/assets/vomsdir/test.vo.2/voms.example.lsc deleted file mode 100644 index 0121b810..00000000 --- a/compose/assets/vomsdir/test.vo.2/voms.example.lsc +++ /dev/null @@ -1,2 +0,0 @@ -/C=IT/O=IGI/CN=voms.example -/C=IT/O=IGI/CN=Test CA diff --git a/compose/assets/vomsdir/test.vo/vgrid02.cnaf.infn.it.lsc b/compose/assets/vomsdir/test.vo/vgrid02.cnaf.infn.it.lsc deleted file mode 100644 index 11316e3d..00000000 --- a/compose/assets/vomsdir/test.vo/vgrid02.cnaf.infn.it.lsc +++ /dev/null @@ -1,2 +0,0 @@ -/C=IT/O=INFN/OU=Host/L=CNAF/CN=vgrid02.cnaf.infn.it -/C=IT/O=INFN/CN=INFN Certification Authority diff --git a/compose/assets/vomsdir/test.vo/voms-dev.cloud.cnaf.infn.it.lsc b/compose/assets/vomsdir/test.vo/voms-dev.cloud.cnaf.infn.it.lsc new file mode 100644 index 00000000..f6319aaf --- /dev/null +++ b/compose/assets/vomsdir/test.vo/voms-dev.cloud.cnaf.infn.it.lsc @@ -0,0 +1,2 @@ +/DC=org/DC=terena/DC=tcs/C=IT/ST=Roma/O=Istituto Nazionale di Fisica Nucleare/CN=voms-dev.cloud.cnaf.infn.it +/C=NL/O=GEANT Vereniging/CN=GEANT eScience SSL CA 4 diff --git a/compose/assets/vomsdir/test.vo/voms.example.lsc b/compose/assets/vomsdir/test.vo/voms.example.lsc deleted file mode 100644 index 0121b810..00000000 --- a/compose/assets/vomsdir/test.vo/voms.example.lsc +++ /dev/null @@ -1,2 +0,0 @@ -/C=IT/O=IGI/CN=voms.example -/C=IT/O=IGI/CN=Test CA diff --git a/compose/assets/vomses/test.vo b/compose/assets/vomses/test.vo index 8b7420f9..2c2b10e8 100644 --- a/compose/assets/vomses/test.vo +++ b/compose/assets/vomses/test.vo @@ -1 +1 @@ -"test.vo" "vgrid02.cnaf.infn.it" "15000" "/C=IT/O=INFN/OU=Host/L=CNAF/CN=vgrid02.cnaf.infn.it" "test.vo" +"test.vo" "voms-dev.cloud.cnaf.infn.it" "15004" "/DC=org/DC=terena/DC=tcs/C=IT/ST=Roma/O=Istituto Nazionale di Fisica Nucleare/CN=voms-dev.cloud.cnaf.infn.it" "test.vo" \ No newline at end of file diff --git a/compose/docker-compose.yml b/compose/docker-compose.yml index 47f5e450..1b73e387 100644 --- a/compose/docker-compose.yml +++ b/compose/docker-compose.yml @@ -1,90 +1,92 @@ -version: "3" - volumes: trustanchors: cabundle: storage: storage2: - hostcert: - saconfig: - mapfilesconfig: + services: trust: - image: indigoiam/trustanchors:latest + image: ${TRUST_IMAGE}:${TRUST_IMAGE_TAG} volumes: - trustanchors:/etc/grid-security/certificates - cabundle:/etc/pki environment: FORCE_TRUST_ANCHORS_UPDATE: 1 - webdav-setup: - image: italiangrid/storm-webdav:latest + storage-setup: + image: ${WEBDAV_IMAGE}:${WEBDAV_IMAGE_TAG} environment: TZ: UTC volumes: - - ./assets/certs/:/certs:ro - ./assets/scripts:/scripts:ro - - ./assets/etc/storm/webdav/sa.d/:/sa.d:ro - - ./assets/etc/storm/webdav/vo-mapfiles.d/:/vo-mapfiles.d:ro - - hostcert:/etc/grid-security/storm-webdav - - saconfig:/etc/storm/webdav/sa.d - - mapfilesconfig:/etc/storm/webdav/vo-mapfiles.d + - ./assets/etc/storm/webdav/sa.d/:/etc/storm/webdav/sa.d/:ro - storage:/storage - + user: root entrypoint: - - /scripts/setup.sh + - /scripts/init-storage.sh webdav: - image: italiangrid/storm-webdav:latest - dns_search: example + build: + context: ../ + dockerfile: Dockerfile ports: - - "9443:8443" - - "9444:1044" + - "8443:8443" + - "8085:8085" depends_on: - - trust - - webdav-setup + trust: + condition: service_completed_successfully + storage-setup: + condition: service_completed_successfully environment: TZ: UTC STORM_WEBDAV_REQUIRE_CLIENT_CERT: "false" - STORM_WEBDAV_HOSTNAME_0: storm.example - STORM_WEBDAV_USE_CONSCRYPT: "true" - STORM_WEBDAV_JVM_OPTS: -Dspring.profiles.active=test - #DEBUG: y - #DEBUG_SUSPEND: y + STORM_WEBDAV_HOSTNAME_0: storm.test.example + STORM_WEBDAV_AUTHZ_SERVER_ENABLE: true + STORM_WEBDAV_AUTHZ_SERVER_ISSUER: https://storm.test.example:8443 + STORM_WEBDAV_JVM_OPTS: -Dspring.profiles.active=policies,issuers volumes: - storage:/storage - cabundle:/etc/pki - trustanchors:/etc/grid-security/certificates - - hostcert:/etc/grid-security/storm-webdav - - saconfig:/etc/storm/webdav/sa.d - - mapfilesconfig:/etc/storm/webdav/vo-mapfiles.d + - ./assets/certs/:/etc/grid-security/storm-webdav/:ro - ./assets/vomsdir:/etc/grid-security/vomsdir:ro - ./assets/scripts:/scripts:ro - - ${HOME}/git/storm-webdav:/code:ro - - entrypoint: - - /scripts/run.sh + - ./assets/etc/storm/webdav/sa.d/:/etc/storm/webdav/sa.d/:ro + - ./assets/etc/storm/webdav/config/application-policies.yml:/app/application-policies.yml:ro + - ./assets/etc/storm/webdav/config/application-issuers.yml:/app/application-issuers.yml:ro + - ../:/code:ro + + healthcheck: + test: curl https://storm.test.example:8443/actuator/health --fail || exit 1 + interval: 5s + timeout: 3s + retries: 10 + start_period: 5s networks: default: aliases: - - storm.example + - storm.test.example ts: - image: italiangrid/storm-webdav-ts:latest + image: ${TS_IMAGE}:${TS_IMAGE_TAG} environment: + - TZ=UTC + - ROBOT_ARGS + - OIDC_AGENT_ALIAS + - OIDC_AGENT_SECRET - IAM_ACCESS_TOKEN - - TZ=UTC - - DAV_HOST=storm.example - - REMOTE_DAV_HOST=storm-alias.example depends_on: - - webdav + webdav: + condition: service_healthy + trust: + condition: service_completed_successfully volumes: - storage:/storage @@ -94,41 +96,38 @@ services: - ./assets/vomsdir:/etc/grid-security/vomsdir - ./assets/scripts:/scripts:ro - ./assets/usercerts/:/usercerts:ro - - ${HOME}/git/storm-webdav:/code:ro + - ./assets/oidc-agent:/home/test/.config/oidc-agent + - ../:/code:ro entrypoint: sleep infinity - - #entrypoint: - # - /scripts/wait-and-run-testsuite.sh networks: default: nginx: - image: storm2/ngx-voms:latest - dns_search: example - + image: ${NGINX_IMAGE}:${NGINX_IMAGE_TAG} ports: - - "9553:443" + - "443:443" depends_on: - - trust - - webdav-setup + webdav: + condition: service_started + trust: + condition: service_completed_successfully environment: TZ: UTC - X509_VOMS_DIR: /vomsdir volumes: - cabundle:/etc/pki - trustanchors:/etc/grid-security/certificates - ./assets/certs:/certs - ./assets/vomsdir:/vomsdir - - ./assets/nginx/nginx.conf:/home/storm/local/openresty/nginx/conf/nginx.conf:ro + - ./assets/nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./assets/nginx/srm.conf:/etc/nginx/conf.d/srm.conf:ro - - storage2:/storage + - storage2:/storage/oauth-authz networks: default: aliases: - - storm-alias.example + - storm-alias.test.example diff --git a/doc/tpc.md b/doc/tpc.md index d93e0274..da193ad3 100644 --- a/doc/tpc.md +++ b/doc/tpc.md @@ -56,3 +56,17 @@ STORM_WEBDAV_AUTHZ_SERVER_MAX_TOKEN_LIFETIME_SEC="43200" STORM_WEBDAV_REQUIRE_CLIENT_CERT="false" ``` For other configuration options, see the /etc/sysconfig/storm-webdav file. + +## SciTags + +StoRM WebDAV supports the `SciTag` header. +To correctly mark the network packets and/or network flows, you need to install [flowd](https://github.com/scitags/flowd) and configure it to use the `np_api` plugin. + +Example flowd configuration (`/etc/flowd/flowd.cfg`): + +``` +PLUGIN='np_api' +BACKEND='udp_firefly' +FLOW_MAP_API='https://www.scitags.org/api.json' +IP_DISCOVERY_ENABLED=True +``` diff --git a/docker/testsuite/Dockerfile b/docker/testsuite/Dockerfile deleted file mode 100644 index 1c07aa03..00000000 --- a/docker/testsuite/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM italiangrid/storm-webdav:latest - -RUN mkdir /home/testsuite && chown storm:storm /home/testsuite - -USER storm - -WORKDIR /home/testsuite diff --git a/docker/testsuite/build-image.sh b/docker/testsuite/build-image.sh deleted file mode 100755 index 35bc82cb..00000000 --- a/docker/testsuite/build-image.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -docker build -t italiangrid/storm-webdav-ts . diff --git a/docker/webdav-centos7/Dockerfile b/docker/webdav-centos7/Dockerfile index 7699aa33..097ad79e 100644 --- a/docker/webdav-centos7/Dockerfile +++ b/docker/webdav-centos7/Dockerfile @@ -12,8 +12,9 @@ RUN \ yum install -y http://repository.egi.eu/sw/production/umd/4/centos7/x86_64/updates/umd-release-4.1.3-1.el7.centos.noarch.rpm && \ adduser --uid ${STORM_USER_UID} storm && \ usermod -a -G wheel storm && \ - yum-config-manager --add-repo https://repo.cloud.cnaf.infn.it/repository/storm/nightly/storm-nightly-centos7.repo && \ + yum-config-manager --add-repo https://repo.cloud.cnaf.infn.it/repository/storm/storm-nightly-centos7.repo && \ yum install -y storm-webdav voms-clients-java jq && \ + yum install -y java-11-openjdk-devel && yes 1 | update-alternatives --config java && \ yum clean all ADD ./run.sh / diff --git a/etc/storm-webdav/logback-access.xml b/etc/storm-webdav/logback-access.xml index a5f130c0..629060cc 100644 --- a/etc/storm-webdav/logback-access.xml +++ b/etc/storm-webdav/logback-access.xml @@ -11,7 +11,7 @@ - %a %localPort "%reqAttribute{storm.remoteUser}" %date{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX", UTC} "%reqAttribute{storm.requestId}" "%m %U %H" %s %b %D + %replace(%a){'^$','-'} %localPort "%reqAttribute{storm.remoteUser}" %date{"yyyy-MM-dd'T'HH:mm:ss.SSSXXX", UTC} "%reqAttribute{storm.requestId}" "%m %U %H" %s %b %D diff --git a/etc/systemd/system/storm-webdav.service.d/storm-webdav.conf b/etc/systemd/system/storm-webdav.service.d/storm-webdav.conf index f173bd26..e91a3069 100644 --- a/etc/systemd/system/storm-webdav.service.d/storm-webdav.conf +++ b/etc/systemd/system/storm-webdav.service.d/storm-webdav.conf @@ -127,3 +127,40 @@ Environment="STORM_WEBDAV_TPC_MAX_CONNECTIONS_PER_ROUTE=25" # Source file for the tape REST API well-known endpoint # Default: '/etc/storm/webdav/wlcg-tape-rest-api.json' # Environment="STORM_WEBDAV_TAPE_WELLKNOWN_SOURCE=/etc/storm/webdav/wlcg-tape-rest-api.json" + +# Buffer size for both internal and third-party copy requests. +# This adds more efficiency than to write the whole data. Valid values are numbers >= 4096. +# Default: 1048576 +# Environment="STORM_WEBDAV_BUFFER_FILE_BUFFER_SIZE_BYTES=1048576" + +# Enable checksum filter which adds checksum as an header following RFC 3230. +# Default: true +# Environment="STORM_WEBDAV_CHECKSUM_FILTER_ENABLED=true" + +# Enable Macaroon filter to process Macaroon tokens. Requires authz server enabled. +# Default: true +# Environment="STORM_WEBDAV_MACAROON_FILTER_ENABLED=true" + +# TLS protocol for non-TPC requests +# Default: TLS +# Environment="STORM_WEBDAV_TLS_PROTOCOL=TLS" + +# VOMS Trust Store directory +# Default: /etc/grid-security/vomsdir +# Environment="STORM_WEBDAV_VOMS_TRUST_STORE_DIR=/etc/grid-security/vomsdir" + +# VOMS Trust Store refresh interval +# Default: 43200 +# Environment="STORM_WEBDAV_VOMS_TRUST_STORE_REFRESH_INTERVAL_SEC=43200" + +# Enable caching for VOMS certificate validation +# Default: true +# Environment="STORM_WEBDAV_VOMS_CACHE_ENABLE=true" + +# Cache entries lifetime, used if caching for VOMS certificate validation is enabled +# Default: 300 +# Environment="STORM_WEBDAV_VOMS_CACHE_ENTRY_LIFETIME_SEC=300" + +# Enable SciTags support +# Default: false +# Environment="STORM_WEBDAV_SCITAG_ENABLED=false" diff --git a/maven/cnaf-mirror-settings.xml b/maven/cnaf-mirror-settings.xml deleted file mode 100644 index 3280e381..00000000 --- a/maven/cnaf-mirror-settings.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - false - - - nexus - CNAF maven mirror - https://repo.cloud.cnaf.infn.it/repository/maven-public - * - - - - - nexus - - - central - http://central - true - true - - - - - - nexus - - diff --git a/pom.xml b/pom.xml index cadfe278..71a7e51d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.italiangrid storm-webdav-server - 1.4.2 + 1.5.0 jar storm-webdav-server @@ -12,7 +12,7 @@ org.springframework.boot spring-boot-starter-parent - 2.7.10 + 2.7.18 @@ -20,8 +20,7 @@ UTF-8 UTF-8 - 3.6.0 - 11 + 3.8.0 2.4 2.4 @@ -32,15 +31,15 @@ 11 - 2.7.10 + 2.7.18 italiangrid_storm-webdav italiangrid https://sonarcloud.io - 0.4.6.v20220506 - 2.7.1.7 + 3.3.3 + 2.8.0.3 2.3 1.2 @@ -50,14 +49,13 @@ 4.2.2 4.2.1 - 31.1-jre + 32.0.0-jre 1.0.5.1 2.3.3.RELEASE 6.0.2 5.5.1 - 1.72 @@ -65,16 +63,6 @@ ${project.name} - - - org.apache.maven.plugins - maven-compiler-plugin - - 11 - 11 - - - org.springframework.boot spring-boot-maven-plugin @@ -99,7 +87,6 @@ src/assembly/tarball.xml - storm-webdav @@ -107,6 +94,9 @@ single + + false + @@ -135,7 +125,6 @@ src/main/resources/static/** src/main/resources/META-INF/spring.factories maven/** - cnaf-mirror-settings.xml sonar-project.properties true @@ -190,13 +179,6 @@ org.springframework.boot spring-boot-starter-actuator - - - - org.apache.logging.log4j - log4j-api - - @@ -256,6 +238,12 @@ org.springframework.boot spring-boot-starter-test test + + + com.vaadin.external.google + android-json + + @@ -326,11 +314,6 @@ metrics-core - - io.dropwizard.metrics - metrics-jvm - - io.dropwizard.metrics metrics-jetty9 @@ -348,45 +331,30 @@ - org.italiangrid - jetty-utils - ${jetty-utils.version} - - - javax.activation - activation - - - javax.mail - mail - - - org.eclipse.jetty.aggregate - jetty-all - - - ch.qos.logback - logback-core - - - ch.qos.logback - logback-classic - - + org.eclipse.jetty.http2 + http2-server + + + + org.eclipse.jetty + jetty-alpn-conscrypt-server - org.bouncycastle - bcpkix-jdk18on - ${bouncycastle.version} + org.slf4j + slf4j-api - org.bouncycastle - bcprov-jdk18on - ${bouncycastle.version} + org.slf4j + log4j-over-slf4j + + org.italiangrid + voms-api-java + ${voms-api-java.version} + ch.qos.logback diff --git a/robot/.gitignore b/robot/.gitignore deleted file mode 100644 index 21c3e1f4..00000000 --- a/robot/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -log.html -output.xml -report.html diff --git a/robot/README.md b/robot/README.md index 3a5bb830..d805c9dd 100644 --- a/robot/README.md +++ b/robot/README.md @@ -1 +1,95 @@ # StoRM WebDAV robot testsuite + +## Run test suite + +The [compose](../compose/README.md) folder already includes all services necessary to run the testsuite. + +Start all services with + +``` +cd compose +docker-compose up -d +``` + +In case of _Error response from daemon: failed to create symlink_ error, please run the `trust` container first. + +Enter into the testsuite container with + +``` +docker-compose exec ts bash +``` + +To perform token based authorization, the testsuite requires a valid oidc-agent client +registered in the [IAM DEV](https://iam-dev.cloud.cnaf.infn.it). + +Please set the following environment variable to allow the token credential setup + +``` +export OIDC_AGENT_ALIAS= +export OIDC_AGENT_SECRET= +``` + +Set the minimum arguments required by the testsuite to run against the two StoRM WebDAV +servers deployed into the compose: + +``` +export ROBOT_ARGS="--variable dav.host:storm.test.example --variable remote.dav.host:storm-alias.test.example --variable remote.davs.port:443" +``` + +Now you can run the test suite with + +``` +/scripts/ci-run-testsuite.sh +``` + +The default path for the test suite report is `/home/test/robot/reports`; +in case you want to copy it locally, run + +``` +docker cp storm-webdav-ts-1:/home/test/robot/reports . +``` + +### Testsuite parameters + +| Parameter name | Description | Default value | +| -------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| `dav.host` | Hostname of the WebDAV server considered as running locally | localhost | +| `dav.port` | Schema of the WebDAV server considered as running locally | 8085 | +| `davs.port` | Schema of the WebDAV server considered as running locally with HTTPS | 8443 | +| `remote.dav.host` | Hostname of the WebDAV server considered as running remotely | localhost | +| `remote.dav.port` | Schema of the WebDAV server considered as running remotely | 8085 | +| `remote.davs.port` | Schema of the WebDAV server considered as running remotely with HTTPS | 8443 | +| `token.endpoint` | WebDAV endpoint for the locally issued tokens | https://localhost:8443/oauth/token | +| `cred.oauth.env_var_name` | Environment variable for an OAuth access token | IAM_ACCESS_TOKEN | +| `cred.voms.use_os` | Use `/tmp/x509up_u` as proxy path | True | +| `oidc-agent.alias` | Alias for the oidc-agent client | dev-wlcg | +| `oauth.group.claim` | Claim for the token group | wlcg.groups | +| `oauth.optional.group.claim` | Claim for the optional token group. In IAM, optional groups appears in the token only if explicitly requested | wlcg.groups:/data-manager | + +For other parameters, see the [variables file](./test/variables.robot). + + +### Enable custom token issuers + +In order for authorization tests being executed with custom token issuers, one needs to modify +the StoRM WebDAV configuration as follow: + +* append the custom token issuer among the `orgs` comma separated list, in the [fga.property](../compose/assets/etc/storm/webdav/sa.d/fga.properties) and [oauth-authz.properties](../compose/assets/etc/storm/webdav/sa.d/oauth-authz.properties) files +* include the custom token issuer in the [application-issuers.yml](../compose/assets/etc/storm/webdav/config/application-issuers.yml) file +* write down authorization policies for the `fga` storage area indicating your token issuer in the [application-policies.yml](../compose/assets/etc/storm/webdav/config/application-policies.yml) file. The default behavior is: + * users presenting a VOMS proxy released by a `test.vo` can read/write in the SA + * anyone can read in the `/public` folder and sub-folders + * users presenting a JWT token which embeds the `/cms` group have read/write access in the `/cms` folder and sub-folders + * users presenting a JWT token which embeds the `/data-manager` group have read/write access in the SA. + +In case the group claim in your token is not `wlcg.groups`, please append among the `ROBOT_ARGS` + +``` +--variable oauth.group.claim: --variable oauth.optional.group.claim: +``` + +Remember to set the proper oidc-agent alias appending also + +``` +--variable oidc-agent.alias: +``` \ No newline at end of file diff --git a/robot/assets/README.md b/robot/assets/README.md deleted file mode 100644 index 80998f3d..00000000 --- a/robot/assets/README.md +++ /dev/null @@ -1 +0,0 @@ -This folder contains assets useful for the execution of the storm-webdav suite diff --git a/robot/assets/fixtures/default/config/application-hackathon.yml b/robot/assets/fixtures/default/config/application-hackathon.yml deleted file mode 100644 index 54fa7ab9..00000000 --- a/robot/assets/fixtures/default/config/application-hackathon.yml +++ /dev/null @@ -1,70 +0,0 @@ -spring: - profiles: - active: oidc - -oauth: - enable-oidc: true - - issuers: - - name: iam-test - issuer: https://iam-test.indigo-datacloud.eu/ - - - name: wlcg - issuer: https://wlcg.cloud.cnaf.infn.it/ - - - name: tf-local - issuer: http://localhost:8080 - - - name: tf - issuer: https://tf.cloud.cnaf.infn.it - -storm: - authz: - policies: - - sa: tf - description: Grant read access to the SA to tf members - actions: - - list - - read - effect: permit - principals: - - type: jwt-issuer - params: - iss: https://tf.cloud.cnaf.infn.it - - sa: wlcg - description: Grant read access to the SA to wlcg members - actions: - - list - - read - effect: permit - principals: - - type: vo - params: - vo: wlcg - - type: jwt-issuer - params: - iss: https://wlcg.cloud.cnaf.infn.it/ - - sa: wlcg - description: Grant all access to /wlcg/protected to /wlcg/test members - actions: - - all - paths: - - protected/** - effect: permit - principals: - - type: fqan - params: - fqan: /wlcg/Role=test - - type: jwt-group - params: - iss: https://wlcg.cloud.cnaf.infn.it/ - group: /wlcg/test - - sa: wlcg - description: Grant all access to /wlcg VOMS members - actions: - - all - effect: permit - principals: - - type: vo - params: - vo: wlcg diff --git a/robot/assets/fixtures/default/config/application-oidc.yml b/robot/assets/fixtures/default/config/application-oidc.yml deleted file mode 100644 index 5351068b..00000000 --- a/robot/assets/fixtures/default/config/application-oidc.yml +++ /dev/null @@ -1,24 +0,0 @@ -spring: - security: - oauth2: - client: - registration: - iam-test: - provider: indigo - authorization-grant-type: authorization_code - client-name: INDIGO IAM test instance - client-id: ${IAM_TEST_CLIENT_ID} - client-secret: ${IAM_TEST_CLIENT_SECRET} - scope: - - openid - - profile - wlcg: - provider: wlcg - authorization-grant-type: authorization_code - client-name: WLCG IAM - client-id: ${WLCG_CLIENT_ID} - client-secret: ${WLCG_CLIENT_SECRET} - scope: - - openid - - profile - - wlcg.groups diff --git a/robot/assets/fixtures/default/config/application.yml b/robot/assets/fixtures/default/config/application.yml deleted file mode 100644 index 6f190f59..00000000 --- a/robot/assets/fixtures/default/config/application.yml +++ /dev/null @@ -1,62 +0,0 @@ -spring: - profiles: - active: oidc - -oauth: - enable-oidc: true - - issuers: - - name: iam-test - issuer: https://iam-test.indigo-datacloud.eu/ - - - name: wlcg - issuer: https://wlcg.cloud.cnaf.infn.it/ - - - name: tf-local - issuer: http://localhost:8080 - - - name: tf - issuer: https://tf.cloud.cnaf.infn.it - -storm: - authz: - policies: - - sa: fga - actions: - - list - - read - effect: permit - description: Grant read access to anyone to the public area - paths: - - /public/** - principals: - - type: anyone - - sa: fga - actions: - - all - effect: permit - description: Grant read/write access to test.vo and WLCG VOMS vo users - principals: - - type: vo - params: - vo: test.vo - - type: vo - params: - vo: wlcg - - type: jwt-group - params: - iss: https://wlcg.cloud.cnaf.infn.it/ - group: /wlcg - - sa: wlcg - actions: - - all - effect: permit - description: Grant read/write access to WLCG VOMS vo users - principals: - - type: vo - params: - vo: wlcg - - type: jwt-group - params: - iss: https://wlcg.cloud.cnaf.infn.it/ - group: /wlcg/xfers diff --git a/robot/assets/fixtures/default/sa.d/README.md b/robot/assets/fixtures/default/sa.d/README.md deleted file mode 100644 index d158bf4f..00000000 --- a/robot/assets/fixtures/default/sa.d/README.md +++ /dev/null @@ -1,2 +0,0 @@ -This folder contains the storage area configuration required for a default execution -of the storm-webdav testsuite diff --git a/robot/assets/fixtures/default/sa.d/auth.properties b/robot/assets/fixtures/default/sa.d/auth.properties deleted file mode 100644 index a0fe26f7..00000000 --- a/robot/assets/fixtures/default/sa.d/auth.properties +++ /dev/null @@ -1,7 +0,0 @@ -name=auth -rootPath=/storage/auth -filesystemType=posixfs -accessPoints=/auth -authenticatedReadEnabled=true -anonymousReadEnabled=false -voMapGrantsWritePermission=false diff --git a/robot/assets/fixtures/default/sa.d/fga.properties b/robot/assets/fixtures/default/sa.d/fga.properties deleted file mode 100644 index afda19f1..00000000 --- a/robot/assets/fixtures/default/sa.d/fga.properties +++ /dev/null @@ -1,5 +0,0 @@ -name=fga -rootPath=/storage/fga -filesystemType=posixfs -accessPoints=/fga -fineGrainedAuthzEnabled=true diff --git a/robot/assets/fixtures/default/sa.d/noauth.properties b/robot/assets/fixtures/default/sa.d/noauth.properties deleted file mode 100644 index 7af45b9c..00000000 --- a/robot/assets/fixtures/default/sa.d/noauth.properties +++ /dev/null @@ -1,7 +0,0 @@ -name=noauth -rootPath=/storage/noauth -filesystemType=posixfs -accessPoints=/noauth -authenticatedReadEnabled=true -anonymousReadEnabled=true -voMapGrantsWritePermission=false diff --git a/robot/assets/fixtures/default/sa.d/oauth_authz.properties b/robot/assets/fixtures/default/sa.d/oauth_authz.properties deleted file mode 100644 index 81171037..00000000 --- a/robot/assets/fixtures/default/sa.d/oauth_authz.properties +++ /dev/null @@ -1,8 +0,0 @@ -name=oauth-authz -rootPath=/storage/oauth-authz -filesystemType=posixfs -accessPoints=/oauth-authz -orgs=https://iam-test.indigo-datacloud.eu/ -authenticatedReadEnabled=false -anonymousReadEnabled=false -voMapGrantsWritePermission=false diff --git a/robot/assets/fixtures/default/sa.d/test_vo.properties b/robot/assets/fixtures/default/sa.d/test_vo.properties deleted file mode 100644 index ee96ca81..00000000 --- a/robot/assets/fixtures/default/sa.d/test_vo.properties +++ /dev/null @@ -1,9 +0,0 @@ -name=test.vo -rootPath=/storage/test.vo -filesystemType=posixfs -accessPoints=/test.vo,/ciccio -vos=test.vo,local -orgs=https://iam-test.indigo-datacloud.eu/ -authenticatedReadEnabled=false -anonymousReadEnabled=false -voMapGrantsWritePermission=false diff --git a/robot/assets/fixtures/default/sa.d/tf.properties b/robot/assets/fixtures/default/sa.d/tf.properties deleted file mode 100644 index fed2e0b6..00000000 --- a/robot/assets/fixtures/default/sa.d/tf.properties +++ /dev/null @@ -1,12 +0,0 @@ -name=tf -rootPath=/storage/tf -filesystemType=posixfs -accessPoints=/tf -orgs=http://localhost:8080,https://tf.cloud.cnaf.infn.it -authenticatedReadEnabled=false -anonymousReadEnabled=false -voMapGrantsWritePermission=false -wlcgScopeAuthzEnabled=true -fineGrainedAuthzEnabled=false -orgsGrantReadPermission=false -orgsGrantWritePermission=false diff --git a/robot/assets/fixtures/redirector/config/application-oidc.yml b/robot/assets/fixtures/redirector/config/application-oidc.yml deleted file mode 100644 index 5351068b..00000000 --- a/robot/assets/fixtures/redirector/config/application-oidc.yml +++ /dev/null @@ -1,24 +0,0 @@ -spring: - security: - oauth2: - client: - registration: - iam-test: - provider: indigo - authorization-grant-type: authorization_code - client-name: INDIGO IAM test instance - client-id: ${IAM_TEST_CLIENT_ID} - client-secret: ${IAM_TEST_CLIENT_SECRET} - scope: - - openid - - profile - wlcg: - provider: wlcg - authorization-grant-type: authorization_code - client-name: WLCG IAM - client-id: ${WLCG_CLIENT_ID} - client-secret: ${WLCG_CLIENT_SECRET} - scope: - - openid - - profile - - wlcg.groups diff --git a/robot/assets/fixtures/redirector/config/application.yml b/robot/assets/fixtures/redirector/config/application.yml deleted file mode 100644 index 6a75854e..00000000 --- a/robot/assets/fixtures/redirector/config/application.yml +++ /dev/null @@ -1,70 +0,0 @@ -spring: - profiles: - active: oidc - -oauth: - enable-oidc: true - - issuers: - - name: iam-test - issuer: https://iam-test.indigo-datacloud.eu/ - - - name: wlcg - issuer: https://wlcg.cloud.cnaf.infn.it/ - - - name: tf-local - issuer: http://localhost:8080 - - - name: tf - issuer: https://tf.cloud.cnaf.infn.it - -storm: - - redirector: - enabled: true - max-token-lifetime-secs: 600 - pool: - endpoints: - - endpoint: http://storm.example:8085 - - authz: - policies: - - sa: fga - actions: - - list - - read - effect: permit - description: Grant read access to anyone to the public area - paths: - - /public/** - principals: - - type: anyone - - sa: fga - actions: - - all - effect: permit - description: Grant read/write access to test.vo and WLCG VOMS vo users - principals: - - type: vo - params: - vo: test.vo - - type: vo - params: - vo: wlcg - - type: jwt-group - params: - iss: https://wlcg.cloud.cnaf.infn.it/ - group: /wlcg - - sa: wlcg - actions: - - all - effect: permit - description: Grant read/write access to WLCG VOMS vo users - principals: - - type: vo - params: - vo: wlcg - - type: jwt-group - params: - iss: https://wlcg.cloud.cnaf.infn.it/ - group: /wlcg/xfers diff --git a/robot/assets/fixtures/redirector/sa.d/README.md b/robot/assets/fixtures/redirector/sa.d/README.md deleted file mode 100644 index d158bf4f..00000000 --- a/robot/assets/fixtures/redirector/sa.d/README.md +++ /dev/null @@ -1,2 +0,0 @@ -This folder contains the storage area configuration required for a default execution -of the storm-webdav testsuite diff --git a/robot/assets/fixtures/redirector/sa.d/auth.properties b/robot/assets/fixtures/redirector/sa.d/auth.properties deleted file mode 100644 index a0fe26f7..00000000 --- a/robot/assets/fixtures/redirector/sa.d/auth.properties +++ /dev/null @@ -1,7 +0,0 @@ -name=auth -rootPath=/storage/auth -filesystemType=posixfs -accessPoints=/auth -authenticatedReadEnabled=true -anonymousReadEnabled=false -voMapGrantsWritePermission=false diff --git a/robot/assets/fixtures/redirector/sa.d/fga.properties b/robot/assets/fixtures/redirector/sa.d/fga.properties deleted file mode 100644 index afda19f1..00000000 --- a/robot/assets/fixtures/redirector/sa.d/fga.properties +++ /dev/null @@ -1,5 +0,0 @@ -name=fga -rootPath=/storage/fga -filesystemType=posixfs -accessPoints=/fga -fineGrainedAuthzEnabled=true diff --git a/robot/assets/fixtures/redirector/sa.d/noauth.properties b/robot/assets/fixtures/redirector/sa.d/noauth.properties deleted file mode 100644 index 7af45b9c..00000000 --- a/robot/assets/fixtures/redirector/sa.d/noauth.properties +++ /dev/null @@ -1,7 +0,0 @@ -name=noauth -rootPath=/storage/noauth -filesystemType=posixfs -accessPoints=/noauth -authenticatedReadEnabled=true -anonymousReadEnabled=true -voMapGrantsWritePermission=false diff --git a/robot/assets/fixtures/redirector/sa.d/oauth_authz.properties b/robot/assets/fixtures/redirector/sa.d/oauth_authz.properties deleted file mode 100644 index 81171037..00000000 --- a/robot/assets/fixtures/redirector/sa.d/oauth_authz.properties +++ /dev/null @@ -1,8 +0,0 @@ -name=oauth-authz -rootPath=/storage/oauth-authz -filesystemType=posixfs -accessPoints=/oauth-authz -orgs=https://iam-test.indigo-datacloud.eu/ -authenticatedReadEnabled=false -anonymousReadEnabled=false -voMapGrantsWritePermission=false diff --git a/robot/assets/fixtures/redirector/sa.d/test_vo.properties b/robot/assets/fixtures/redirector/sa.d/test_vo.properties deleted file mode 100644 index ee96ca81..00000000 --- a/robot/assets/fixtures/redirector/sa.d/test_vo.properties +++ /dev/null @@ -1,9 +0,0 @@ -name=test.vo -rootPath=/storage/test.vo -filesystemType=posixfs -accessPoints=/test.vo,/ciccio -vos=test.vo,local -orgs=https://iam-test.indigo-datacloud.eu/ -authenticatedReadEnabled=false -anonymousReadEnabled=false -voMapGrantsWritePermission=false diff --git a/robot/assets/fixtures/redirector/sa.d/tf.properties b/robot/assets/fixtures/redirector/sa.d/tf.properties deleted file mode 100644 index fed2e0b6..00000000 --- a/robot/assets/fixtures/redirector/sa.d/tf.properties +++ /dev/null @@ -1,12 +0,0 @@ -name=tf -rootPath=/storage/tf -filesystemType=posixfs -accessPoints=/tf -orgs=http://localhost:8080,https://tf.cloud.cnaf.infn.it -authenticatedReadEnabled=false -anonymousReadEnabled=false -voMapGrantsWritePermission=false -wlcgScopeAuthzEnabled=true -fineGrainedAuthzEnabled=false -orgsGrantReadPermission=false -orgsGrantWritePermission=false diff --git a/robot/assets/fixtures/redirector/sa.d/wlcg.properties b/robot/assets/fixtures/redirector/sa.d/wlcg.properties deleted file mode 100644 index 0d635e49..00000000 --- a/robot/assets/fixtures/redirector/sa.d/wlcg.properties +++ /dev/null @@ -1,12 +0,0 @@ -name=wlcg -rootPath=/storage/wlcg -filesystemType=posixfs -accessPoints=/wlcg -orgs=https://wlcg.cloud.cnaf.infn.it/ -authenticatedReadEnabled=false -anonymousReadEnabled=false -voMapGrantsWritePermission=false -wlcgScopeAuthzEnabled=true -fineGrainedAuthzEnabled=true -orgsGrantReadPermission=false -orgsGrantWritePermission=false diff --git a/robot/common/credentials.robot b/robot/common/credentials.robot index 2e53a682..80ad3f06 100644 --- a/robot/common/credentials.robot +++ b/robot/common/credentials.robot @@ -4,34 +4,14 @@ Library VOMSHelperLibrary *** Variables *** -${cred.voms.use_os} True - ## Where the testsuite should look for an OAuth ## access token ${cred.oauth.env_var_name} IAM_ACCESS_TOKEN -## Embedded VOMS proxies -${cred.voms.1} assets/certs/voms.1 -${cred.voms.2} assets/certs/voms.2 - -${cred.voms.default} ${cred.voms.1} - -## Embedded GRID proxies -${cred.grid.1} assets/certs/grid.1 -${cred.grid.default} ${cred.grid.1} - -## Embedded X.509 certs -${cred.cert.1.p12} assets/certs/test0.p12 -${cred.cert.1.cert} assets/certs/test0.pem -${cred.cert.1.password} pass - -${cred.cert.2.p12} assets/certs/test1.p12 -${cred.cert.2.cert} assets/certs/test1.pem -${cred.cert.2.password} pass +${cred.voms.use_os} True -${cred.cert.default.p12} ${cred.cert.1.p12} -${cred.cert.default.cert} ${cred.cert.1.cert} -${cred.cert.default.password} ${cred.cert.1.password} +## Embedded VOMS proxies +${cred.voms.default} assets/certs/voms.1 *** Keywords *** Default Proxy Path diff --git a/robot/common/curl.robot b/robot/common/curl.robot index e527b422..7a464572 100644 --- a/robot/common/curl.robot +++ b/robot/common/curl.robot @@ -10,7 +10,10 @@ ${x509.trustdir} /etc/grid-security/certificates *** Keywords *** Curl [Arguments] ${url} ${opts}=${curl.opts.default} - ${rc} ${out} Run and Return RC And Output curl ${url} ${opts} + ${cmd} Set Variable curl ${url} ${opts} + Log ${cmd} level=debug + ${rc} ${out} Run and Return RC And Output ${cmd} + Log ${out} level=debug [Return] ${rc} ${out} Curl Success [Arguments] ${url} ${opts}=${curl.opts.default} @@ -32,18 +35,36 @@ Curl Voms HEAD Success [Arguments] ${url} ${opts}=${curl.opts.default} ${rc} ${out} Curl Success ${url} ${all_opts} [Return] ${rc} ${out} +Curl Voms HEAD Failure [Arguments] ${url} ${opts}=${curl.opts.default} + ${voms_opts} Get Curl Voms Proxy Options + ${all_opts} Set variable --HEAD ${opts} ${voms_opts} + ${rc} ${out} Curl Error ${url} ${all_opts} + [Return] ${rc} ${out} + Curl Voms Get Success [Arguments] ${url} ${opts}=${curl.opts.default} ${voms_opts} Get Curl Voms Proxy Options ${all_opts} Set variable -X GET ${opts} ${voms_opts} ${rc} ${out} Curl Success ${url} ${all_opts} [Return] ${rc} ${out} +Curl Voms Get Failure [Arguments] ${url} ${opts}=${curl.opts.default} + ${voms_opts} Get Curl Voms Proxy Options + ${all_opts} Set variable -X GET ${opts} ${voms_opts} + ${rc} ${out} Curl Error ${url} ${all_opts} + [Return] ${rc} ${out} + Curl Voms MKCOL Success [Arguments] ${url} ${opts}=${curl.opts.default} ${voms_opts} Get Curl Voms Proxy Options ${all_opts} Set variable -X MKCOL ${opts} ${voms_opts} ${rc} ${out} Curl Success ${url} ${all_opts} [Return] ${rc} ${out} +Curl Voms MKCOL Failure [Arguments] ${url} ${opts}=${curl.opts.default} + ${voms_opts} Get Curl Voms Proxy Options + ${all_opts} Set variable -X MKCOL ${opts} ${voms_opts} + ${rc} ${out} Curl Error ${url} ${all_opts} + [Return] ${rc} ${out} + Curl Voms Pull COPY Success [Arguments] ${dest} ${source} ${opts}=${curl.opts.default} ${voms_opts} Get Curl Voms Proxy Options ${all_opts} Set variable -X COPY -H "Source: ${source}" ${opts} ${voms_opts} @@ -80,6 +101,12 @@ Curl Voms PUT Success [Arguments] ${file} ${url} ${opts}=${curl.opts.default ${rc} ${out} Curl Success ${url} ${all_opts} [Return] ${rc} ${out} +Curl Voms PUT Failure [Arguments] ${file} ${url} ${opts}=${curl.opts.default} + ${voms_opts} Get Curl Voms Proxy Options + ${all_opts} Set variable -X PUT -T ${file} ${opts} ${voms_opts} + ${rc} ${out} Curl Error ${url} ${all_opts} + [Return] ${rc} ${out} + Curl Voms POST Success [Arguments] ${url} ${opts}=${curl.opts.default} ${voms_opts} Get Curl Voms Proxy Options ${all_opts} Set variable -X POST ${opts} ${voms_opts} @@ -92,18 +119,42 @@ Curl Voms POST Failure [Arguments] ${url} ${opts}=${curl.opts.default} ${rc} ${out} Curl Error ${url} ${all_opts} [Return] ${rc} ${out} +Curl Voms DELETE Success [Arguments] ${url} ${opts}=${curl.opts.default} + ${voms_opts} Get Curl Voms Proxy Options + ${all_opts} Set variable -X DELETE ${opts} ${voms_opts} + ${rc} ${out} Curl Success ${url} ${all_opts} + [Return] ${rc} ${out} + +Curl Voms DELETE Failure [Arguments] ${url} ${opts}=${curl.opts.default} + ${voms_opts} Get Curl Voms Proxy Options + ${all_opts} Set variable -X DELETE ${opts} ${voms_opts} + ${rc} ${out} Curl Error ${url} ${all_opts} + [Return] ${rc} ${out} + Curl Voms MOVE Success [Arguments] ${dest} ${source} ${opts}=${curl.opts.default} ${voms_opts} Get Curl Voms Proxy Options ${all_opts} Set variable -X MOVE -H "Destination: ${dest}" ${opts} ${voms_opts} ${rc} ${out} Curl Success ${source} ${all_opts} [Return] ${rc} ${out} +Curl Voms MOVE Failure [Arguments] ${dest} ${source} ${opts}=${curl.opts.default} + ${voms_opts} Get Curl Voms Proxy Options + ${all_opts} Set variable -X MOVE -H "Destination: ${dest}" ${opts} ${voms_opts} + ${rc} ${out} Curl Error ${source} ${all_opts} + [Return] ${rc} ${out} + Curl Voms MOVE [Arguments] ${dest} ${source} ${opts}=-s -L -i ${voms_opts} Get Curl Voms Proxy Options ${all_opts} Set variable -X MOVE -H "Destination: ${dest}" ${opts} ${voms_opts} ${rc} ${out} Curl ${source} ${all_opts} [Return] ${rc} ${out} +Curl Voms OPTIONS [Arguments] ${url} ${opts}=-s -L -i + ${voms_opts} Get Curl Voms Proxy Options + ${all_opts} Set variable -X OPTIONS ${voms_opts} + ${rc} ${out} Curl ${url} ${all_opts} + [Return] ${rc} ${out} + Curl pull COPY Success [Arguments] ${dest} ${source} ${opts}=${curl.opts.default} ${all_opts} Set variable -X COPY -H "Source: ${source}" ${opts} ${rc} ${out} Curl Success ${dest} ${all_opts} @@ -112,4 +163,10 @@ Curl pull COPY Success [Arguments] ${dest} ${source} ${opts}=${curl.opts.def Curl push COPY Success [Arguments] ${dest} ${source} ${opts}=${curl.opts.default} ${all_opts} Set variable -X COPY -H "Destination: ${dest}" ${opts} ${rc} ${out} Curl Success ${source} ${all_opts} + [Return] ${rc} ${out} + +Curl Voms PROPFIND [Arguments] ${url} ${body} ${opts}=${curl.opts.default} + ${voms_opts} Get Curl Voms Proxy Options + ${all_opts} Set variable -X PROPFIND ${opts} ${voms_opts} --data ${body} + ${rc} ${out} Curl ${url} ${all_opts} [Return] ${rc} ${out} \ No newline at end of file diff --git a/robot/common/oidc-agent.robot b/robot/common/oidc-agent.robot new file mode 100644 index 00000000..ea126e51 --- /dev/null +++ b/robot/common/oidc-agent.robot @@ -0,0 +1,16 @@ +*** Settings *** + +Resource common/credentials.robot + +*** Variables *** + +${oidc-agent.scope.default} -s openid +${oidc-agent.alias} dev-wlcg + + +*** Keywords *** + +Get token [Arguments] ${scope}=${oidc-agent.scope.default} ${issuer}=${oidc-agent.alias} ${opts}=${EMPTY} + ${rc} ${out} Execute and Check Success oidc-token ${scope} ${opts} ${issuer} + Set Environment Variable ${cred.oauth.env_var_name} ${out} + [Return] ${out} \ No newline at end of file diff --git a/robot/common/setup_and_teardown.robot b/robot/common/setup_and_teardown.robot new file mode 100644 index 00000000..e579684a --- /dev/null +++ b/robot/common/setup_and_teardown.robot @@ -0,0 +1,30 @@ +*** Keywords *** + +Default Setup + Default VOMS credential + +Default Teardown + Unset VOMS credential + +Setup file [Arguments] ${file_name} ${content}=Hello World! + Default Setup + Create Test File ${file_name} ${content} + +Setup directory [Arguments] ${dir_name} + Default Setup + Create Test Directory ${dir_name} + +Teardown file [Arguments] ${file_name} + Default Teardown + Remove Test File ${file_name} + Remove Test File ${file_name}.dest + +Teardown file cross sa [Arguments] ${file_name} + Default Teardown + Remove Test File ${file_name} + Remove Test File ${file_name}.dest sa=${sa.oauth} + +Teardown directory [Arguments] ${dir_name} + Default Teardown + Remove Test Directory ${dir_name} + Remove Test Directory ${dir_name}.dest \ No newline at end of file diff --git a/robot/common/storage_areas.robot b/robot/common/storage_areas.robot index 0455379d..1ba23ed6 100644 --- a/robot/common/storage_areas.robot +++ b/robot/common/storage_areas.robot @@ -6,11 +6,9 @@ Resource common/utils.robot *** Variables *** ${sa.default} test.vo -${sa.auth} auth ${sa.noauth} noauth ${sa.fga} fga ${sa.oauth} oauth-authz -${sa.wlcg} wlcg ${storage.root} /storage @@ -29,11 +27,16 @@ Create 1MB Test File [Arguments] ${file} ${sa}=${sa.default} ${path}= Normalize Path ${storage.root}/${sa}/${file} File Should Not Exist ${path} ${rc} ${out} Execute and Check Success dd if=/dev/zero of=${path} bs=1 count=0 seek=1048576 + +Create Test Directory [Arguments] ${directory} ${sa}=${sa.default} + ${path}= Normalize Path ${storage.root}/${sa}/${directory} + Directory Should Not Exist ${path} + Create Directory ${path} Remove Test File [Arguments] ${file} ${sa}=${sa.default} ${path}= Normalize Path ${storage.root}/${sa}/${file} Remove file ${path} -Remove Test Directory [Arguments] ${file} ${sa}=${sa.default} - ${path}= Normalize Path ${storage.root}/${sa}/${file} - Remove Directory ${path} \ No newline at end of file +Remove Test Directory [Arguments] ${directory} ${sa}=${sa.default} + ${path}= Normalize Path ${storage.root}/${sa}/${directory} + Remove Directory ${path} recursive=true \ No newline at end of file diff --git a/robot/common/utils.robot b/robot/common/utils.robot index 6e07c0c9..40d141ef 100644 --- a/robot/common/utils.robot +++ b/robot/common/utils.robot @@ -1,12 +1,16 @@ *** Keywords *** Execute and Check Success [Arguments] ${cmd} + Log ${cmd} level=debug ${rc} ${output} Run and Return RC And Output ${cmd} + Log ${output} level=debug Should Be Equal As Integers ${rc} 0 ${cmd} exited with status ${rc} != 0 : ${output} False [Return] ${rc} ${output} Execute and Check Failure [Arguments] ${cmd} + Log ${cmd} level=debug ${rc} ${output} Run and Return RC And Output ${cmd} + Log ${output} level=debug Should Not Be Equal As Integers ${rc} 0 ${cmd} exited with 0 : ${output} False [Return] ${rc} ${output} diff --git a/robot/reports/.gitignore b/robot/reports/.gitignore deleted file mode 100644 index ced4d9da..00000000 --- a/robot/reports/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -./log.html -./output.xml -./report.html diff --git a/robot/run-testsuite.sh b/robot/run-testsuite.sh index 42fb8c8f..b8472441 100755 --- a/robot/run-testsuite.sh +++ b/robot/run-testsuite.sh @@ -16,15 +16,11 @@ # set -ex -DAV_HOST=${DAV_HOST:-localhost} - -REMOTE_DAV_HOST=${REMOTE_DAV_HOST:-${DAV_HOST:-localhost}} - REPORTS_DIR=${REPORTS_DIR:-reports} ROBOT_ARGS=${ROBOT_ARGS:-} -DEFAULT_ARGS="--pythonpath .:common --variable dav.host:${DAV_HOST} --variable remote.dav.host:${REMOTE_DAV_HOST} -d ${REPORTS_DIR}" +DEFAULT_ARGS="--pythonpath .:common -d ${REPORTS_DIR}" ARGS=${DEFAULT_ARGS} diff --git a/robot/test/authorization.robot b/robot/test/authorization.robot new file mode 100644 index 00000000..4cdd367f --- /dev/null +++ b/robot/test/authorization.robot @@ -0,0 +1,223 @@ +*** Settings *** +Resource common/storage_areas.robot +Resource common/credentials.robot +Resource common/davix.robot +Resource common/curl.robot +Resource common/setup_and_teardown.robot +Resource common/oidc-agent.robot +Resource test/variables.robot + +Test Setup Get token +Test Teardown Get token + +*** Variables *** + +${oauth.group.claim} wlcg.groups +${oauth.group.value} cms + +${oauth.optional.group.claim} ${oauth.group.claim}:/${oauth.optional.group.value} +${oauth.optional.group.value} data-manager + + +*** Keywords *** + +Setup directory fga [Arguments] ${dir_name} ${file_name}=test_file + Create Test Directory ${dir_name} ${sa.fga} + Create Test File ${dir_name}/${file_name} Hello world! ${sa.fga} + +Teardown file fga [Arguments] ${file_name} + Remove Test File ${file_name} ${sa.fga} + +Teardown directory fga [Arguments] ${dir_name} + Remove Test Directory ${dir_name} ${sa.fga} + + +*** Test cases *** + +Read access allowed to anyone to the public area + [Tags] fga get + [Setup] Setup directory fga public + ${url} DAVS URL public/test_file ${sa.fga} + ${rc} ${out} Curl Success ${url} ${curl.opts.default} + Should Contain ${out} Hello world! + [Teardown] Teardown directory fga public + +List access allowed to anyone to the public area + [Tags] fga propfind + [Setup] Setup directory fga public + ${url} DAVS URL public ${sa.fga} + ${rc} ${out} Curl Success ${url} -X PROPFIND ${curl.opts.default} + Should Contain ${out} test_file + [Teardown] Teardown directory fga public + +Anonymous put not allowed to the public area + [Tags] fga put + [Setup] Run Keywords Create Temporary File put_not_allowed 123456789 + ... AND Setup directory fga public + ${url} DAVS URL public/put_not_allowed ${sa.fga} + ${rc} ${out} Curl Error ${url} -X PUT -T ${TEMPDIR}/put_not_allowed ${curl.opts.default} + Should Match Regexp ${out} 401|403 + [Teardown] Run Keywords Remove Temporary File put_not_allowed + ... AND Teardown directory fga public + +Anonymous mkcol not allowed to the public area + [Tags] fga mkcol + [Setup] Setup directory fga public + ${url} DAVS URL public/mkcol_not_allowed ${sa.fga} + ${rc} ${out} Curl Error ${url} -X MKCOL ${curl.opts.default} + Should Match Regexp ${out} 401|403 + [Teardown] Teardown directory fga public + +Anonymous read not allowed outside the public area + [Tags] fga get + [Setup] Setup directory fga anonymous + ${url} DAVS URL anonymous/test_file ${sa.fga} + ${rc} ${out} Curl Error ${url} ${curl.opts.default} + Should Match Regexp ${out} 401|403 + [Teardown] Teardown directory fga anonymous + +Anonymous list not allowed outside the public area + [Tags] fga propfind + [Setup] Setup directory fga anonymous + ${url} DAVS URL anonymous ${sa.fga} + ${rc} ${out} Curl Error ${url} -X PROPFIND ${curl.opts.default} + Should Match Regexp ${out} 401|403 + [Teardown] Teardown directory fga anonymous + +Read access allowed to trusted issued tokens + [Tags] fga get oauth + [Setup] Setup directory fga trusted_issuer + ${token} Get token scope=-s openid + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL trusted_issuer/test_file ${sa.fga} + ${rc} ${out} Curl Success ${url} ${curl.opts.oauth} ${curl.opts.default} + Should Contain ${out} Hello world! + [Teardown] Teardown directory fga trusted_issuer + +List access allowed to trusted issued tokens + [Tags] fga propfind oauth + [Setup] Setup directory fga trusted_issuer + ${token} Get token scope=-s openid + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL trusted_issuer ${sa.fga} + ${rc} ${out} Curl Success ${url} -X PROPFIND ${curl.opts.oauth} ${curl.opts.default} + Should Contain ${out} test_file + [Teardown] Teardown directory fga trusted_issuer + +Put not allowed to the trusted issued tokens + [Tags] fga put oauth + [Setup] Create Temporary File trusted_issuer 123456789 + ${token} Get token scope=-s openid + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL trusted_issuer ${sa.fga} + ${rc} ${out} Curl Error ${url} -X PUT -T ${TEMPDIR}/trusted_issuer ${curl.opts.oauth} ${curl.opts.default} + Should Match Regexp ${out} 401|403 + [Teardown] Remove Temporary File trusted_issuer + +Mkcol not allowed to the trusted issued tokens + [Tags] fga mkcol oauth + ${token} Get token scope=-s openid + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL trusted_issuer ${sa.fga} + ${rc} ${out} Curl Error ${url} -X MKCOL ${curl.opts.oauth} ${curl.opts.default} + Should Match Regexp ${out} 401|403 + +Read access allowed to the cms group in the namespace + [Tags] fga get oauth + [Setup] Setup directory fga cms + ${token} Get token scope=-s ${oauth.group.claim} + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL cms/test_file ${sa.fga} + ${rc} ${out} Curl Success ${url} ${curl.opts.oauth} ${curl.opts.default} + Should Contain ${out} Hello world! + [Teardown] Teardown directory fga cms + +List access allowed to the cms group in the namespace + [Tags] fga propfind oaut + [Setup] Setup directory fga cms + ${token} Get token scope=-s ${oauth.group.claim} + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL cms ${sa.fga} + ${rc} ${out} Curl Success ${url} -X PROPFIND ${curl.opts.oauth} ${curl.opts.default} + Should Contain ${out} test_file + [Teardown] Teardown directory fga cms + +Put allowed to the cms group in the namespace + [Tags] fga put oauth + [Setup] Run Keywords Create Temporary File cms_group 123456789 + ... AND Setup directory fga cms + ${token} Get token scope=-s ${oauth.group.claim} + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL cms/cms_group ${sa.fga} + ${rc} ${out} Curl Success ${url} -X PUT -T ${TEMPDIR}/cms_group ${curl.opts.oauth} ${curl.opts.default} + Should Contain ${out} 201 Created + [Teardown] Run Keywords Remove Temporary File cms_group + ... AND Teardown directory fga cms + +Mkcol allowed to the cms group in the namespace + [Tags] fga mkcol oauth + [Setup] Setup directory fga cms + ${token} Get token scope=-s ${oauth.group.claim} + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL cms/cms_group ${sa.fga} + Curl Success ${url} -X MKCOL ${curl.opts.oauth} ${curl.opts.default} + [Teardown] Teardown directory fga cms + +Put denied to the cms group outside the namespace + [Tags] fga put oauth + [Setup] Run Keywords Create Temporary File denied 123456789 + ... AND Setup directory fga denied + ${token} Get token scope=-s ${oauth.group.claim} + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL denied/denied ${sa.fga} + ${rc} ${out} Curl Error ${url} -X PUT -T ${TEMPDIR}/denied ${curl.opts.oauth} ${curl.opts.default} + Should Match Regexp ${out} 401|403 + [Teardown] Run Keywords Remove Temporary File denied + ... AND Teardown directory fga denied + +Mkcol denied to the cms group outside the namespace + [Tags] fga mkcol oauth + ${token} Get token scope=-s ${oauth.group.claim} + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL denied ${sa.fga} + ${rc} ${out} Curl Error ${url} -X MKCOL ${curl.opts.oauth} ${curl.opts.default} + Should Match Regexp ${out} 401|403 + +Read access allowed to data-manager group + [Tags] fga get oauth + [Setup] Setup directory fga data-manager + ${token} Get token scope=-s ${oauth.optional.group.claim} + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL data-manager/test_file ${sa.fga} + ${rc} ${out} Curl Success ${url} ${curl.opts.oauth} ${curl.opts.default} + Should Contain ${out} Hello world! + [Teardown] Teardown directory fga data-manager + +List access allowed to data-manager group + [Tags] fga propfind oauth + [Setup] Setup directory fga data-manager + ${token} Get token scope=-s ${oauth.optional.group.claim} + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL data-manager ${sa.fga} + ${rc} ${out} Curl Success ${url} -X PROPFIND ${curl.opts.oauth} ${curl.opts.default} + Should Contain ${out} test_file + [Teardown] Teardown directory fga data-manager + +Put allowed to data-manager group + [Tags] fga put oauth + [Setup] Create Temporary File data-manager 123456789 + ${token} Get token scope=-s ${oauth.optional.group.claim} + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL data-manager ${sa.fga} + ${rc} ${out} Curl Success ${url} -X PUT -T ${TEMPDIR}/data-manager ${curl.opts.oauth} ${curl.opts.default} + Should Contain ${out} 201 Created + [Teardown] Run Keywords Remove Temporary File data-manager + ... AND Teardown file fga data-manager + +Mkcol allowed to data-manager group + [Tags] fga mkcol oauth + ${token} Get token scope=-s ${oauth.optional.group.claim} + ${curl.opts.oauth} Set Variable -H "Authorization: Bearer %{${cred.oauth.env_var_name}}" + ${url} DAVS URL data-manager ${sa.fga} + ${rc} ${out} Curl Success ${url} -X MKCOL ${curl.opts.oauth} ${curl.opts.default} + [Teardown] Teardown directory fga data-manager \ No newline at end of file diff --git a/robot/test/basic_tests.robot b/robot/test/basic_tests.robot index 5ecfa8af..6af1dc68 100644 --- a/robot/test/basic_tests.robot +++ b/robot/test/basic_tests.robot @@ -3,149 +3,39 @@ Resource common/storage_areas.robot Resource common/credentials.robot Resource common/davix.robot Resource common/curl.robot +Resource common/setup_and_teardown.robot Resource test/variables.robot Test Setup Default Setup Test Teardown Default Teardown -*** Keywords *** -Default Setup - Default VOMS credential - -Default Teardown - Unset VOMS credential - -Get works Setup - Default Setup - Create Test File get_test - -Get works Teardown - Default Teardown - Remove Test File get_test - -Put works Setup - Default Setup - Create Temporary File put_test 123456789 - -Put works Teardown - Default Teardown - Remove Temporary File put_test - -Rm works Setup - Default Setup - Create Test File rm_test - -Rm works Teardown - Default Setup - Remove Test File rm_test - -Mkdir works Teardown - Default Teardown - Remove Test Directory mkdir_test - -Partial Get Works Setup - Default Setup - Create Test File pget_test 1x2y456789 - -Partial Get Works Teardown - Default Setup - Remove Test File pget_test - -Partial Put Works Setup - Default Setup - Create Temporary File pput0_test 0000000000 - Create Temporary File pput1_test 1111111111 - -Partial Put Works Teardown - Default Teardown - Remove Test File pput_test - Remove Temporary File pput0_test - Remove Temporary File pput1_test - -Single Test File Setup [Arguments] ${file_name} - Default Setup - Create Test File ${file_name} - -Single Test File Teardown [Arguments] ${file_name} - Default Teardown - Remove Test File ${file_name} - -Head works on large files setup [Arguments] ${file_name} - Default setup - Create Test File With Size ${file_name} 2g - -Head works on large files teardown [Arguments] ${file_name} - Default Teardown - Remove Test File ${file_name} - *** Test cases *** -Get works - [Tags] voms get - [Setup] Get works Setup - Davix Get Success ${davs.endpoint}/${sa.default}/get_test - [Teardown] Get works Teardown - -Get returns 404 for file that does not exist - [Tags] voms get - ${rc} ${out} Davix Get Failure ${davs.endpoint}/${sa.default}/does_not_exist - Should Contain ${out} 404 - ${rc} ${out} Davix Get Failure ${davs.endpoint}/${sa.default}/does_not_exist/also - Should Contain ${out} 404 - -Put works - [Tags] voms put - [Setup] Put works Setup - Davix Put Success ${TEMPDIR}/put_test ${davs.endpoint}/${sa.default}/put_test - Davix Get Success ${davs.endpoint}/${sa.default}/put_test - Remove File put_test - [Teardown] Put works Teardown - -Rm works - [Tags] voms rm - [Setup] Rm works Setup - Davix Get Success ${davs.endpoint}/${sa.default}/rm_test - Davix Rm Success ${davs.endpoint}/${sa.default}/rm_test - ${rc} ${out} Davix Get Failure ${davs.endpoint}/${sa.default}/rm_test - Should Contain ${out} 404 - [Teardown] Rm works teardown - -Mkdir works - [Tags] voms Mkdir - ## There's a bug in Davix which returns 0 even if the mkdir call fails - ## Davix Mkdir Success ${davs.endpoint}/${sa.default}/mkdir_test - ${rc} ${out} Curl Voms MKCOL Success ${davs.endpoint}/${sa.default}/mkdir_test - [Teardown] Mkdir works teardown - -Partial Get works - [Tags] voms get partial - [Setup] Partial Get Works Setup - ${opts} Set Variable -H "Range: 0-3" ${curl.opts.default} - ${rc} ${out} Curl Voms Get Success ${davs.endpoint}/${sa.default}/pget_test ${opts} - Should Contain ${out} 1x2y - Should Contain ${out} ength: 4 - [Teardown] Partial Get Works Teardown - -Partial Put works - [Tags] voms put partial - [Setup] Partial Put Works Setup - ${opts} Set Variable -H "Content-Range: bytes=0-3/*" ${curl.opts.default} - ${dest} DAVS Url pput_test - ${rc} ${out} Curl Voms Put Success ${TEMPDIR}/pput0_test ${dest} - ${rc} ${out} Curl Voms Put Success ${TEMPDIR}/pput1_test ${dest} ${opts} - [Teardown] Partial Put Works Teardown - Post not allowed on content [Tags] voms post - [Setup] Single Test File Setup test_post_not_allowed + [Setup] Setup file test_post_not_allowed ${url} DAVS Url test_post_not_allowed ${rc} ${out} Curl Voms Post Failure ${url} - Should Contain ${out} 405 - [Teardown] Single Test File Teardown test_post_not_allowed - -Head works on large files - [Tags] voms head - [Setup] Head works on large files setup hwlf - ${rc} ${out} Curl Voms HEAD Success ${davs.endpoint}/${sa.default}/hwlf - Should Contain ${out} ength: 2147483648 - [Teardown] Head works on large files teardown hwlf \ No newline at end of file + Should Contain ${out} 405 Method Not Allowed + [Teardown] Teardown file test_post_not_allowed + +Rename file with missing parent + [Tags] voms + [Setup] Setup file rename-me + ${source} DAVS URL rename-me + ${dest} DAVS URL /parent-dir/child-dir/rename-me + ${rc} ${out} Curl Voms GET Success ${source} + Should Contain ${out} Hello World! + ${rc} ${out} Curl Voms HEAD Failure ${dest} + Should Contain ${out} 404 + ${rc} ${out} Curl Voms HEAD Failure ${davs.endpoint}/${sa.default}/parent-dir/child-dir + Should Contain ${out} 404 + ${rc} ${out} Curl Voms HEAD Failure ${davs.endpoint}/${sa.default}/parent-dir + Should Contain ${out} 404 + Curl Voms MKCOL Success ${davs.endpoint}/${sa.default}/parent-dir + Curl Voms MKCOL Success ${davs.endpoint}/${sa.default}/parent-dir/child-dir + ${rc} ${out} Curl Voms MOVE Success ${dest} ${source} + Davix Get Success ${dest} ${davix.opts.voms} + [Teardown] Run Keywords Default Teardown + ... AND Remove Test File rename-me + ... AND Remove Test Directory parent-dir \ No newline at end of file diff --git a/robot/test/checksum.robot b/robot/test/checksum.robot index 1007a32f..0a0cdd01 100644 --- a/robot/test/checksum.robot +++ b/robot/test/checksum.robot @@ -3,38 +3,42 @@ Resource common/storage_areas.robot Resource common/credentials.robot Resource common/davix.robot Resource common/curl.robot +Resource common/setup_and_teardown.robot Resource test/variables.robot Test Setup Default Setup Test Teardown Default Teardown -*** Keywords *** -Default Setup - Default VOMS credential - -Default Teardown - Unset VOMS credential - -Set extended attr [Arguments] ${file} ${attr} ${attr_value} +Default Tags checksum -Set checksum attr [Arguments] ${file} ${checksum} +*** Keywords *** -Get checksum works setup +Setup file for checksum [Arguments] ${file_name} ${content}=Hello World! Default Setup - Create Temporary File checksum_test 123456789 + Create Temporary File ${file_name} ${content} -Get checksum works Teardown +Teardown file for checksum [Arguments] ${file_name} Default Teardown - Remove Test File checksum_test - Remove Temporary File checksum_test + Teardown file ${file_name} + Remove Temporary File ${file_name} + *** Test cases *** Get checksum works - [Setup] Get checksum works setup - [Tags] voms checksum put - ${dst} DAVS Url checksum_test - Davix Put Success ${TEMPDIR}/checksum_test ${dst} - ${rc} ${out} Curl Voms Get Success ${dst} - Should Contain ${out} Digest: adler32=91e01de - [Teardown] Get checksum works Teardown \ No newline at end of file + [Setup] Setup file for checksum checksum_works 123456789 + [Tags] voms get + ${url} DAVS URL checksum_works + Davix Put Success ${TEMPDIR}/checksum_works ${url} + ${rc} ${out} Curl Voms GET Success ${url} + Should Contain ${out} Digest: adler32=091e01de + [Teardown] Teardown file for checksum checksum_works + +Head checksum works + [Setup] Setup file for checksum checksum_works test123456789 + [Tags] voms put + ${url} DAVS URL checksum_works + Davix Put Success ${TEMPDIR}/checksum_works ${url} + ${rc} ${out} Curl Voms HEAD Success ${url} + Should Contain ${out} Digest: adler32=1d3b039e + [Teardown] Teardown file for checksum checksum_works \ No newline at end of file diff --git a/robot/test/copy.robot b/robot/test/copy.robot index e8068752..2456bc34 100644 --- a/robot/test/copy.robot +++ b/robot/test/copy.robot @@ -3,6 +3,7 @@ Resource common/storage_areas.robot Resource common/credentials.robot Resource common/davix.robot Resource common/curl.robot +Resource common/setup_and_teardown.robot Resource test/variables.robot Test Setup Default Setup @@ -10,45 +11,83 @@ Test Teardown Default Teardown Default Tags copy -*** Keywords *** +*** Test cases *** -Default Setup - Default VOMS credential +Copy works + [Tags] voms + [Setup] Setup file copy_works + ${dest} DAVS URL copy_works.dest + ${source} DAVS URL copy_works + ${rc} ${out} Curl Voms Push COPY Success ${dest} ${source} + Davix Get Success ${dest} ${davix.opts.voms} + [Teardown] Teardown file copy_works -Default Teardown - Unset VOMS credential +Copy directory works + [Tags] voms + [Setup] Setup directory copy_works + ${dest} DAVS URL copy_works.dest + ${source} DAVS URL copy_works + ${rc} ${out} Curl Voms Push COPY Success ${dest} ${source} + Davix Get Success ${dest} ${davix.opts.voms} + [Teardown] Teardown directory copy_works -Setup copy file [Arguments] ${file_name} - Default Setup - Create Test File ${file_name} content=Hello World! +Copy not empty directory works + [Tags] voms + [Setup] Run Keywords Setup directory copy_works + ... AND Create Test File copy_works/file_copy_works + ${dest} DAVS URL copy_works.dest + ${source} DAVS URL copy_works + ${rc} ${out} Curl Voms Push COPY Success ${dest} ${source} + Davix Get Success ${dest} ${davix.opts.voms} + [Teardown] Teardown directory copy_works -Teardown copy file [Arguments] ${file_name} - Default Teardown - Remove Test File ${file_name} - Remove Test File ${file_name}.copied +Copy override works + [Tags] voms + [Setup] Setup file copy_works + ${dest} DAVS URL copy_works.dest + ${source} DAVS URL copy_works + Curl Voms Push COPY Success ${dest} ${source} + ${overwriteHeader} Set variable --header "Overwrite: T" + ${rc} ${out} Curl Voms Push COPY Success ${dest} ${source} ${curl.opts.default} ${overwriteHeader} + Davix Get Success ${dest} ${davix.opts.voms} + [Teardown] Teardown file copy_works -Teardown copy file cross sa [Arguments] ${file_name} - Default Teardown - Remove Test File ${file_name} - Remove Test File ${file_name}.copied sa=${sa.oauth} +Copy override fails + [Tags] voms + [Setup] Setup file copy_works + ${dest} DAVS URL copy_works.dest + ${source} DAVS URL copy_works + Curl Voms Push COPY Success ${dest} ${source} + ${overwriteHeader} Set variable --header "Overwrite: F" + ${rc} ${out} Curl Voms Push COPY Failure ${dest} ${source} ${curl.opts.default} ${overwriteHeader} + Should Contain ${out} 412 Precondition Failed + [Teardown] Teardown file copy_works -*** Test cases *** +Copy not existent resource + [Tags] voms + [Setup] Default Setup + ${dest} DAVS URL copy_works.dest + ${source} DAVS URL copy_works + ${rc} ${out} Curl Voms Push COPY Failure ${dest} ${source} + Should Contain ${out} 404 Not Found + [Teardown] Default Teardown -Local copy works +Copy with destination equal to source [Tags] voms - [Setup] Setup copy file copy_works - ${dest} DAVS URL copy_works.copied + [Setup] Setup file copy_works + ${dest} DAVS URL copy_works ${source} DAVS URL copy_works - ${rc} ${out} Curl Voms Push COPY Success ${dest} ${source} - Davix Get Success ${dest} ${davix.opts.voms} - [Teardown] Teardown copy file copy_works + ${overwriteHeader} Set variable --header "Overwrite: T" + ${rc} ${out} Curl Voms Push COPY Failure ${dest} ${source} ${curl.opts.default} ${overwriteHeader} + Should Contain ${out} 403 + [Teardown] Teardown file copy_works -Local copy across storage areas fails +Copy across storage areas fails [Tags] voms - [Setup] Setup copy file copy_x_sa_works - ${dest} DAVS URL copy_x_sa_works.copied sa=${sa.oauth} + [Setup] Setup file copy_x_sa_works + ${dest} DAVS URL copy_x_sa_works.dest sa=${sa.oauth} ${source} DAVS URL copy_x_sa_works ${rc} ${out} Curl Voms Push COPY ${dest} ${source} Should Contain ${out} 400 Should Contain ${out} Local copy across storage areas is not supported - [Teardown] Teardown copy file cross sa copy_x_sa_works + [Teardown] Teardown file cross sa copy_x_sa_works \ No newline at end of file diff --git a/robot/test/delete.robot b/robot/test/delete.robot new file mode 100644 index 00000000..de0111cd --- /dev/null +++ b/robot/test/delete.robot @@ -0,0 +1,51 @@ +*** Settings *** +Resource common/storage_areas.robot +Resource common/credentials.robot +Resource common/davix.robot +Resource common/curl.robot +Resource common/setup_and_teardown.robot +Resource test/variables.robot + +Test Setup Default Setup +Test Teardown Default Teardown + +Default Tags delete + + +*** Test cases *** + +Delete works + [Tags] voms + [Setup] Setup file delete_works + ${url} DAVS URL delete_works + ${rc} ${out} Curl Voms DELETE Success ${url} + ${rc} ${out} Davix Get Failure ${url} ${davix.opts.voms} + Should Contain ${out} 404 + [Teardown] Teardown file delete_works + +Delete directory works + [Tags] voms + [Setup] Setup directory delete_works + ${url} DAVS URL delete_works + ${rc} ${out} Curl Voms DELETE Success ${url} + ${rc} ${out} Davix Get Failure ${url} ${davix.opts.voms} + Should Contain ${out} 404 + [Teardown] Teardown directory delete_works + +Delete not empty directory fails + [Documentation] Since v1.3.1 removing not empty directories is not allowed + [Tags] voms + [Setup] Run Keywords Setup directory delete_works + ... AND Create Test File delete_works/file_delete_works + ${url} DAVS URL delete_works + ${rc} ${out} Curl Voms DELETE Failure ${url} + Should Contain ${out} 412 Precondition Failed + [Teardown] Teardown directory delete_works + +Delete not existent resource + [Tags] voms + [Setup] Default Setup + ${url} DAVS URL delete_works + ${rc} ${out} Curl Voms DELETE Failure ${url} + Should Contain ${out} 404 Not Found + [Teardown] Default Teardown \ No newline at end of file diff --git a/robot/test/get.robot b/robot/test/get.robot new file mode 100644 index 00000000..e489d78c --- /dev/null +++ b/robot/test/get.robot @@ -0,0 +1,56 @@ +*** Settings *** +Resource common/storage_areas.robot +Resource common/credentials.robot +Resource common/davix.robot +Resource common/curl.robot +Resource common/setup_and_teardown.robot +Resource test/variables.robot + +Test Setup Default Setup +Test Teardown Default Teardown + +Default Tags get + + +*** Test cases *** + +Get works + [Tags] voms + [Setup] Setup file get_works + ${url} DAVS URL get_works + ${rc} ${out} Curl Voms GET Success ${url} + Should Contain ${out} Hello World! + Davix Get Success ${url} ${davix.opts.voms} + [Teardown] Teardown file get_works + +Get directory works + [Tags] voms + [Setup] Setup directory get_works + ${url} DAVS URL get_works + ${rc} ${out} Curl Voms GET Success ${url} + Davix Get Success ${url} ${davix.opts.voms} + [Teardown] Teardown directory get_works + +Get not empty directory works + [Tags] voms + [Setup] Run Keywords Setup directory get_works + ... AND Create Test File get_works/file_get_works + ${url} DAVS URL get_works + ${rc} ${out} Curl Voms GET Success ${url} + Davix Get Success ${url} ${davix.opts.voms} + [Teardown] Teardown directory get_works + +Get root directory works + [Tags] voms + [Setup] Default Setup + ${url} Set Variable ${davs.endpoint}/${sa.default} + ${rc} ${out} Curl Voms GET Success ${url} + Davix Get Success ${url} ${davix.opts.voms} + [Teardown] Default Teardown + +Get not existent resource + [Tags] voms + ${rc} ${out} Davix Get Failure ${davs.endpoint}/${sa.default}/does_not_exist + Should Contain ${out} 404 + ${rc} ${out} Davix Get Failure ${davs.endpoint}/${sa.default}/does_not_exist/also + Should Contain ${out} 404 \ No newline at end of file diff --git a/robot/test/head.robot b/robot/test/head.robot new file mode 100644 index 00000000..f1a0d8e5 --- /dev/null +++ b/robot/test/head.robot @@ -0,0 +1,65 @@ +*** Settings *** +Resource common/storage_areas.robot +Resource common/credentials.robot +Resource common/davix.robot +Resource common/curl.robot +Resource common/setup_and_teardown.robot +Resource test/variables.robot + +Test Setup Default Setup +Test Teardown Default Teardown + +Default Tags head + + +*** Test cases *** + +Head works + [Tags] voms + [Setup] Setup file head_works + ${url} DAVS URL head_works + ${rc} ${out} Curl Voms HEAD Success ${url} + Should Contain ${out} Content-Length: 12 + [Teardown] Teardown file head_works + +Head directory works + [Tags] voms + [Setup] Setup directory head_works + ${url} DAVS URL head_works + ${rc} ${out} Curl Voms HEAD Success ${url} + Should Contain ${out} Content-Length: 4096 + [Teardown] Teardown directory head_works + +Head not empty directory works + [Tags] voms + [Setup] Run Keywords Setup directory head_works + ... AND Create Test File head_works/file_head_works some-text + ${url} DAVS URL head_works + ${rc} ${out} Curl Voms HEAD Success ${url} + Should Contain ${out} Content-Length: 4096 + ${rc} ${out} Curl Voms HEAD Success ${url}/file_head_works + Should Contain ${out} Content-Length: 9 + [Teardown] Teardown directory head_works + +Head root directory works + [Tags] voms + [Setup] Default Setup + ${url} Set Variable ${davs.endpoint}/${sa.default} + ${rc} ${out} Curl Voms HEAD Success ${url} + Should Contain ${out} Content-Length: 4096 + [Teardown] Default Teardown + +Head not existent resource + [Tags] voms + ${rc} ${out} Curl Voms HEAD Failure ${davs.endpoint}/${sa.default}/does_not_exist + Should Contain ${out} 404 + ${rc} ${out} Curl Voms HEAD Failure ${davs.endpoint}/${sa.default}/does_not_exist/also + Should Contain ${out} 404 + +Head works on large files + [Tags] voms + [Setup] Run Keywords Default setup + ... AND Create Test File With Size hwlf 2G + ${rc} ${out} Curl Voms HEAD Success ${davs.endpoint}/${sa.default}/hwlf + Should Contain ${out} ength: 2147483648 + [Teardown] Teardown file hwlf \ No newline at end of file diff --git a/robot/test/mkcol.robot b/robot/test/mkcol.robot new file mode 100644 index 00000000..bf744f52 --- /dev/null +++ b/robot/test/mkcol.robot @@ -0,0 +1,38 @@ +*** Settings *** +Resource common/storage_areas.robot +Resource common/credentials.robot +Resource common/davix.robot +Resource common/curl.robot +Resource common/setup_and_teardown.robot +Resource test/variables.robot + +Test Setup Default Setup +Test Teardown Default Teardown + +Default Tags mkcol + + +*** Test cases *** + +Mkcol works + [Tags] voms + ${url} DAVS URL mkcol_works + Curl Voms MKCOL Success ${url} + ${rc} ${out} Curl Voms HEAD Success ${url} + Should Contain ${out} Content-Length: 4096 + [Teardown] Teardown directory mkcol_works + +Mkcol with missing parent + [Tags] voms + ${url} DAVS URL missing-dir/mkcol_works + ${rc} ${out} Curl Voms MKCOL Failure ${url} + Should Contain ${out} 409 Conflict + [Teardown] Teardown directory missing-dir + +Mkcol on existent resource + [Tags] voms + [Setup] Setup directory mkcol_works + ${url} DAVS URL mkcol_works + ${rc} ${out} Curl Voms MKCOL Failure ${url} + Should Contain ${out} 405 Method Not Allowed + [Teardown] Teardown directory mkcol_works \ No newline at end of file diff --git a/robot/test/move.robot b/robot/test/move.robot index 9cf2f989..de7a2b94 100644 --- a/robot/test/move.robot +++ b/robot/test/move.robot @@ -3,6 +3,7 @@ Resource common/storage_areas.robot Resource common/credentials.robot Resource common/davix.robot Resource common/curl.robot +Resource common/setup_and_teardown.robot Resource test/variables.robot Test Setup Default Setup @@ -10,45 +11,84 @@ Test Teardown Default Teardown Default Tags move -*** Keywords *** -Default Setup - Default VOMS credential +*** Test cases *** + +Move works + [Tags] voms + [Setup] Setup file move_works + ${dest} DAVS URL move_works.dest + ${source} DAVS URL move_works + ${rc} ${out} Curl Voms MOVE Success ${dest} ${source} + Davix Get Success ${dest} ${davix.opts.voms} + [Teardown] Teardown file move_works -Default Teardown - Unset VOMS credential +Move directory works + [Tags] voms + [Setup] Setup directory move_works + ${dest} DAVS URL move_works.dest + ${source} DAVS URL move_works + ${rc} ${out} Curl Voms MOVE Success ${dest} ${source} + Davix Get Success ${dest} ${davix.opts.voms} + [Teardown] Teardown directory move_works -Setup move file [Arguments] ${file_name} - Default Setup - Create Test File ${file_name} content=Hello World! +Move not empty directory works + [Tags] voms + [Setup] Run Keywords Setup directory move_works + ... AND Create Test File move_works/file_move_works + ${dest} DAVS URL move_works.dest + ${source} DAVS URL move_works + ${rc} ${out} Curl Voms MOVE Success ${dest} ${source} + Davix Get Success ${dest} ${davix.opts.voms} + [Teardown] Teardown directory move_works -Teardown move file [Arguments] ${file_name} - Default Teardown - Remove Test File ${file_name} - Remove Test File ${file_name}.moved +Move override works + [Tags] voms + [Setup] Setup file move_works + ${dest} DAVS URL move_works.dest + ${source} DAVS URL move_works + Curl Voms Push COPY Success ${dest} ${source} + ${overwriteHeader} Set variable --header "Overwrite: T" + ${rc} ${out} Curl Voms MOVE Success ${dest} ${source} ${curl.opts.default} ${overwriteHeader} + Davix Get Success ${dest} ${davix.opts.voms} + [Teardown] Teardown file move_works -Teardown move file cross sa [Arguments] ${file_name} - Default Teardown - Remove Test File ${file_name} - Remove Test File ${file_name}.moved sa=${sa.oauth} +Move override fails + [Tags] voms + [Setup] Setup file move_works + ${dest} DAVS URL move_works.dest + ${source} DAVS URL move_works + Curl Voms Push COPY Success ${dest} ${source} + ${overwriteHeader} Set variable --header "Overwrite: F" + ${rc} ${out} Curl Voms MOVE Failure ${dest} ${source} ${curl.opts.default} ${overwriteHeader} + Should Contain ${out} 412 Precondition Failed + [Teardown] Teardown file move_works -*** Test cases *** +Move not existent resource + [Tags] voms + [Setup] Default Setup + ${dest} DAVS URL move_works.dest + ${source} DAVS URL move_works + ${rc} ${out} Curl Voms MOVE Failure ${dest} ${source} + Should Contain ${out} 404 Not Found + [Teardown] Default Teardown -Move works - [Tags] voms move - [Setup] Setup move file move_works - ${dest} DAVS URL move_works.moved +Move with destination equal to source + [Tags] voms + [Setup] Setup file move_works + ${dest} DAVS URL move_works ${source} DAVS URL move_works - ${rc} ${out} Curl Voms MOVE Success ${dest} ${source} - Davix Get Success ${dest} ${davix.opts.voms} - [Teardown] Teardown move file move_works + ${overwriteHeader} Set variable --header "Overwrite: T" + ${rc} ${out} Curl Voms MOVE Failure ${dest} ${source} ${curl.opts.default} ${overwriteHeader} + Should Contain ${out} 403 + [Teardown] Teardown file move_works Move across storage areas fails - [Tags] voms move - [Setup] Setup move file move_x_sa_works - ${dest} DAVS URL move_x_sa_works.moved sa=${sa.oauth} + [Tags] voms + [Setup] Setup file move_x_sa_works + ${dest} DAVS URL move_x_sa_works.dest sa=${sa.oauth} ${source} DAVS URL move_works ${rc} ${out} Curl Voms MOVE ${dest} ${source} Should Contain ${out} 400 Should Contain ${out} Move across storage areas is not supported - [Teardown] Teardown move file cross sa move_x_sa_works + [Teardown] Teardown file cross sa move_x_sa_works diff --git a/robot/test/oauth.robot b/robot/test/oauth.robot index ef64b75c..a3dd9553 100644 --- a/robot/test/oauth.robot +++ b/robot/test/oauth.robot @@ -8,9 +8,6 @@ Resource test/variables.robot Test Setup Default Setup Test Teardown Default Teardown -*** Keywords *** -OAuth Get Works Setup - *** Test cases *** @@ -27,5 +24,5 @@ OAuth Put works ${url} DAVS URL oauth_put_test ${sa.oauth} Davix Put Success ${TEMPDIR}/oauth_put_test ${url} ${davix.opts.oauth} Davix Get Success ${url} ${davix.opts.oauth} - Remove File oauth_put_test + Remove Test File oauth_put_test ${sa.oauth} [Teardown] Remove Temporary File oauth_put_test \ No newline at end of file diff --git a/robot/test/options.robot b/robot/test/options.robot new file mode 100644 index 00000000..b984c409 --- /dev/null +++ b/robot/test/options.robot @@ -0,0 +1,39 @@ +*** Settings *** +Resource common/storage_areas.robot +Resource common/credentials.robot +Resource common/davix.robot +Resource common/curl.robot +Resource common/setup_and_teardown.robot +Resource test/variables.robot + +Test Setup Default Setup +Test Teardown Default Teardown + +Default Tags options + + +*** Test cases *** + +Options on storage area root works + [Tags] voms + [Setup] Default Setup + ${url} Set Variable ${davs.endpoint}/${sa.default} + ${rc} ${out} Curl Voms OPTIONS ${url} + Should Be Equal As Integers ${rc} 0 + [Teardown] Default Teardown + +Options on file works + [Tags] voms + [Setup] Setup file option_works + ${url} DAVS URL option_works + ${rc} ${out} Curl Voms OPTIONS ${url} + Should Be Equal As Integers ${rc} 0 + [Teardown] Teardown file option_works + +Options on directory works + [Tags] voms + [Setup] Setup directory option_works + ${url} DAVS URL option_works + ${rc} ${out} Curl Voms OPTIONS ${url} + Should Be Equal As Integers ${rc} 0 + [Teardown] Teardown directory option_works \ No newline at end of file diff --git a/robot/test/partial_transfer.robot b/robot/test/partial_transfer.robot new file mode 100644 index 00000000..0f1642ce --- /dev/null +++ b/robot/test/partial_transfer.robot @@ -0,0 +1,93 @@ +*** Settings *** +Resource common/storage_areas.robot +Resource common/credentials.robot +Resource common/davix.robot +Resource common/curl.robot +Resource common/setup_and_teardown.robot +Resource test/variables.robot + +Test Setup Default Setup +Test Teardown Default Teardown + +Default Tags partial + +*** Keywords *** + +Partial Put Setup + Default Setup + Create Temporary File pput0_test 0000000000 + Create Temporary File pput1_test 1111111111 + +Partial Put Teardown + Default Teardown + Remove Test File pput_test + Remove Temporary File pput0_test + Remove Temporary File pput1_test + +*** Test cases *** + +Partial Get works + [Tags] voms get + [Setup] Setup file partial_works test123456789 + ${url} DAVS URL partial_works + ${rc} ${out} Curl Voms Get Success ${url} ${curl.opts.default} -H "Range: 0-3" + Should Contain ${out} test + Should Not Contain ${out} 123456789 + Should Contain ${out} Content-Length: 4 + ${rc} ${out} Curl Voms Get Success ${url} ${curl.opts.default} -H "Range: 4-7" + Should Contain ${out} 1234 + Should Contain ${out} Content-Length: 4 + ${rc} ${out} Curl Voms Get Success ${url} ${curl.opts.default} -H "Range: 9-12" + Should Contain ${out} 6789 + Should Contain ${out} Content-Length: 4 + [Teardown] Teardown file partial_works + +Partial Get with multiple range + [Tags] voms get + [Setup] Setup file partial_works test123456789 + ${url} DAVS URL partial_works + ${rc} ${out} Curl Voms Get Success ${url} ${curl.opts.default} -H "Range: 1-3,5-7,10-11" + Should Contain ${out} Content-Range: bytes 1-3/13 + Should Contain ${out} est + Should Contain ${out} Content-Range: bytes 5-7/13 + Should Contain ${out} 234 + Should Contain ${out} Content-Range: bytes 10-11/13 + Should Contain ${out} 78 + [Teardown] Teardown file partial_works + +Partial Get not entirely on range + [Tags] voms get + [Setup] Setup file partial_works test123456789 + ${url} DAVS URL partial_works + ${rc} ${out} Curl Voms Get Success ${url} ${curl.opts.default} -H "Range: 11-13" + Should Contain ${out} Content-Range: bytes 11-12/13 + Should Contain ${out} 89 + Should Contain ${out} Content-Length: 2 + [Teardown] Teardown file partial_works + +Partial Get out of range + [Tags] voms get + [Setup] Setup file partial_works test123456789 + ${url} DAVS URL partial_works + ${rc} ${out} Curl Voms Get Failure ${url} ${curl.opts.default} -H "Range: 20-24" + Should Match Regexp ${out} 416 Requested Range Not Satisfiable|416 Range Not Satisfiable + [Teardown] Teardown file partial_works + +Partial Get out in one of multiple range + [Tags] voms get + [Setup] Setup file partial_works test123456789 + ${url} DAVS URL partial_works + ${rc} ${out} Curl Voms Get Success ${url} ${curl.opts.default} -H "Range: 1-3,20-24" + Should Contain ${out} Content-Range: bytes 1-3/13 + Should Contain ${out} est + Should Contain ${out} Content-Length: 3 + [Teardown] Teardown file partial_works + +Partial Put works + [Tags] voms put + [Setup] Partial Put Setup + ${opts} Set Variable -H "Content-Range: bytes=0-3/*" ${curl.opts.default} + ${dest} DAVS Url pput_test + ${rc} ${out} Curl Voms Put Success ${TEMPDIR}/pput0_test ${dest} + ${rc} ${out} Curl Voms Put Success ${TEMPDIR}/pput1_test ${dest} ${opts} + [Teardown] Partial Put Teardown \ No newline at end of file diff --git a/robot/test/propfind.robot b/robot/test/propfind.robot new file mode 100644 index 00000000..113aad9a --- /dev/null +++ b/robot/test/propfind.robot @@ -0,0 +1,74 @@ +*** Settings *** +Resource common/storage_areas.robot +Resource common/credentials.robot +Resource common/davix.robot +Resource common/curl.robot +Resource common/setup_and_teardown.robot +Resource test/variables.robot + +Test Setup Default Setup +Test Teardown Default Teardown + +Default Tags propfind + +*** Keywords *** + +Get PROPFIND ALLPROP body + ${output} Set variable "" + [Return] ${output} + +Get PROPFIND PROPNAME body + ${output} Set variable "" + [Return] ${output} + +Get PROPFIND PROP body [Arguments] ${propname} + ${output} Set variable "" + [Return] ${output} + + +*** Test cases *** + +Propfind allprop works + [Tags] voms + [Setup] Setup file propfind_works + ${url} DAVS URL propfind_works + ${body} Get PROPFIND ALLPROP body + ${rc} ${out} Curl Voms PROPFIND ${url} ${body} + Should Contain ${out} + Should Contain ${out} FALSE + Should Contain ${out} propfind_works + Should Contain ${out} HTTP/1.1 200 OK + Should Contain ${out} 12 + [Teardown] Teardown file propfind_works + +Propfind allprop not empty directory works + [Tags] voms + [Setup] Run Keywords Setup directory propfind_works + ... AND Create Test File propfind_works/file_propfind_works + ${url} DAVS URL propfind_works + ${body} Get PROPFIND ALLPROP body + ${rc} ${out} Curl Voms PROPFIND ${url} ${body} + Should Contain ${out} + Should Contain ${out} FALSEfile_propfind_works + Should Contain ${out} TRUEpropfind_works + Should Contain ${out} HTTP/1.1 200 OK + Should Contain ${out} 0 + [Teardown] Teardown directory propfind_works + +Propfind propname works + [Tags] voms + [Setup] Setup file propfind_works + ${url} DAVS URL propfind_works + ${body} Get PROPFIND PROPNAME body + ${rc} ${out} Curl Voms PROPFIND ${url} ${body} + Should Contain ${out} propfind_works + [Teardown] Teardown file propfind_works + +Propfind status property works + [Tags] voms + [Setup] Setup file propfind_works + ${url} DAVS URL propfind_works + ${body} Get PROPFIND PROP body status + ${rc} ${out} Curl Voms PROPFIND ${url} ${body} + Should Contain ${out} HTTP/1.1 200 OK + [Teardown] Teardown file propfind_works \ No newline at end of file diff --git a/robot/test/put.robot b/robot/test/put.robot new file mode 100644 index 00000000..b04cbb39 --- /dev/null +++ b/robot/test/put.robot @@ -0,0 +1,67 @@ +*** Settings *** +Resource common/storage_areas.robot +Resource common/credentials.robot +Resource common/davix.robot +Resource common/curl.robot +Resource common/setup_and_teardown.robot +Resource test/variables.robot + +Test Setup Default Setup +Test Teardown Default Teardown + +Default Tags put + +*** Keywords *** + +Put Setup [Arguments] ${file_name} + Default Setup + Create Temporary File ${file_name} 123456789 + +Put Teardown [Arguments] ${file_name} + Default Teardown + Remove Temporary File ${file_name} + Remove Test File ${file_name} + +Put directory Teardown [Arguments] ${file_name} ${directory_name}=${file_name} + Default Teardown + Remove Temporary File ${file_name} + Remove Test Directory ${directory_name} + +*** Test cases *** + +Put works + [Tags] voms + [Setup] Put Setup put_works + ${url} DAVS URL put_works + ${rc} ${out} Curl Voms PUT Success ${TEMPDIR}/put_works ${url} + Should Contain ${out} 201 Created + ${rc} ${out} Curl Voms Get Success ${url} + Should Contain ${out} 123456789 + [Teardown] Put Teardown put_works + +Put override works + [Tags] voms + [Setup] Put Setup put_works + ${url} DAVS URL put_works + Curl Voms PUT Success ${TEMPDIR}/put_works ${url} + ${rc} ${out} Curl Voms PUT Success ${TEMPDIR}/put_works ${url} + Should Contain ${out} 204 No Content + [Teardown] Put Teardown put_works + +Put with missing parent works + [Tags] voms + [Setup] Put Setup put_works + ${url} DAVS URL put-directory/put_works + ${rc} ${out} Curl Voms PUT Success ${TEMPDIR}/put_works ${url} + Should Contain ${out} 201 Created + [Teardown] Run Keywords Put Teardown put_works + ... AND Remove Test Directory put-directory + +Put over directory not allowed + [Tags] voms known-issue + [Setup] Run Keywords Setup directory put_works + ... AND Put Setup put_works + ${url} DAVS URL put_works + ${rc} ${out} Curl Voms PUT Failure ${TEMPDIR}/put_works ${url} + Should Contain ${out} 405 Method not allowed + [Teardown] Put directory Teardown put_works \ No newline at end of file diff --git a/robot/test/token_request.robot b/robot/test/token_request.robot index 269416cf..c3d15d31 100644 --- a/robot/test/token_request.robot +++ b/robot/test/token_request.robot @@ -77,7 +77,7 @@ Put works with locally issued token ${opts} Set variable -H "Authorization: Bearer ${token}" --capath /etc/grid-security/certificates Davix Put Success ${TEMPDIR}/token_put_test ${url} ${opts} Davix Get Success ${url} ${opts} - Remove file token_put_test + Remove Test File token_put_test [Teardown] Put works with locally issued token Teardown Get works with locally issued token fga @@ -97,5 +97,5 @@ Put works with locally issued token fga ${opts} Set variable -H "Authorization: Bearer ${token}" --capath /etc/grid-security/certificates Davix Put Success ${TEMPDIR}/token_put_test ${url} ${opts} Davix Get Success ${url} ${opts} - Remove file token_put_test + Remove Test File token_put_test ${sa.fga} [Teardown] Put works with locally issued token fga Teardown diff --git a/robot/test/tpc.robot b/robot/test/tpc.robot index 601f94fe..658b56ce 100644 --- a/robot/test/tpc.robot +++ b/robot/test/tpc.robot @@ -90,7 +90,7 @@ Pull copy works [Teardown] Pull copy works Teardown Pull copy works https - [Tags] voms tpc dbg + [Tags] voms tpc [Setup] Pull copy works https Setup ${dest} DAVS URL tpc_test_https ${src} Remote DAVS URL tpc_test_https sa=${sa.noauth} @@ -120,7 +120,7 @@ Pull copy works oauth and https [Teardown] Pull copy works oauth and https Teardown Push copy works - [Tags] voms oauth tpc push kk + [Tags] voms oauth tpc push [Setup] Push copy works Setup ${dst} Remote DAVS URL tpc_test_push sa=${sa.oauth} ${src} DAVS URL tpc_test_push @@ -131,7 +131,7 @@ Push copy works [Teardown] Push copy works Teardown Oauth pull copy works - [Tags] oauth tpc pull maghe987 + [Tags] oauth tpc pull [Setup] Oauth pull copy works Setup ${src} Remote DAVS URL oauth_pull_copy_works sa=${sa.oauth} ${dst} DAVS URL oauth_pull_copy_works.copy sa=${sa.oauth} diff --git a/src/main/java/org/italiangrid/storm/webdav/authn/AuthenticationUtils.java b/src/main/java/org/italiangrid/storm/webdav/authn/AuthenticationUtils.java index 0cd57a82..ae4b373b 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authn/AuthenticationUtils.java +++ b/src/main/java/org/italiangrid/storm/webdav/authn/AuthenticationUtils.java @@ -15,10 +15,7 @@ */ package org.italiangrid.storm.webdav.authn; -import static java.util.Objects.isNull; - import java.util.Map; -import java.util.Objects; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; @@ -33,7 +30,7 @@ private AuthenticationUtils() { } public static String getPalatableSubject(Authentication authn) { - if (Objects.isNull(authn) || authn instanceof AnonymousAuthenticationToken) { + if (authn == null || authn instanceof AnonymousAuthenticationToken) { return "Anonymous user"; } else if (authn instanceof OAuth2AuthenticationToken) { OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) authn; @@ -41,7 +38,7 @@ public static String getPalatableSubject(Authentication authn) { String subjectIssuer = String.format("%s @ %s", attributes.get("sub"), attributes.get("iss")); - if (!isNull(attributes.get("name"))) { + if (attributes.get("name") != null) { return (String) attributes.get("name"); } diff --git a/src/main/java/org/italiangrid/storm/webdav/authn/ErrorPageAuthenticationEntryPoint.java b/src/main/java/org/italiangrid/storm/webdav/authn/ErrorPageAuthenticationEntryPoint.java index 7f869a1c..80216572 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authn/ErrorPageAuthenticationEntryPoint.java +++ b/src/main/java/org/italiangrid/storm/webdav/authn/ErrorPageAuthenticationEntryPoint.java @@ -35,7 +35,7 @@ public class ErrorPageAuthenticationEntryPoint implements AuthenticationEntryPoint { - final String errorPage = "/errors/401"; + static final String ERROR_PAGE = "/errors/401"; @Override public void commence(HttpServletRequest request, HttpServletResponse response, @@ -73,7 +73,7 @@ public void commence(HttpServletRequest request, HttpServletResponse response, request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, authException); - RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage); + RequestDispatcher dispatcher = request.getRequestDispatcher(ERROR_PAGE); dispatcher.forward(request, response); } } diff --git a/src/main/java/org/italiangrid/storm/webdav/authn/PrincipalHelper.java b/src/main/java/org/italiangrid/storm/webdav/authn/PrincipalHelper.java index 41cb1cc4..1b2427dc 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authn/PrincipalHelper.java +++ b/src/main/java/org/italiangrid/storm/webdav/authn/PrincipalHelper.java @@ -18,7 +18,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Map; -import java.util.Objects; import java.util.Optional; import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties; @@ -43,14 +42,12 @@ public PrincipalHelper(ServiceConfigurationProperties config) throws MalformedUR } public String getPrincipalAsString(Authentication authn) { - if (Objects.isNull(authn) || authn instanceof AnonymousAuthenticationToken) { - return "anonymous"; + if (authn == null || authn instanceof AnonymousAuthenticationToken) { + return ANONYMOUS; } else if (authn instanceof OAuth2AuthenticationToken) { OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) authn; Map attributes = authToken.getPrincipal().getAttributes(); - String subjectIssuer = String.format("%s@%s", attributes.get("sub"), attributes.get("iss")); - return subjectIssuer; - + return String.format("%s@%s", attributes.get("sub"), attributes.get("iss")); } else if (authn instanceof PreAuthenticatedAuthenticationToken) { return authn.getName(); } else if (authn instanceof JwtAuthenticationToken) { diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/VOMSAuthenticationFilter.java b/src/main/java/org/italiangrid/storm/webdav/authz/VOMSAuthenticationFilter.java index d6ea1680..843424ec 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/VOMSAuthenticationFilter.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/VOMSAuthenticationFilter.java @@ -50,9 +50,9 @@ protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { public Object extractPrincipal(X509Certificate cert) { - return cert.getSubjectDN().getName(); + return cert.getSubjectX500Principal().getName(); } - + @Override public boolean principalChanged(HttpServletRequest request, Authentication currentAuthentication) { diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/VOMSPolicyService.java b/src/main/java/org/italiangrid/storm/webdav/authz/VOMSPolicyService.java index 1081737d..af579caf 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/VOMSPolicyService.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/VOMSPolicyService.java @@ -15,7 +15,6 @@ */ package org.italiangrid.storm.webdav.authz; -import static java.util.Objects.isNull; import static org.italiangrid.storm.webdav.authz.SAPermission.canRead; import static org.italiangrid.storm.webdav.authz.SAPermission.canWrite; @@ -49,7 +48,7 @@ public VOMSPolicyService(StorageAreaConfiguration saConfig) { voMapPerms = ArrayListMultimap.create(); for (StorageAreaInfo sa : saConfig.getStorageAreaInfo()) { - if (!isNull(sa.vos())) { + if (sa.vos() != null) { for (String vo : sa.vos()) { voPerms.put(vo, canRead(sa.name())); voPerms.put(vo, canWrite(sa.name())); diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/VOMSPreAuthDetailsSource.java b/src/main/java/org/italiangrid/storm/webdav/authz/VOMSPreAuthDetailsSource.java index fa4c3cd9..8bd704fc 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/VOMSPreAuthDetailsSource.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/VOMSPreAuthDetailsSource.java @@ -109,7 +109,7 @@ protected Optional getSubjectAuthority(HttpServletRequest Optional chain = Utils.getCertificateChainFromRequest(request); if (chain.isPresent()) { return Optional.of(new X509SubjectAuthority( - ProxyUtils.getEndUserCertificate(chain.get()).getSubjectDN().getName())); + ProxyUtils.getEndUserCertificate(chain.get()).getSubjectX500Principal().getName())); } return Optional.empty(); diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/LocalAuthorizationPdp.java b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/LocalAuthorizationPdp.java index f762177a..03c294df 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/LocalAuthorizationPdp.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/LocalAuthorizationPdp.java @@ -25,6 +25,7 @@ import java.net.URL; import java.util.EnumSet; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; import javax.servlet.http.HttpServletRequest; @@ -42,8 +43,8 @@ public class LocalAuthorizationPdp implements PathAuthorizationPdp, TpcUtils { public static final Logger LOG = LoggerFactory.getLogger(LocalAuthorizationPdp.class); - public static final EnumSet READ_PERMS = EnumSet.of(Permission.r, Permission.rw); - public static final EnumSet WRITE_PERMS = EnumSet.of(Permission.w, Permission.rw); + private static final Set READ_PERMS = EnumSet.of(Permission.r, Permission.rw); + private static final Set WRITE_PERMS = EnumSet.of(Permission.w, Permission.rw); private final URL localAuthzServerIssuer; diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/PathAuthorizationPolicy.java b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/PathAuthorizationPolicy.java index e5a2efc1..ba29d611 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/PathAuthorizationPolicy.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/PathAuthorizationPolicy.java @@ -16,7 +16,6 @@ package org.italiangrid.storm.webdav.authz.pdp; import java.util.List; -import java.util.Objects; import java.util.UUID; import javax.servlet.http.HttpServletRequest; @@ -158,7 +157,7 @@ public Builder withPrincipalMatcher(PrincipalMatcher m) { } public PathAuthorizationPolicy build() { - if (Objects.isNull(id)) { + if (id == null) { id = UUID.randomUUID().toString(); } return new PathAuthorizationPolicy(this); diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/WlcgStructuredPathAuthorizationPdp.java b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/WlcgStructuredPathAuthorizationPdp.java index 054a0b87..6cb4d3ba 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/WlcgStructuredPathAuthorizationPdp.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/WlcgStructuredPathAuthorizationPdp.java @@ -16,7 +16,6 @@ package org.italiangrid.storm.webdav.authz.pdp; import static java.lang.String.format; -import static java.util.Objects.isNull; import static java.util.stream.Collectors.toList; import static org.italiangrid.storm.webdav.authz.pdp.PathAuthorizationResult.deny; import static org.italiangrid.storm.webdav.authz.pdp.PathAuthorizationResult.indeterminate; @@ -47,17 +46,23 @@ public class WlcgStructuredPathAuthorizationPdp implements PathAuthorizationPdp, MatcherUtils, TpcUtils { public static final String WLCG_STORAGE_SCOPE_PATTERN_STRING = - "^storage.(read|modify|create):(\\/.*)$"; + "^storage.(read|modify|create|stage):(\\/.*)$"; public static final Pattern WLCG_STORAGE_SCOPE_PATTERN = Pattern.compile(WLCG_STORAGE_SCOPE_PATTERN_STRING); public static final String SCOPE_CLAIM = "scope"; + public static final String STORAGE_STAGE = "storage.stage"; public static final String STORAGE_READ = "storage.read"; public static final String STORAGE_MODIFY = "storage.modify"; public static final String STORAGE_CREATE = "storage.create"; + protected static final Set READ_SCOPES = Sets.newHashSet(STORAGE_READ, STORAGE_STAGE); + protected static final Set WRITE_SCOPES = Sets.newHashSet(STORAGE_CREATE, STORAGE_MODIFY); + protected static final Set ALL_STORAGE_SCOPES = + Sets.newHashSet(STORAGE_READ, STORAGE_MODIFY, STORAGE_CREATE, STORAGE_STAGE); + public static final String ERROR_INVALID_AUTHENTICATION = "Invalid authentication: expected a JwtAuthenticationToken object"; public static final String ERROR_INVALID_TOKEN_NO_SCOPE = @@ -68,12 +73,10 @@ public class WlcgStructuredPathAuthorizationPdp public static final String ERROR_UNKNOWN_TOKEN_ISSUER = "Unknown token issuer: %s"; - public static final Set READONLY_METHODS = - Sets.newHashSet("GET", "OPTIONS", "HEAD", "PROPFIND"); - - public static final Set REPLACE_METHODS = Sets.newHashSet("PUT", "MKCOL"); - - public static final Set MODIFY_METHODS = Sets.newHashSet("PATCH", "DELETE"); + protected static final Set READONLY_METHODS = Sets.newHashSet("GET", "PROPFIND"); + protected static final Set REPLACE_METHODS = Sets.newHashSet("PUT", "MKCOL"); + protected static final Set MODIFY_METHODS = Sets.newHashSet("PATCH", "DELETE"); + protected static final Set CATCHALL_METHODS = Sets.newHashSet("HEAD", "OPTIONS"); public static final String COPY_METHOD = "COPY"; public static final String MOVE_METHOD = "MOVE"; @@ -125,39 +128,39 @@ public static Set resolveWlcgScopes(JwtAuthenticationToken token) { boolean filterMatcherByRequest(HttpServletRequest request, String method, StructuredPathScopeMatcher m, boolean requestedResourceExists) { - String requiredScope = null; + if (CATCHALL_METHODS.contains(method)) { + return ALL_STORAGE_SCOPES.stream().anyMatch(prefix -> prefix.equals(m.getPrefix())); + } if (READONLY_METHODS.contains(method)) { - requiredScope = STORAGE_READ; - } else if (REPLACE_METHODS.contains(method)) { + return READ_SCOPES.contains(m.getPrefix()); + } + if (REPLACE_METHODS.contains(method)) { if (requestedResourceExists) { - requiredScope = STORAGE_MODIFY; - } else { - requiredScope = STORAGE_CREATE; + return STORAGE_MODIFY.equals(m.getPrefix()); } - } else if (MODIFY_METHODS.contains(method)) { - requiredScope = STORAGE_MODIFY; - } else if (COPY_METHOD.equals(method)) { - - requiredScope = STORAGE_READ; + return WRITE_SCOPES.contains(m.getPrefix()); + } + if (MODIFY_METHODS.contains(method)) { + return STORAGE_MODIFY.equals(m.getPrefix()); + } + if (COPY_METHOD.equals(method)) { if (isPullTpc(request, localUrlService)) { if (requestedResourceExists) { - requiredScope = STORAGE_MODIFY; - } else { - requiredScope = STORAGE_CREATE; + return STORAGE_MODIFY.equals(m.getPrefix()); } + return WRITE_SCOPES.contains(m.getPrefix()); } + return READ_SCOPES.contains(m.getPrefix()); - } else if (MOVE_METHOD.equals(method)) { - requiredScope = STORAGE_MODIFY; } - if (isNull(requiredScope)) { - throw new IllegalArgumentException(format(ERROR_UNSUPPORTED_METHOD_PATTERN, method)); + if (MOVE_METHOD.equals(method)) { + return STORAGE_MODIFY.equals(m.getPrefix()); } - return m.getPrefix().equals(requiredScope); + throw new IllegalArgumentException(format(ERROR_UNSUPPORTED_METHOD_PATTERN, method)); } @@ -177,16 +180,16 @@ public PathAuthorizationResult authorizeRequest(PathAuthorizationRequest authzRe JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) authentication; - if (isNull(jwtAuth.getToken().getClaimAsString(SCOPE_CLAIM))) { + if (jwtAuth.getToken().getClaimAsString(SCOPE_CLAIM) == null) { return indeterminate(ERROR_INVALID_TOKEN_NO_SCOPE); } StorageAreaInfo sa = pathResolver.resolveStorageArea(requestPath); - if (isNull(sa)) { + if (sa == null) { return indeterminate(ERROR_SA_NOT_FOUND); } - + final String tokenIssuer = jwtAuth.getToken().getIssuer().toString(); if (!sa.orgs().contains(tokenIssuer)) { @@ -210,10 +213,17 @@ public PathAuthorizationResult authorizeRequest(PathAuthorizationRequest authzRe final boolean requestedResourceExists = pathResolver.pathExists(requestPath); final String saPath = getStorageAreaPath(requestPath, sa); - scopeMatchers = scopeMatchers.stream() - .filter(m -> filterMatcherByRequest(request, method, m, requestedResourceExists)) - .filter(m -> m.matchesPath(saPath)) - .collect(toList()); + if ("MKCOL".equals(method)) { + scopeMatchers = scopeMatchers.stream() + .filter(m -> filterMatcherByRequest(request, method, m, requestedResourceExists)) + .filter(m -> m.matchesPathIncludingParents(saPath)) + .collect(toList()); + } else { + scopeMatchers = scopeMatchers.stream() + .filter(m -> filterMatcherByRequest(request, method, m, requestedResourceExists)) + .filter(m -> m.matchesPath(saPath)) + .collect(toList()); + } if (scopeMatchers.isEmpty()) { return deny(ERROR_INSUFFICIENT_TOKEN_SCOPE); diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AnonymousUser.java b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AnonymousUser.java index dc13c395..74fbc843 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AnonymousUser.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AnonymousUser.java @@ -15,8 +15,6 @@ */ package org.italiangrid.storm.webdav.authz.pdp.principal; -import static java.util.Objects.isNull; - import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; @@ -24,7 +22,7 @@ public class AnonymousUser implements PrincipalMatcher { @Override public boolean matchesPrincipal(Authentication authentication) { - return isNull(authentication) || authentication instanceof AnonymousAuthenticationToken; + return authentication == null || authentication instanceof AnonymousAuthenticationToken; } @Override diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AnyAuthenticatedUser.java b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AnyAuthenticatedUser.java index 40d99d38..0e309924 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AnyAuthenticatedUser.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AnyAuthenticatedUser.java @@ -15,8 +15,6 @@ */ package org.italiangrid.storm.webdav.authz.pdp.principal; -import static java.util.Objects.isNull; - import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; @@ -24,7 +22,7 @@ public class AnyAuthenticatedUser implements PrincipalMatcher { @Override public boolean matchesPrincipal(Authentication authentication) { - return !isNull(authentication) && !(authentication instanceof AnonymousAuthenticationToken) + return authentication != null && !(authentication instanceof AnonymousAuthenticationToken) && authentication.isAuthenticated(); } diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AuthorityHolder.java b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AuthorityHolder.java index 4b572723..c71c7f3f 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AuthorityHolder.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/pdp/principal/AuthorityHolder.java @@ -15,8 +15,6 @@ */ package org.italiangrid.storm.webdav.authz.pdp.principal; -import java.util.Objects; - import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; @@ -39,7 +37,7 @@ public static AuthorityHolder fromAuthority(GrantedAuthority authority) { @Override public boolean matchesPrincipal(Authentication authentication) { - return !Objects.isNull(authentication) && authentication.getAuthorities() + return authentication != null && authentication.getAuthorities() .stream() .anyMatch(a -> a.getAuthority().equals(authority.getAuthority())); } diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/util/StructuredPathScopeMatcher.java b/src/main/java/org/italiangrid/storm/webdav/authz/util/StructuredPathScopeMatcher.java index 1172db03..42017870 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/util/StructuredPathScopeMatcher.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/util/StructuredPathScopeMatcher.java @@ -17,8 +17,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Strings.isNullOrEmpty; -import static java.util.Objects.nonNull; +import java.nio.file.Path; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -32,12 +32,12 @@ public class StructuredPathScopeMatcher implements ScopeMatcher { public static final Logger LOG = LoggerFactory.getLogger(StructuredPathScopeMatcher.class); private static final Pattern POINT_DIR_MATCHER = Pattern.compile("\\.\\./?"); - + private static final Character SEP = ':'; private static final String SEP_STR = SEP.toString(); private final String prefix; - private final String path; + private final Path path; private final Pattern prefixMatchPattern; private final Pattern pathMatchPattern; @@ -45,7 +45,7 @@ public class StructuredPathScopeMatcher implements ScopeMatcher { private StructuredPathScopeMatcher(String prefix, String path) { this.prefix = prefix; - this.path = path; + this.path = Path.of(path); final String prefixMatchRegexp = String.format("^%s%c", prefix, SEP); prefixMatchPattern = Pattern.compile(prefixMatchRegexp); @@ -54,28 +54,33 @@ private StructuredPathScopeMatcher(String prefix, String path) { @Override public boolean matchesScope(String scope) { - checkArgument(nonNull(scope), "scope must be non-null"); + checkArgument(scope != null, "scope must be non-null"); if (POINT_DIR_MATCHER.matcher(scope).find()) { throw new IllegalArgumentException("Scope contains relative path references"); } Matcher prefixMatcher = prefixMatchPattern.matcher(scope); - + boolean prefixMatches = prefixMatcher.find(); - + if (prefixMatches) { final String scopePath = scope.substring(prefix.length() + 1); - return pathMatchPattern.matcher(scopePath).matches(); + return pathMatchPattern.matcher(scopePath).matches(); } else { return false; } } - + public boolean matchesPath(String path) { return pathMatchPattern.matcher(path).matches(); } - + + public boolean matchesPathIncludingParents(String path) { + Path targetPath = Path.of(path); + return this.path.startsWith(targetPath) || matchesPath(path); + } + public static StructuredPathScopeMatcher fromString(String scope) { final int sepIndex = scope.indexOf(SEP); final String prefix = scope.substring(0, sepIndex); @@ -135,7 +140,7 @@ public String getPrefix() { } public String getPath() { - return path; + return path.toString(); } - + } diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/vomap/DefaultVOMembershipProvider.java b/src/main/java/org/italiangrid/storm/webdav/authz/vomap/DefaultVOMembershipProvider.java index 331b1d34..a4676653 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/vomap/DefaultVOMembershipProvider.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/vomap/DefaultVOMembershipProvider.java @@ -58,7 +58,6 @@ public boolean hasSubjectAsMember(String subject) { } finally { refreshLock.readLock().unlock(); - ; } } diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/vomap/MapfileVOMembershipSource.java b/src/main/java/org/italiangrid/storm/webdav/authz/vomap/MapfileVOMembershipSource.java index 54a3dce3..132eca24 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/vomap/MapfileVOMembershipSource.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/vomap/MapfileVOMembershipSource.java @@ -22,7 +22,6 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; -import java.util.HashSet; import java.util.List; import java.util.Set; @@ -32,6 +31,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.collect.Sets; + import eu.emi.security.authn.x509.impl.OpensslNameUtils; public class MapfileVOMembershipSource implements VOMembershipSource { @@ -64,12 +65,12 @@ private CSVParser getParser() { private boolean isValidCSVRecord(CSVRecord r) { if (r.size() > 3) { - logger.debug("Invalid CSVRecord: {}. Illegal size: {}", r, r.size()); + logger.warn("Invalid CSVRecord: {}. Illegal size: {}", r, r.size()); return false; } if (!r.get(0).startsWith("/")) { - logger.debug("Invalid CSVRecord: {}. Subject does not start with / : {}", + logger.warn("Invalid CSVRecord: {}. Subject does not start with / : {}", r, r.get(0)); return false; } @@ -82,7 +83,7 @@ public Set getVOMembers() { long startTime = System.currentTimeMillis(); - Set subjects = new HashSet(); + Set subjects = Sets.newHashSet(); CSVParser parser = getParser(); @@ -97,7 +98,8 @@ public Set getVOMembers() { } if (!isValidCSVRecord(r)) { - break; + /* Fix https://issues.infn.it/jira/browse/STOR-1399 */ + continue; } String subject = r.get(0); diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/vomap/VOMapDetailServiceBuilder.java b/src/main/java/org/italiangrid/storm/webdav/authz/vomap/VOMapDetailServiceBuilder.java index 5c5e4de2..e2657a8c 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/vomap/VOMapDetailServiceBuilder.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/vomap/VOMapDetailServiceBuilder.java @@ -18,7 +18,6 @@ import static java.util.Collections.emptySet; import java.io.File; -import java.io.FilenameFilter; import java.util.HashSet; import java.util.Set; @@ -76,15 +75,7 @@ public VOMapDetailsService build() { File configDir = new File(serviceConf.getVOMapFilesConfigDir()); directorySanityChecks(configDir); - File[] files = configDir.listFiles(new FilenameFilter() { - - @Override - public boolean accept(File dir, String name) { - - return name.endsWith(VOMAPFILE_SUFFIX); - - } - }); + File[] files = configDir.listFiles((dir, name) -> name.endsWith(VOMAPFILE_SUFFIX)); if (files.length == 0) { logger.warn("No mapfiles found in {}. Was looking for files ending in {}", configDir, @@ -94,7 +85,7 @@ public boolean accept(File dir, String name) { - Set providers = new HashSet(); + Set providers = new HashSet<>(); for (File f : files) { try { String voName = FilenameUtils.removeExtension(f.getName()); @@ -106,7 +97,6 @@ public boolean accept(File dir, String name) { } catch (Throwable t) { logger.error("Error parsing mapfile {}: {}", f.getAbsolutePath(), t.getMessage(), t); - continue; } } diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/voters/FineGrainedAuthzVoter.java b/src/main/java/org/italiangrid/storm/webdav/authz/voters/FineGrainedAuthzVoter.java index a4f2ed04..3b66ce2a 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/voters/FineGrainedAuthzVoter.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/voters/FineGrainedAuthzVoter.java @@ -15,7 +15,6 @@ */ package org.italiangrid.storm.webdav.authz.voters; -import static java.util.Objects.isNull; import static org.italiangrid.storm.webdav.authz.pdp.PathAuthorizationRequest.newAuthorizationRequest; import java.util.Collection; @@ -48,7 +47,7 @@ public int vote(Authentication authentication, FilterInvocation filter, final String requestPath = getRequestPath(filter.getHttpRequest()); StorageAreaInfo sa = resolver.resolveStorageArea(requestPath); - if (isNull(sa) || Boolean.FALSE.equals(sa.fineGrainedAuthzEnabled())) { + if (sa == null || !sa.fineGrainedAuthzEnabled()) { return ACCESS_ABSTAIN; } diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/voters/FineGrainedCopyMoveAuthzVoter.java b/src/main/java/org/italiangrid/storm/webdav/authz/voters/FineGrainedCopyMoveAuthzVoter.java index 087244c1..1de3c859 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/voters/FineGrainedCopyMoveAuthzVoter.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/voters/FineGrainedCopyMoveAuthzVoter.java @@ -15,7 +15,6 @@ */ package org.italiangrid.storm.webdav.authz.voters; -import static java.util.Objects.isNull; import static org.italiangrid.storm.webdav.server.servlet.WebDAVMethod.COPY; import static org.italiangrid.storm.webdav.server.servlet.WebDAVMethod.PUT; @@ -29,6 +28,7 @@ import org.italiangrid.storm.webdav.config.StorageAreaInfo; import org.italiangrid.storm.webdav.server.PathResolver; import org.italiangrid.storm.webdav.tpc.LocalURLService; +import org.italiangrid.storm.webdav.tpc.TransferConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.ConfigAttribute; @@ -52,7 +52,7 @@ public int vote(Authentication authentication, FilterInvocation filter, return ACCESS_ABSTAIN; } - String destination = filter.getRequest().getHeader(DESTINATION_HEADER); + String destination = filter.getRequest().getHeader(TransferConstants.DESTINATION_HEADER); if (destination == null) { return ACCESS_ABSTAIN; @@ -68,11 +68,11 @@ && requestHasRemoteDestinationHeader(filter.getRequest(), localUrlService)) { String destinationPath = getSanitizedPathFromUrl(destination); StorageAreaInfo sa = resolver.resolveStorageArea(destinationPath); - if (isNull(sa)) { + if (sa == null) { return ACCESS_ABSTAIN; } - if (Boolean.FALSE.equals(sa.fineGrainedAuthzEnabled())) { + if (!sa.fineGrainedAuthzEnabled()) { return ACCESS_ABSTAIN; } diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/voters/MacaroonAuthzVoter.java b/src/main/java/org/italiangrid/storm/webdav/authz/voters/MacaroonAuthzVoter.java new file mode 100644 index 00000000..c8c0a69b --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/authz/voters/MacaroonAuthzVoter.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.authz.voters; + +import java.util.Collection; + +import org.springframework.http.HttpMethod; +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.FilterInvocation; +import org.springframework.util.Assert; + +public class MacaroonAuthzVoter implements AccessDecisionVoter { + + @Override + public boolean supports(ConfigAttribute attribute) { + return true; + } + + @Override + public boolean supports(Class clazz) { + return true; + } + + @Override + public int vote(Authentication authentication, FilterInvocation filterInvocation, + Collection attributes) { + Assert.notNull(authentication, "authentication must not be null"); + Assert.notNull(filterInvocation, "filterInvocation must not be null"); + + if (HttpMethod.POST.name().equals(filterInvocation.getHttpRequest().getMethod())) { + return ACCESS_GRANTED; + } + return ACCESS_ABSTAIN; + } + +} diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/voters/WlcgScopeAuthzCopyMoveVoter.java b/src/main/java/org/italiangrid/storm/webdav/authz/voters/WlcgScopeAuthzCopyMoveVoter.java index 2017c71f..f1714411 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/voters/WlcgScopeAuthzCopyMoveVoter.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/voters/WlcgScopeAuthzCopyMoveVoter.java @@ -15,7 +15,6 @@ */ package org.italiangrid.storm.webdav.authz.voters; -import static java.util.Objects.isNull; import static org.italiangrid.storm.webdav.server.servlet.WebDAVMethod.COPY; import static org.italiangrid.storm.webdav.server.servlet.WebDAVMethod.PUT; @@ -29,6 +28,7 @@ import org.italiangrid.storm.webdav.config.StorageAreaInfo; import org.italiangrid.storm.webdav.server.PathResolver; import org.italiangrid.storm.webdav.tpc.LocalURLService; +import org.italiangrid.storm.webdav.tpc.TransferConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.ConfigAttribute; @@ -58,7 +58,7 @@ public int vote(Authentication authentication, FilterInvocation filter, return ACCESS_ABSTAIN; } - String destination = filter.getRequest().getHeader(DESTINATION_HEADER); + String destination = filter.getRequest().getHeader(TransferConstants.DESTINATION_HEADER); if (destination == null) { return ACCESS_ABSTAIN; @@ -74,11 +74,11 @@ && requestHasRemoteDestinationHeader(filter.getRequest(), localUrlService)) { String destinationPath = getSanitizedPathFromUrl(destination); StorageAreaInfo sa = resolver.resolveStorageArea(destinationPath); - if (isNull(sa)) { + if (sa == null) { return ACCESS_ABSTAIN; } - if (Boolean.FALSE.equals(sa.wlcgScopeAuthzEnabled())) { + if (!sa.wlcgScopeAuthzEnabled()) { return ACCESS_ABSTAIN; } diff --git a/src/main/java/org/italiangrid/storm/webdav/authz/voters/WlcgScopeAuthzVoter.java b/src/main/java/org/italiangrid/storm/webdav/authz/voters/WlcgScopeAuthzVoter.java index bf728ba8..5da7658a 100644 --- a/src/main/java/org/italiangrid/storm/webdav/authz/voters/WlcgScopeAuthzVoter.java +++ b/src/main/java/org/italiangrid/storm/webdav/authz/voters/WlcgScopeAuthzVoter.java @@ -15,7 +15,6 @@ */ package org.italiangrid.storm.webdav.authz.voters; -import static java.util.Objects.isNull; import static org.italiangrid.storm.webdav.authz.pdp.PathAuthorizationRequest.newAuthorizationRequest; import java.util.Collection; @@ -53,11 +52,11 @@ public int vote(Authentication authentication, FilterInvocation filter, final String requestPath = getRequestPath(filter.getHttpRequest()); StorageAreaInfo sa = resolver.resolveStorageArea(requestPath); - if (isNull(sa)) { + if (sa == null) { return ACCESS_ABSTAIN; } - if (Boolean.FALSE.equals(sa.wlcgScopeAuthzEnabled())) { + if (!sa.wlcgScopeAuthzEnabled()) { return ACCESS_ABSTAIN; } diff --git a/src/main/java/org/italiangrid/storm/webdav/config/FineGrainedAuthzPolicyParser.java b/src/main/java/org/italiangrid/storm/webdav/config/FineGrainedAuthzPolicyParser.java index 1d7922b9..1e545b06 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/FineGrainedAuthzPolicyParser.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/FineGrainedAuthzPolicyParser.java @@ -40,6 +40,7 @@ import org.italiangrid.storm.webdav.config.FineGrainedAuthzPolicyProperties.Action; import org.italiangrid.storm.webdav.config.FineGrainedAuthzPolicyProperties.PrincipalProperties; import org.italiangrid.storm.webdav.config.FineGrainedAuthzPolicyProperties.PrincipalProperties.PrincipalType; +import org.italiangrid.storm.webdav.oauth.authority.JwtClientAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtGroupAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtIssuerAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtScopeAuthority; @@ -137,6 +138,9 @@ PrincipalMatcher parsePrincipal(PrincipalProperties p) { } else if (PrincipalType.JWT_SUBJECT.equals(p.getType())) { matcher = AuthorityHolder.fromAuthority( new JwtSubjectAuthority(p.getParams().get("iss"), p.getParams().get("sub"))); + } else if (PrincipalType.JWT_CLIENT.equals(p.getType())) { + matcher = AuthorityHolder.fromAuthority( + new JwtClientAuthority(p.getParams().get("iss"), p.getParams().get("id"))); } else if (PrincipalType.VO.equals(p.getType())) { matcher = AuthorityHolder.fromAuthority(new VOMSVOAuthority(p.getParams().get("vo"))); } else if (PrincipalType.VO_MAP.equals(p.getType())) { @@ -162,7 +166,7 @@ PathAuthorizationPolicy parsePolicy(FineGrainedAuthzPolicyProperties policy) { policy.getPrincipals() .stream() .map(this::parsePrincipal) - .forEach(builder::withPrincipalMatcher);; + .forEach(builder::withPrincipalMatcher); return builder.build(); } diff --git a/src/main/java/org/italiangrid/storm/webdav/config/FineGrainedAuthzPolicyProperties.java b/src/main/java/org/italiangrid/storm/webdav/config/FineGrainedAuthzPolicyProperties.java index 29be9fb0..dc3d35c8 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/FineGrainedAuthzPolicyProperties.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/FineGrainedAuthzPolicyProperties.java @@ -45,6 +45,7 @@ public enum PrincipalType { JWT_SCOPE, JWT_ISSUER, JWT_SUBJECT, + JWT_CLIENT, VO, FQAN, VO_MAP, diff --git a/src/main/java/org/italiangrid/storm/webdav/config/OAuthProperties.java b/src/main/java/org/italiangrid/storm/webdav/config/OAuthProperties.java index bd833381..aab8d1f6 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/OAuthProperties.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/OAuthProperties.java @@ -96,6 +96,9 @@ public boolean isEnforceAudienceChecks() { @Min(value = 1, message = "The refresh period must be a positive integer") int refreshPeriodMinutes = 60; + @Min(value = 1, message = "The refresh timeout must be a positive integer") + int refreshTimeoutSeconds = 30; + public List getIssuers() { return issuers; } @@ -112,6 +115,14 @@ public void setRefreshPeriodMinutes(int refreshPeriodMinutes) { this.refreshPeriodMinutes = refreshPeriodMinutes; } + public int getRefreshTimeoutSeconds() { + return refreshTimeoutSeconds; + } + + public void setRefreshTimeoutSeconds(int refreshTimeoutSeconds) { + this.refreshTimeoutSeconds = refreshTimeoutSeconds; + } + public void setEnableOidc(boolean enableOidc) { this.enableOidc = enableOidc; } diff --git a/src/main/java/org/italiangrid/storm/webdav/config/OwnerStorageAreaInfo.java b/src/main/java/org/italiangrid/storm/webdav/config/OwnerStorageAreaInfo.java index 6b47c906..1f85128b 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/OwnerStorageAreaInfo.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/OwnerStorageAreaInfo.java @@ -39,34 +39,34 @@ public interface OwnerStorageAreaInfo extends StorageAreaInfo, Config { public Set orgs(); @DefaultValue("false") - public Boolean anonymousReadEnabled(); + public boolean anonymousReadEnabled(); @Override @DefaultValue("false") - public Boolean authenticatedReadEnabled(); + public boolean authenticatedReadEnabled(); @Override @DefaultValue("true") - public Boolean voMapEnabled(); + public boolean voMapEnabled(); @Override @DefaultValue("false") - public Boolean voMapGrantsWritePermission(); + public boolean voMapGrantsWritePermission(); @Override @DefaultValue("true") - public Boolean orgsGrantReadPermission(); + public boolean orgsGrantReadPermission(); @Override @DefaultValue("true") - public Boolean orgsGrantWritePermission(); + public boolean orgsGrantWritePermission(); @Override @DefaultValue("false") - public Boolean wlcgScopeAuthzEnabled(); + public boolean wlcgScopeAuthzEnabled(); @Override @DefaultValue("false") - public Boolean fineGrainedAuthzEnabled(); + public boolean fineGrainedAuthzEnabled(); } diff --git a/src/main/java/org/italiangrid/storm/webdav/config/SAConfigurationParser.java b/src/main/java/org/italiangrid/storm/webdav/config/SAConfigurationParser.java index 569eba8f..753c352d 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/SAConfigurationParser.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/SAConfigurationParser.java @@ -17,7 +17,6 @@ import java.io.File; import java.io.FileReader; -import java.io.FilenameFilter; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -32,12 +31,12 @@ public class SAConfigurationParser implements StorageAreaConfiguration { - private final Set RESERVED_SA_NAMES = - Sets.newHashSet("oauth", ".well-known", "actuator", "assets", "authn-info", "logout", "oidc-login"); + private static final Set RESERVED_SA_NAMES = Sets.newHashSet("oauth", ".well-known", + "actuator", "assets", "authn-info", "logout", "oidc-login"); private final ServiceConfiguration serviceConfig; - private String PROPERTIES_FILENAME_SUFFIX = ".properties"; + private static final String PROPERTIES_FILENAME_SUFFIX = ".properties"; private List saInfos; @@ -54,15 +53,11 @@ public SAConfigurationParser(ServiceConfiguration sc) { File dir = new File(saConfDir); directorySanityChecks(dir); - File[] saFiles = dir.listFiles(new FilenameFilter() { - - @Override - public boolean accept(File file, String name) { - if (RESERVED_SA_NAMES.contains(name) && name.endsWith(PROPERTIES_FILENAME_SUFFIX)) { - log.warn("Skipping {} as it is a reserved storage area name"); - } - return (!RESERVED_SA_NAMES.contains(name) && name.endsWith(PROPERTIES_FILENAME_SUFFIX)); + File[] saFiles = dir.listFiles((file, name) -> { + if (RESERVED_SA_NAMES.contains(name) && name.endsWith(PROPERTIES_FILENAME_SUFFIX)) { + log.warn("Skipping {} as it is a reserved storage area name", name); } + return (!RESERVED_SA_NAMES.contains(name) && name.endsWith(PROPERTIES_FILENAME_SUFFIX)); }); if (saFiles.length == 0) { @@ -72,7 +67,7 @@ public boolean accept(File file, String name) { throw new StoRMIntializationError(msg); } - saInfos = new ArrayList(); + saInfos = new ArrayList<>(); for (File f : saFiles) { diff --git a/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfiguration.java b/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfiguration.java index 9b3a0e03..c5fc69a0 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfiguration.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfiguration.java @@ -33,10 +33,14 @@ public interface ServiceConfiguration { public long getTrustAnchorsRefreshIntervalInSeconds(); + public int getMinConnections(); + public int getMaxConnections(); public int getMaxQueueSize(); + public int getThreadPoolMaxIdleTimeInMsec(); + public int getConnectorMaxIdleTimeInMsec(); public String getSAConfigDir(); diff --git a/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfigurationProperties.java b/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfigurationProperties.java index b6e09321..dbacabad 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfigurationProperties.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/ServiceConfigurationProperties.java @@ -249,6 +249,20 @@ public void setPolicies(List policies) { } } + public static class ServerProperties { + + @Positive + int maxIdleTimeMsec = 3600000; + + public int getMaxIdleTimeMsec() { + return maxIdleTimeMsec; + } + + public void setMaxIdleTimeMsec(int maxIdleTimeMsec) { + this.maxIdleTimeMsec = maxIdleTimeMsec; + } + } + public static class ConnectorProperties { @Positive @@ -260,10 +274,13 @@ public static class ConnectorProperties { int securePort = 8443; @Positive - int maxConnections = 200; + int minConnections = 50; + + @Positive + int maxConnections = 300; @Positive - int maxQueueSize = 512; + int maxQueueSize = 900; @Positive int maxIdleTimeMsec = 30000; @@ -292,6 +309,14 @@ public void setSecurePort(int securePort) { this.securePort = securePort; } + public int getMinConnections() { + return minConnections; + } + + public void setMinConnections(int minConnections) { + this.minConnections = minConnections; + } + public int getMaxConnections() { return maxConnections; } @@ -597,6 +622,8 @@ public void setTrustStore(VOMSTrustStoreProperties trustStore) { private MacaroonFilterProperties macaroonFilter; + private ServerProperties server; + private ConnectorProperties connector; private TLSProperties tls; @@ -643,12 +670,18 @@ public void setTls(TLSProperties tls) { this.tls = tls; } + public ServerProperties getServer() { + return server; + } + + public void setServer(ServerProperties server) { + this.server = server; + } public ConnectorProperties getConnector() { return connector; } - public void setConnector(ConnectorProperties connector) { this.connector = connector; } @@ -732,55 +765,56 @@ public long getTrustAnchorsRefreshIntervalInSeconds() { return getTls().getTrustAnchorsRefreshIntervalSecs(); } + @Override + public int getMinConnections() { + return getConnector().getMinConnections(); + } @Override public int getMaxConnections() { return getConnector().getMaxConnections(); } - @Override public int getMaxQueueSize() { return getConnector().getMaxQueueSize(); } + @Override + public int getThreadPoolMaxIdleTimeInMsec() { + return getServer().getMaxIdleTimeMsec(); + } @Override public int getConnectorMaxIdleTimeInMsec() { return getConnector().getMaxIdleTimeMsec(); } - @Override public String getSAConfigDir() { return getSa().getConfigDir(); } - @Override public boolean enableVOMapFiles() { return getVoMapFiles().isEnabled(); } - @Override public String getVOMapFilesConfigDir() { return getVoMapFiles().getConfigDir(); } - @Override public long getVOMapFilesRefreshIntervalInSeconds() { return getVoMapFiles().getRefreshIntervalSec(); } - @Override public boolean isAuthorizationDisabled() { return getAuthz().isDisabled(); } - @Override public boolean requireClientCertificateAuthentication() { return getTls().isRequireClientCert(); @@ -887,4 +921,5 @@ public TapeProperties getTape() { public void setTape(TapeProperties tape) { this.tape = tape; } + } diff --git a/src/main/java/org/italiangrid/storm/webdav/config/StorageAreaInfo.java b/src/main/java/org/italiangrid/storm/webdav/config/StorageAreaInfo.java index 58781227..230c13af 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/StorageAreaInfo.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/StorageAreaInfo.java @@ -32,19 +32,19 @@ public interface StorageAreaInfo { public Set orgs(); - public Boolean anonymousReadEnabled(); + public boolean anonymousReadEnabled(); - public Boolean authenticatedReadEnabled(); + public boolean authenticatedReadEnabled(); - public Boolean voMapEnabled(); + public boolean voMapEnabled(); - public Boolean voMapGrantsWritePermission(); + public boolean voMapGrantsWritePermission(); - public Boolean orgsGrantReadPermission(); + public boolean orgsGrantReadPermission(); - public Boolean orgsGrantWritePermission(); + public boolean orgsGrantWritePermission(); - public Boolean wlcgScopeAuthzEnabled(); - - public Boolean fineGrainedAuthzEnabled(); + public boolean wlcgScopeAuthzEnabled(); + + public boolean fineGrainedAuthzEnabled(); } diff --git a/src/main/java/org/italiangrid/storm/webdav/config/validation/PrincipalValidator.java b/src/main/java/org/italiangrid/storm/webdav/config/validation/PrincipalValidator.java index 7b191541..dab3fe78 100644 --- a/src/main/java/org/italiangrid/storm/webdav/config/validation/PrincipalValidator.java +++ b/src/main/java/org/italiangrid/storm/webdav/config/validation/PrincipalValidator.java @@ -17,8 +17,8 @@ import static com.google.common.base.Strings.isNullOrEmpty; import static java.lang.String.format; -import static java.util.Objects.isNull; import static org.italiangrid.storm.webdav.config.FineGrainedAuthzPolicyProperties.PrincipalProperties.PrincipalType.FQAN; +import static org.italiangrid.storm.webdav.config.FineGrainedAuthzPolicyProperties.PrincipalProperties.PrincipalType.JWT_CLIENT; import static org.italiangrid.storm.webdav.config.FineGrainedAuthzPolicyProperties.PrincipalProperties.PrincipalType.JWT_GROUP; import static org.italiangrid.storm.webdav.config.FineGrainedAuthzPolicyProperties.PrincipalProperties.PrincipalType.JWT_SCOPE; import static org.italiangrid.storm.webdav.config.FineGrainedAuthzPolicyProperties.PrincipalProperties.PrincipalType.JWT_ISSUER; @@ -51,6 +51,8 @@ public class PrincipalValidator implements .put(JWT_SUBJECT, "iss") .put(JWT_SUBJECT, "sub") .put(JWT_ISSUER, "iss") + .put(JWT_CLIENT, "iss") + .put(JWT_CLIENT, "id") .put(VO, "vo") .put(VO_MAP, "vo") .put(X509_SUBJECT, "subject") @@ -63,8 +65,8 @@ public boolean isValid( ConstraintValidatorContext context) { Collection requiredArgs = REQUIRED_ARGS.get(value.getType()); - - if (isNull(requiredArgs) || requiredArgs.isEmpty()) { + + if (requiredArgs == null || requiredArgs.isEmpty()) { return true; } diff --git a/src/main/java/org/italiangrid/storm/webdav/fs/DefaultFSStrategy.java b/src/main/java/org/italiangrid/storm/webdav/fs/DefaultFSStrategy.java index aa3202d5..9ca90799 100644 --- a/src/main/java/org/italiangrid/storm/webdav/fs/DefaultFSStrategy.java +++ b/src/main/java/org/italiangrid/storm/webdav/fs/DefaultFSStrategy.java @@ -19,6 +19,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -30,8 +32,6 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; -import com.google.common.io.Files; - @Component public class DefaultFSStrategy implements FilesystemAccess { @@ -60,10 +60,10 @@ public File mkdir(File parentDirectory, String dirName) { } @Override - public boolean rm(File f) { + public void rm(File f) throws IOException { LOG.debug("rm: {}", f.getAbsolutePath()); - return f.delete(); + Files.delete(f.toPath()); } @Override @@ -71,15 +71,15 @@ public void mv(File source, File dest) { LOG.debug("mv: source={}, dest={}", source.getAbsolutePath(), dest.getAbsolutePath()); - + try { if (source.getCanonicalPath().equals(dest.getCanonicalPath())) { throw new SameFileError("Source and destination files are the same"); } - - // Overwrites the destination, if it exists - Files.move(source, dest); + + // Overwrites the destination, if it exists + Files.move(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { throw new StoRMWebDAVError(e.getMessage(), e); @@ -101,7 +101,7 @@ public void cp(File source, File dest) { dest.getAbsolutePath()); try { - + if (source.getCanonicalPath().equals(dest.getCanonicalPath())) { throw new SameFileError("Source and destination files are the same"); } @@ -112,7 +112,7 @@ public void cp(File source, File dest) { } else { - Files.copy(source, dest); + Files.copy(source.toPath(), dest.toPath()); } diff --git a/src/main/java/org/italiangrid/storm/webdav/fs/FilesystemAccess.java b/src/main/java/org/italiangrid/storm/webdav/fs/FilesystemAccess.java index 3d6bcdf5..503b67b1 100644 --- a/src/main/java/org/italiangrid/storm/webdav/fs/FilesystemAccess.java +++ b/src/main/java/org/italiangrid/storm/webdav/fs/FilesystemAccess.java @@ -16,13 +16,14 @@ package org.italiangrid.storm.webdav.fs; import java.io.File; +import java.io.IOException; import java.io.InputStream; public interface FilesystemAccess { public File mkdir(File parentDirectory, String dirName); - public boolean rm(File f); + public void rm(File f) throws IOException; public void mv(File source, File dest); diff --git a/src/main/java/org/italiangrid/storm/webdav/fs/MetricsFSStrategyWrapper.java b/src/main/java/org/italiangrid/storm/webdav/fs/MetricsFSStrategyWrapper.java index 2083d56d..bfdf6e83 100644 --- a/src/main/java/org/italiangrid/storm/webdav/fs/MetricsFSStrategyWrapper.java +++ b/src/main/java/org/italiangrid/storm/webdav/fs/MetricsFSStrategyWrapper.java @@ -18,6 +18,7 @@ import static com.codahale.metrics.MetricRegistry.name; import java.io.File; +import java.io.IOException; import java.io.InputStream; import com.codahale.metrics.MetricRegistry; @@ -68,12 +69,12 @@ public File mkdir(File parentDirectory, String dirName) { } @Override - public boolean rm(File f) { + public void rm(File f) throws IOException { final Timer.Context context = rmTimer.time(); try { - return delegate.rm(f); + delegate.rm(f); } finally { context.stop(); diff --git a/src/main/java/org/italiangrid/storm/webdav/fs/attrs/DefaultExtendedFileAttributesHelper.java b/src/main/java/org/italiangrid/storm/webdav/fs/attrs/DefaultExtendedFileAttributesHelper.java index 8bb6a7dc..04392647 100644 --- a/src/main/java/org/italiangrid/storm/webdav/fs/attrs/DefaultExtendedFileAttributesHelper.java +++ b/src/main/java/org/italiangrid/storm/webdav/fs/attrs/DefaultExtendedFileAttributesHelper.java @@ -31,6 +31,9 @@ public class DefaultExtendedFileAttributesHelper implements ExtendedAttributesHelper { + private static final String USERDEFINEDFILEATTRIBUTEVIEW_NOT_SUPPORTED_MESSAGE = + "UserDefinedFileAttributeView not supported on file "; + public static final String STORM_ADLER32_CHECKSUM_ATTR_NAME = "storm.checksum.adler32"; public DefaultExtendedFileAttributesHelper() { @@ -62,8 +65,7 @@ public void setExtendedFileAttribute(File f, String attributeName, if (faView == null) { throw new IOException( - "UserDefinedFileAttributeView not supported on file " - + f.getAbsolutePath()); + USERDEFINEDFILEATTRIBUTEVIEW_NOT_SUPPORTED_MESSAGE + f.getAbsolutePath()); } faView.write(attributeName, StandardCharsets.UTF_8.encode(attributeValue)); @@ -75,14 +77,13 @@ public String getExtendedFileAttributeValue(File f, String attributeName) checkNotNull(f); checkArgument(!isNullOrEmpty(attributeName)); - + UserDefinedFileAttributeView faView = Files.getFileAttributeView( f.toPath(), UserDefinedFileAttributeView.class); if (faView == null) { throw new IOException( - "UserDefinedFileAttributeView not supported on file " - + f.getAbsolutePath()); + USERDEFINEDFILEATTRIBUTEVIEW_NOT_SUPPORTED_MESSAGE + f.getAbsolutePath()); } return getAttributeValue(faView, attributeName); @@ -93,14 +94,13 @@ public String getExtendedFileAttributeValue(File f, String attributeName) public List getExtendedFileAttributeNames(File f) throws IOException { checkNotNull(f); - + UserDefinedFileAttributeView faView = Files.getFileAttributeView( f.toPath(), UserDefinedFileAttributeView.class); if (faView == null) { throw new IOException( - "UserDefinedFileAttributeView not supported on file " - + f.getAbsolutePath()); + USERDEFINEDFILEATTRIBUTEVIEW_NOT_SUPPORTED_MESSAGE + f.getAbsolutePath()); } return faView.list(); @@ -127,7 +127,7 @@ public String getChecksumAttribute(File f) throws IOException { public boolean fileSupportsExtendedAttributes(File f) throws IOException { checkNotNull(f); - + UserDefinedFileAttributeView faView = Files.getFileAttributeView( f.toPath(), UserDefinedFileAttributeView.class); @@ -137,7 +137,7 @@ public boolean fileSupportsExtendedAttributes(File f) throws IOException { @Override public void setChecksumAttribute(Path p, String checksumValue) throws IOException { setChecksumAttribute(p.toFile(), checksumValue); - + } @Override diff --git a/src/main/java/org/italiangrid/storm/webdav/metrics/StorageAreaStatsFilter.java b/src/main/java/org/italiangrid/storm/webdav/metrics/StorageAreaStatsFilter.java index 14e71ff3..2903f6b3 100644 --- a/src/main/java/org/italiangrid/storm/webdav/metrics/StorageAreaStatsFilter.java +++ b/src/main/java/org/italiangrid/storm/webdav/metrics/StorageAreaStatsFilter.java @@ -16,7 +16,6 @@ package org.italiangrid.storm.webdav.metrics; import static com.codahale.metrics.MetricRegistry.name; -import static java.util.Objects.isNull; import java.io.IOException; @@ -52,7 +51,7 @@ public StorageAreaStatsFilter(MetricRegistry registry, PathResolver resolver) { private void updateStats(HttpServletRequest request, HttpServletResponse response) { StorageAreaInfo sa = resolver.resolveStorageArea(getSerlvetRequestPath(request)); - if (!isNull(sa)) { + if (sa != null) { String prefix = name(SA_KEY, sa.name(), REQUESTS_KEY); registry.meter(prefix).mark(); if (response.getStatus() >= 400) { diff --git a/src/main/java/org/italiangrid/storm/webdav/milton/StoRMDirectoryResource.java b/src/main/java/org/italiangrid/storm/webdav/milton/StoRMDirectoryResource.java index cc914b40..1102a935 100644 --- a/src/main/java/org/italiangrid/storm/webdav/milton/StoRMDirectoryResource.java +++ b/src/main/java/org/italiangrid/storm/webdav/milton/StoRMDirectoryResource.java @@ -20,12 +20,15 @@ import java.io.InputStream; import java.nio.file.DirectoryStream; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import org.italiangrid.storm.webdav.error.DirectoryNotEmpty; import org.italiangrid.storm.webdav.error.StoRMWebDAVError; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import io.milton.http.Request; import io.milton.http.exceptions.BadRequestException; @@ -42,6 +45,8 @@ public class StoRMDirectoryResource extends StoRMResource implements PutableResource, MakeCollectionableResource, DeletableResource, DeletableCollectionResource, CopyableResource { + private static final Logger logger = LoggerFactory.getLogger(StoRMDirectoryResource.class); + public StoRMDirectoryResource(StoRMResourceFactory factory, File f) { super(factory, f); @@ -112,8 +117,13 @@ public void delete() throws NotAuthorizedException, ConflictException, BadReques throw new StoRMWebDAVError(e); } - getFilesystemAccess().rm(getFile()); - + try { + getFilesystemAccess().rm(getFile()); + } catch (NoSuchFileException e) { + logger.warn("Unable to remove directory {}: {}", getFile(), e.getMessage()); + } catch (IOException e) { + throw new StoRMWebDAVError(e); + } } @Override diff --git a/src/main/java/org/italiangrid/storm/webdav/milton/StoRMFileResource.java b/src/main/java/org/italiangrid/storm/webdav/milton/StoRMFileResource.java index aefbd19f..e33c6dd6 100644 --- a/src/main/java/org/italiangrid/storm/webdav/milton/StoRMFileResource.java +++ b/src/main/java/org/italiangrid/storm/webdav/milton/StoRMFileResource.java @@ -16,7 +16,6 @@ package org.italiangrid.storm.webdav.milton; import static io.milton.property.PropertySource.PropertyAccessibility.READ_ONLY; -import static java.util.Objects.isNull; import java.io.BufferedInputStream; import java.io.File; @@ -27,6 +26,7 @@ import java.io.OutputStream; import java.net.FileNameMap; import java.net.URLConnection; +import java.nio.file.NoSuchFileException; import java.util.List; import java.util.Map; @@ -85,8 +85,14 @@ public StoRMFileResource(StoRMResourceFactory factory, File f) { @Override public void delete() throws NotAuthorizedException, ConflictException, BadRequestException { - getFilesystemAccess().rm(getFile()); - + try { + getFilesystemAccess().rm(getFile()); + } catch (NoSuchFileException e) { + logger.warn("Unable to remove file {}: {}", getFile(), e.getMessage()); + } catch (IOException e) { + logger.error("Unable to remove file {}: {}", getFile(), e.getMessage()); + throw new StoRMWebDAVError(e); + } } protected void handleIOException(IOException e) { @@ -126,7 +132,7 @@ public void replaceContent(InputStream in, Long length) protected void validateRange(Range range) { long fileSize = getFile().length(); - if (isNull(range.getStart())) { + if (range.getStart() == null) { throw new StoRMWebDAVError("Invalid range: range start not defined"); } @@ -134,7 +140,7 @@ protected void validateRange(Range range) { throw new StoRMWebDAVError("Invalid range: range start out of bounds"); } - if (!isNull(range.getFinish()) && range.getFinish() > fileSize) { + if (range.getFinish() != null && range.getFinish() > fileSize) { throw new StoRMWebDAVError("Invalid range: range end out of bounds"); } } @@ -147,7 +153,7 @@ public void replacePartialContent(Range range, InputStream in) { long rangeStart = range.getStart(); long rangeEnd = getFile().length(); - if (!isNull(range.getFinish()) && range.getFinish() < rangeEnd) { + if (range.getFinish() != null && range.getFinish() < rangeEnd) { rangeEnd = range.getFinish(); } @@ -185,14 +191,13 @@ protected void calculateChecksum() { @Override public Object getProperty(QName name) { - if (name.getNamespaceURI().equals(STORM_NAMESPACE_URI)) { - if (name.getLocalPart().equals(PROPERTY_CHECKSUM)) { - try { - return getExtendedAttributesHelper().getChecksumAttribute(getFile()); - } catch (IOException e) { - logger.warn("Errror getting checksum value for file: {}", getFile().getAbsolutePath(), e); - return null; - } + if (name.getNamespaceURI().equals(STORM_NAMESPACE_URI) + && name.getLocalPart().equals(PROPERTY_CHECKSUM)) { + try { + return getExtendedAttributesHelper().getChecksumAttribute(getFile()); + } catch (IOException e) { + logger.warn("Errror getting checksum value for file: {}", getFile().getAbsolutePath(), e); + return null; } } diff --git a/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonRequest.java b/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonRequest.java index ecc0d7c1..d1021cc4 100644 --- a/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonRequest.java +++ b/src/main/java/org/italiangrid/storm/webdav/milton/StoRMMiltonRequest.java @@ -28,8 +28,8 @@ public class StoRMMiltonRequest extends ServletRequest { - private static final String regex = "(http.*:\\d*)/webdav/(.*)$"; - private static final Pattern p = Pattern.compile(regex); + private static final String REGEX = "(http.*:\\d*)/webdav/(.*)$"; + private static final Pattern p = Pattern.compile(REGEX); public StoRMMiltonRequest(HttpServletRequest r, ServletContext servletContext) { @@ -60,5 +60,5 @@ public Auth getAuthorization() { // Always return null as milton is confused by the OAuth2 Bearer scheme return null; } - + } diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/GrantedAuthoritiesMapperSupport.java b/src/main/java/org/italiangrid/storm/webdav/oauth/GrantedAuthoritiesMapperSupport.java index c02d5f70..6ff8e74a 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/GrantedAuthoritiesMapperSupport.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/GrantedAuthoritiesMapperSupport.java @@ -15,8 +15,6 @@ */ package org.italiangrid.storm.webdav.oauth; -import static java.util.Objects.isNull; - import java.util.Collection; import java.util.Set; @@ -36,8 +34,8 @@ public class GrantedAuthoritiesMapperSupport { protected final Multimap authzMap = ArrayListMultimap.create(); protected final AuthorizationServerProperties authzServerProperties; - public static final String[] OAUTH_GROUP_CLAIM_NAMES = {"groups", "wlcg.groups"}; - public static final String SCOPE_CLAIM_NAME = "scope"; + protected static final String[] OAUTH_GROUP_CLAIM_NAMES = {"groups", "wlcg.groups", "entitlements"}; + protected static final String SCOPE_CLAIM_NAME = "scope"; protected final Set anonymousGrantedAuthorities = Sets.newHashSet(); @@ -48,24 +46,24 @@ public GrantedAuthoritiesMapperSupport(StorageAreaConfiguration conf, authzServerProperties = props.getAuthzServer(); for (StorageAreaInfo sa : conf.getStorageAreaInfo()) { - if (!isNull(sa.orgs())) { + if (sa.orgs() != null) { sa.orgs().forEach(i -> addSaGrantedAuthorities(sa, i)); } } for (StorageAreaInfo sa : conf.getStorageAreaInfo()) { - if (Boolean.TRUE.equals(sa.anonymousReadEnabled())) { + if (sa.anonymousReadEnabled()) { anonymousGrantedAuthorities.add(SAPermission.canRead(sa.name())); } } } protected void addSaGrantedAuthorities(StorageAreaInfo sa, String issuer) { - if (Boolean.TRUE.equals(sa.orgsGrantReadPermission())) { + if (sa.orgsGrantReadPermission()) { authzMap.put(issuer, SAPermission.canRead(sa.name())); } - if (Boolean.TRUE.equals(sa.orgsGrantWritePermission())) { + if (sa.orgsGrantWritePermission()) { authzMap.put(issuer, SAPermission.canWrite(sa.name())); } diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/StormJwtAuthoritiesConverter.java b/src/main/java/org/italiangrid/storm/webdav/oauth/StormJwtAuthoritiesConverter.java index 7ea26539..6a68bd03 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/StormJwtAuthoritiesConverter.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/StormJwtAuthoritiesConverter.java @@ -18,18 +18,17 @@ import static org.italiangrid.storm.webdav.oauth.authzserver.jwt.DefaultJwtTokenIssuer.CLAIM_AUTHORITIES; import java.util.Collection; -import java.util.Objects; import java.util.Optional; import java.util.Set; import org.italiangrid.storm.webdav.authz.SAPermission; import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties; import org.italiangrid.storm.webdav.config.StorageAreaConfiguration; +import org.italiangrid.storm.webdav.oauth.authority.JwtClientAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtGroupAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtIssuerAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtScopeAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtSubjectAuthority; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; @@ -42,7 +41,6 @@ public class StormJwtAuthoritiesConverter extends GrantedAuthoritiesMapperSupport implements Converter> { - @Autowired public StormJwtAuthoritiesConverter(StorageAreaConfiguration conf, ServiceConfigurationProperties props) { super(conf, props); @@ -58,7 +56,7 @@ protected Set extractAuthoritiesLocalAuthzServer(Jwt jwt) { Optional.ofNullable( jwt.getClaimAsStringList(CLAIM_AUTHORITIES)) .ifPresent(a -> a.forEach(at -> authorities.add(SAPermission.fromString(at)))); - + return authorities; } @@ -66,7 +64,7 @@ protected Set extractOauthScopeAuthorities(Jwt jwt) { Set scopeAuthorities = Sets.newHashSet(); - if (!Objects.isNull(jwt.getClaimAsString(SCOPE_CLAIM_NAME))) { + if (jwt.getClaimAsString(SCOPE_CLAIM_NAME) != null) { String tokenIssuer = jwt.getClaimAsString(JwtClaimNames.ISS); String[] scopes = jwt.getClaimAsString(SCOPE_CLAIM_NAME).split(" "); @@ -86,7 +84,7 @@ protected Set extractOauthGroupAuthorities(Jwt jwt) { String tokenIssuer = jwt.getClaimAsString(JwtClaimNames.ISS); for (String groupClaim : OAUTH_GROUP_CLAIM_NAMES) { - if (Boolean.TRUE.equals(jwt.containsClaim(groupClaim))) { + if (jwt.hasClaim(groupClaim)) { jwt.getClaimAsStringList(groupClaim) .forEach(gc -> groupAuthorities.add(new JwtGroupAuthority(tokenIssuer, gc))); break; @@ -109,8 +107,11 @@ protected Collection extractAuthorities(Jwt jwt) { authorities.addAll(extractOauthGroupAuthorities(jwt)); authorities.addAll(extractOauthScopeAuthorities(jwt)); - authorities.add(new JwtIssuerAuthority(jwt.getIssuer().toString())); - authorities.add(new JwtSubjectAuthority(jwt.getIssuer().toString(), jwt.getSubject())); + authorities.add(new JwtIssuerAuthority(issuer)); + authorities.add(new JwtSubjectAuthority(issuer, jwt.getSubject())); + if (jwt.getClaim("client_id") != null) { + authorities.add(new JwtClientAuthority(issuer, jwt.getClaim("client_id"))); + } return authorities; } diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/authority/JwtClientAuthority.java b/src/main/java/org/italiangrid/storm/webdav/oauth/authority/JwtClientAuthority.java new file mode 100644 index 00000000..c36fb310 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/authority/JwtClientAuthority.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.oauth.authority; + +public class JwtClientAuthority extends JwtAuthority { + + private static final long serialVersionUID = 1L; + + public static final String AUTH_TEMPLATE = "O_client(%s,%s)"; + + private final String clientId; + + public JwtClientAuthority(String issuer, String clientId) { + super(issuer, String.format(AUTH_TEMPLATE, issuer, clientId)); + this.clientId = clientId; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((clientId == null) ? 0 : clientId.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + JwtClientAuthority other = (JwtClientAuthority) obj; + if (clientId == null) { + if (other.clientId != null) + return false; + } else if (!clientId.equals(other.clientId)) + return false; + return true; + } + +} diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/ErrorResponseDTO.java b/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/ErrorResponseDTO.java index 23d7e6ce..2521dc6f 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/ErrorResponseDTO.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/ErrorResponseDTO.java @@ -18,13 +18,13 @@ import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; @JsonInclude(NON_EMPTY) -@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class ErrorResponseDTO { - + public static final String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type"; public static final String INVALID_REQUEST = "invalid_request"; public static final String INVALID_SCOPE = "invalid_scope"; diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/ResourceAccessTokenRequest.java b/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/ResourceAccessTokenRequest.java index e3bce43f..26a3645c 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/ResourceAccessTokenRequest.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/ResourceAccessTokenRequest.java @@ -21,7 +21,7 @@ public enum Permission { r, w, rw - }; + } final String path; final Permission permission; diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/TokenResponseDTO.java b/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/TokenResponseDTO.java index 06f9967a..48ef1316 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/TokenResponseDTO.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/TokenResponseDTO.java @@ -18,11 +18,11 @@ import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; @JsonInclude(NON_EMPTY) -@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class TokenResponseDTO { String accessToken; diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/jwt/DefaultJwtTokenIssuer.java b/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/jwt/DefaultJwtTokenIssuer.java index 8905ac39..53dd655f 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/jwt/DefaultJwtTokenIssuer.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/jwt/DefaultJwtTokenIssuer.java @@ -15,7 +15,6 @@ */ package org.italiangrid.storm.webdav.oauth.authzserver.jwt; -import static java.util.Objects.isNull; import static java.util.stream.Collectors.toList; import java.time.Clock; @@ -98,7 +97,7 @@ protected Date computeTokenExpirationTimestamp(AccessTokenRequest request, Instant defaultExpiration = now.plusSeconds(properties.getMaxTokenLifetimeSec()); Instant expiration = defaultExpiration; - if (!isNull(request.getLifetime()) && request.getLifetime() > 0) { + if (request.getLifetime() != null && request.getLifetime() > 0) { requestedExpiration = Optional.of(now.plusSeconds(request.getLifetime())); } @@ -126,19 +125,19 @@ protected Date computeResourceTokenExpiration(ResourceAccessTokenRequest request public SignedJWT createAccessToken(AccessTokenRequest request, Authentication authentication) { Set tokenAuthorities = Sets.newHashSet(); - + Set saAuthorities = policyService.getSAPermissions(authentication); tokenAuthorities.addAll(saAuthorities); - + tokenAuthorities.addAll(authentication.getAuthorities()); - + JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder(); claimsSet.issuer(properties.getIssuer()); claimsSet.audience(properties.getIssuer()); claimsSet.subject(authentication.getName()); claimsSet.expirationTime(computeTokenExpirationTimestamp(request, authentication)); - + claimsSet.claim(CLAIM_AUTHORITIES, tokenAuthorities.stream().map(Object::toString).collect(toList())); @@ -162,7 +161,7 @@ public SignedJWT createResourceAccessToken(ResourceAccessTokenRequest request, claimsSet.subject(helper.getPrincipalAsString(authentication)); claimsSet.expirationTime(computeResourceTokenExpiration(request)); - + claimsSet.claim(PATH_CLAIM, request.getPath()); claimsSet.claim(PERMS_CLAIM, request.getPermission().name()); claimsSet.claim(ORIGIN_CLAIM, request.getOrigin()); diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/web/AuthzServerMetadata.java b/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/web/AuthzServerMetadata.java index dba834b0..efb85698 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/web/AuthzServerMetadata.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/authzserver/web/AuthzServerMetadata.java @@ -21,11 +21,11 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.annotation.JsonNaming; @JsonInclude(NON_EMPTY) -@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) public class AuthzServerMetadata { private String issuer; diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/DefaultOidcConfigurationFetcher.java b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/DefaultOidcConfigurationFetcher.java index 090ee2db..f1c1c1f3 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/DefaultOidcConfigurationFetcher.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/DefaultOidcConfigurationFetcher.java @@ -18,34 +18,46 @@ import static java.lang.String.format; import java.net.URI; +import java.time.Duration; +import java.util.Arrays; import java.util.Map; +import org.italiangrid.storm.webdav.config.OAuthProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; +import com.nimbusds.jose.KeySourceException; +import com.nimbusds.jose.RemoteKeySourceException; + @Service public class DefaultOidcConfigurationFetcher implements OidcConfigurationFetcher { public static final String WELL_KNOWN_FRAGMENT = "/.well-known/openid-configuration"; public static final String ISSUER_MISMATCH_ERROR_TEMPLATE = "Issuer in metadata '%s' does not match with requested issuer '%s'"; - public static final String NO_JWKS_URI_ERROR_TEMPLATE = + public static final String NO_JWKS_URI_ERROR_TEMPLATE = "No jwks_uri found in metadata for issuer '%s'"; + private static final MediaType APPLICATION_JWK_SET_JSON = + new MediaType("application", "jwk-set+json"); + public static final Logger LOG = LoggerFactory.getLogger(DefaultOidcConfigurationFetcher.class); - final RestTemplateBuilder restBuilder; + final RestTemplate restTemplate; - @Autowired - public DefaultOidcConfigurationFetcher(RestTemplateBuilder restBuilder) { - this.restBuilder = restBuilder; + public DefaultOidcConfigurationFetcher(RestTemplateBuilder restBuilder, + OAuthProperties oAuthProperties) { + final Duration timeout = Duration.ofSeconds(oAuthProperties.getRefreshTimeoutSeconds()); + this.restTemplate = restBuilder.setConnectTimeout(timeout).setReadTimeout(timeout).build(); } private void metadataChecks(String issuer, Map oidcConfiguration) { @@ -59,40 +71,63 @@ private void metadataChecks(String issuer, Map oidcConfiguration throw new OidcConfigurationResolutionError( format(ISSUER_MISMATCH_ERROR_TEMPLATE, metadataIssuer, issuer)); } - + if (!oidcConfiguration.containsKey("jwks_uri")) { - throw new OidcConfigurationResolutionError(format(NO_JWKS_URI_ERROR_TEMPLATE,issuer)); + throw new OidcConfigurationResolutionError(format(NO_JWKS_URI_ERROR_TEMPLATE, issuer)); } } @Override public Map loadConfigurationForIssuer(String issuer) { LOG.debug("Fetching OpenID configuration for {}", issuer); - + ParameterizedTypeReference> typeReference = new ParameterizedTypeReference>() {}; - RestTemplate rest = restBuilder.build(); - URI uri = UriComponentsBuilder.fromUriString(issuer + WELL_KNOWN_FRAGMENT).build().toUri(); - + ResponseEntity> response = null; try { - - RequestEntity request = RequestEntity.get(uri).build(); - Map conf = rest.exchange(request, typeReference).getBody(); - metadataChecks(issuer, conf); - return conf; + response = restTemplate.exchange(RequestEntity.get(uri).build(), typeReference); } catch (RuntimeException e) { - final String errorMsg = - format("Unable to resolve OpenID configuration for issuer '%s' from '%s': %s", issuer, - uri, e.getMessage()); - + final String errorMsg = format("Unable to resolve OpenID configuration from '%s'", uri); if (LOG.isDebugEnabled()) { - LOG.error(errorMsg, e); + LOG.error("{}: {}", errorMsg, e.getMessage()); } - throw new OidcConfigurationResolutionError(errorMsg, e); } + if (response.getStatusCodeValue() != 200) { + throw new OidcConfigurationResolutionError( + format("Received status code: %s", response.getStatusCodeValue())); + } + if (response.getBody() == null) { + throw new OidcConfigurationResolutionError("Received null body"); + } + metadataChecks(issuer, response.getBody()); + return response.getBody(); } + @Override + public String loadJWKSourceForURL(URI uri) throws KeySourceException { + + LOG.debug("Fetching JWK from {}", uri); + + HttpHeaders headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON)); + ResponseEntity response = null; + try { + RequestEntity request = RequestEntity.get(uri).headers(headers).build(); + response = restTemplate.exchange(request, String.class); + } catch (RuntimeException e) { + final String errorMsg = format("Unable to get JWK from '%s'", uri); + if (LOG.isDebugEnabled()) { + LOG.error("{}: {}", errorMsg, e.getMessage()); + } + throw new RemoteKeySourceException(errorMsg, e); + } + if (response.getStatusCodeValue() != 200) { + throw new KeySourceException(format("Unable to get JWK from '%s': received status code %s", + uri, response.getStatusCodeValue())); + } + return response.getBody(); + } } diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/NoExpirationStringCache.java b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/NoExpirationStringCache.java new file mode 100644 index 00000000..4bc9e253 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/NoExpirationStringCache.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.oauth.utils; + +import java.util.concurrent.Callable; + +import org.springframework.cache.support.AbstractValueAdaptingCache; +import org.springframework.lang.Nullable; + +public class NoExpirationStringCache extends AbstractValueAdaptingCache { + + private static final String NAME = "NoExpirationCache"; + private final String value; + + public NoExpirationStringCache(String value) { + super(false); + this.value = value; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public Object getNativeCache() { + return this; + } + + @Override + @Nullable + protected Object lookup(Object key) { + return value; + } + + @Override + public void put(Object key, Object value) { + // Nothing to do + } + + @Override + public void evict(Object key) { + // Nothing to do + } + + @Override + public void clear() { + // Nothing to do + } + + @SuppressWarnings("unchecked") + @Override + public T get(Object key, Callable valueLoader) { + return (T) fromStoreValue(value); + } +} diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/OidcConfigurationFetcher.java b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/OidcConfigurationFetcher.java index 786e9f1b..7aa30179 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/OidcConfigurationFetcher.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/OidcConfigurationFetcher.java @@ -15,10 +15,15 @@ */ package org.italiangrid.storm.webdav.oauth.utils; +import java.net.URI; import java.util.Map; +import com.nimbusds.jose.KeySourceException; + public interface OidcConfigurationFetcher { Map loadConfigurationForIssuer(String issuer); + String loadJWKSourceForURL(URI uri) throws KeySourceException; + } diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/TrustedJwtDecoderCacheLoader.java b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/TrustedJwtDecoderCacheLoader.java index efde8ee9..161e1625 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/utils/TrustedJwtDecoderCacheLoader.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/utils/TrustedJwtDecoderCacheLoader.java @@ -15,6 +15,7 @@ */ package org.italiangrid.storm.webdav.oauth.utils; +import java.net.URI; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; @@ -28,8 +29,8 @@ import org.italiangrid.storm.webdav.oauth.validator.WlcgProfileValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cache.Cache; import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jwt.Jwt; @@ -53,7 +54,6 @@ public class TrustedJwtDecoderCacheLoader extends CacheLoader oidcConfiguration = fetcher.loadConfigurationForIssuer(issuer); + URI jwksUri = URI.create(oidcConfiguration.get("jwks_uri").toString()); + Cache noExpirationCache = + new NoExpirationStringCache(fetcher.loadJWKSourceForURL(jwksUri)); NimbusJwtDecoder decoder = - NimbusJwtDecoder.withJwkSetUri((oidcConfiguration.get("jwks_uri").toString())).build(); + NimbusJwtDecoder.withJwkSetUri((oidcConfiguration.get("jwks_uri").toString())) + .cache(noExpirationCache) + .build(); OAuth2TokenValidator jwtValidator = JwtValidators.createDefaultWithIssuer(issuer); OAuth2TokenValidator wlcgProfileValidator = new WlcgProfileValidator(); @@ -91,7 +96,7 @@ public JwtDecoder load(String issuer) throws Exception { validators.add(new AudienceValidator(as)); } - decoder.setJwtValidator(new DelegatingOAuth2TokenValidator(validators)); + decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators)); return decoder; } diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/validator/AudienceValidator.java b/src/main/java/org/italiangrid/storm/webdav/oauth/validator/AudienceValidator.java index 1403f880..252d57cc 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/validator/AudienceValidator.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/validator/AudienceValidator.java @@ -16,7 +16,6 @@ package org.italiangrid.storm.webdav.oauth.validator; import static com.google.common.base.Preconditions.checkArgument; -import static java.util.Objects.isNull; import java.util.Set; @@ -45,7 +44,7 @@ public class AudienceValidator implements OAuth2TokenValidator { OAuth2TokenValidatorResult.failure(INVALID_AUDIENCE_ERROR); public AudienceValidator(AuthorizationServer server) { - checkArgument(!isNull(server.getAudiences()), "null audiences"); + checkArgument(server.getAudiences() != null, "null audiences"); checkArgument(!server.getAudiences().isEmpty(), "empty audiences"); requiredAudiences.addAll(server.getAudiences()); } @@ -53,7 +52,7 @@ public AudienceValidator(AuthorizationServer server) { @Override public OAuth2TokenValidatorResult validate(Jwt jwt) { - if (isNull(jwt.getAudience()) || jwt.getAudience().isEmpty()) { + if (jwt.getAudience() == null || jwt.getAudience().isEmpty()) { return SUCCESS; } @@ -65,7 +64,7 @@ public OAuth2TokenValidatorResult validate(Jwt jwt) { LOG.debug("Audience check failed. Token audience: {}, local audience: {}", jwt.getAudience(), requiredAudiences); - + return INVALID_AUDIENCE; } diff --git a/src/main/java/org/italiangrid/storm/webdav/oauth/validator/WlcgProfileValidator.java b/src/main/java/org/italiangrid/storm/webdav/oauth/validator/WlcgProfileValidator.java index 5c55ffa0..5b11a118 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oauth/validator/WlcgProfileValidator.java +++ b/src/main/java/org/italiangrid/storm/webdav/oauth/validator/WlcgProfileValidator.java @@ -16,7 +16,6 @@ package org.italiangrid.storm.webdav.oauth.validator; import static com.google.common.base.Strings.isNullOrEmpty; -import static java.util.Objects.isNull; import java.util.Collections; import java.util.Set; @@ -32,8 +31,9 @@ public class WlcgProfileValidator implements OAuth2TokenValidator { - public static final Logger LOG = LoggerFactory.getLogger(AudienceValidator.class); + public static final Logger LOG = LoggerFactory.getLogger(WlcgProfileValidator.class); + public static final String INVALID_TOKEN_ERROR_CODE = "invalid_token"; public static final String WLCG_VER_CLAIM = "wlcg.ver"; public static final String SCOPE_CLAIM = "scope"; @@ -41,25 +41,25 @@ public class WlcgProfileValidator implements OAuth2TokenValidator { Collections.unmodifiableSet(Sets.newHashSet("1.0")); private static final OAuth2Error INVALID_PROFILE_VERSION = - new OAuth2Error("invalid_token", "Unsupported WLCG token profile version", null); + new OAuth2Error(INVALID_TOKEN_ERROR_CODE, "Unsupported WLCG token profile version", null); private static final OAuth2Error MISSING_SCOPE = - new OAuth2Error("invalid_token", "scope claim not found in token", null); + new OAuth2Error(INVALID_TOKEN_ERROR_CODE, "scope claim not found in token", null); private static final OAuth2Error MISSING_NBF = - new OAuth2Error("invalid_token", "nbf claim not found in token", null); + new OAuth2Error(INVALID_TOKEN_ERROR_CODE, "nbf claim not found in token", null); private static final OAuth2Error MISSING_EXP = - new OAuth2Error("invalid_token", "exp claim not found in token", null); + new OAuth2Error(INVALID_TOKEN_ERROR_CODE, "exp claim not found in token", null); private static final OAuth2Error MISSING_SUB = - new OAuth2Error("invalid_token", "sub claim not found in token", null); + new OAuth2Error(INVALID_TOKEN_ERROR_CODE, "sub claim not found in token", null); private static final OAuth2Error MISSING_AUD = - new OAuth2Error("invalid_token", "aud claim not found in token", null); + new OAuth2Error(INVALID_TOKEN_ERROR_CODE, "aud claim not found in token", null); private static final OAuth2Error MISSING_JTI = - new OAuth2Error("invalid_token", "jti claim not found in token", null); + new OAuth2Error(INVALID_TOKEN_ERROR_CODE, "jti claim not found in token", null); private static final OAuth2TokenValidatorResult SUCCESS = OAuth2TokenValidatorResult.success(); @@ -77,27 +77,27 @@ public OAuth2TokenValidatorResult validate(Jwt token) { return OAuth2TokenValidatorResult.failure(INVALID_PROFILE_VERSION); } - if (Boolean.FALSE.equals(token.containsClaim(SCOPE_CLAIM))) { + if (!token.hasClaim(SCOPE_CLAIM)) { return OAuth2TokenValidatorResult.failure(MISSING_SCOPE); } - if (isNull(token.getNotBefore())) { + if (token.getNotBefore() == null) { return OAuth2TokenValidatorResult.failure(MISSING_NBF); } - if (isNull(token.getExpiresAt())) { + if (token.getExpiresAt() == null) { return OAuth2TokenValidatorResult.failure(MISSING_EXP); } - if (isNull(token.getSubject())) { + if (token.getSubject() == null) { return OAuth2TokenValidatorResult.failure(MISSING_SUB); } - if (isNull(token.getAudience()) || token.getAudience().isEmpty()) { + if (token.getAudience() == null || token.getAudience().isEmpty()) { return OAuth2TokenValidatorResult.failure(MISSING_AUD); } - - if (isNull(token.getId())) { + + if (token.getId() == null) { return OAuth2TokenValidatorResult.failure(MISSING_JTI); } diff --git a/src/main/java/org/italiangrid/storm/webdav/oidc/ClientRegistrationCacheLoader.java b/src/main/java/org/italiangrid/storm/webdav/oidc/ClientRegistrationCacheLoader.java index 61321d47..805ed969 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oidc/ClientRegistrationCacheLoader.java +++ b/src/main/java/org/italiangrid/storm/webdav/oidc/ClientRegistrationCacheLoader.java @@ -16,7 +16,6 @@ package org.italiangrid.storm.webdav.oidc; import static java.lang.String.format; -import static java.util.Objects.isNull; import java.util.Map; import java.util.concurrent.ExecutorService; @@ -71,10 +70,8 @@ private ClientRegistration getClientRegistration(String registrationId, map.from(properties::getAuthorizationGrantType) .as(AuthorizationGrantType::new) .to(builder::authorizationGrantType); - map.from(properties::getRedirectUri).to(builder::redirectUriTemplate); - map.from(properties::getScope) - .as((scope) -> StringUtils.toStringArray(scope)) - .to(builder::scope); + map.from(properties::getRedirectUri).to(builder::redirectUri); + map.from(properties::getScope).as(StringUtils::toStringArray).to(builder::scope); map.from(properties::getClientName).to(builder::clientName); return builder.build(); } @@ -111,7 +108,7 @@ private static Builder getBuilder(Builder builder, Provider provider) { public ClientRegistration load(String key) throws Exception { OAuth2ClientProperties.Registration reg = clientProperties.getRegistration().get(key); - if (isNull(reg)) { + if (reg == null) { return null; } diff --git a/src/main/java/org/italiangrid/storm/webdav/oidc/OidcGrantedAuthoritiesMapper.java b/src/main/java/org/italiangrid/storm/webdav/oidc/OidcGrantedAuthoritiesMapper.java index 6c5629b3..72a0e6fd 100644 --- a/src/main/java/org/italiangrid/storm/webdav/oidc/OidcGrantedAuthoritiesMapper.java +++ b/src/main/java/org/italiangrid/storm/webdav/oidc/OidcGrantedAuthoritiesMapper.java @@ -15,19 +15,18 @@ */ package org.italiangrid.storm.webdav.oidc; -import static java.util.Objects.isNull; - import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.Set; import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties; import org.italiangrid.storm.webdav.config.StorageAreaConfiguration; import org.italiangrid.storm.webdav.oauth.GrantedAuthoritiesMapperSupport; +import org.italiangrid.storm.webdav.oauth.authority.JwtClientAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtGroupAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtIssuerAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtSubjectAuthority; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; @@ -39,7 +38,6 @@ public class OidcGrantedAuthoritiesMapper extends GrantedAuthoritiesMapperSupport implements GrantedAuthoritiesMapper { - @Autowired public OidcGrantedAuthoritiesMapper(StorageAreaConfiguration conf, ServiceConfigurationProperties props) { super(conf, props); @@ -51,7 +49,7 @@ protected Collection grantGroupAuthorities(OidcUserAuthority u for (String groupClaimName : OAUTH_GROUP_CLAIM_NAMES) { List groups = userAuthority.getIdToken().getClaimAsStringList(groupClaimName); - if (!isNull(groups)) { + if (groups != null) { groups.stream() .map(g -> new JwtGroupAuthority(idTokenIssuer, g)) .forEach(groupAuthorities::add); @@ -68,9 +66,13 @@ protected Collection mapAuthorities(OidcUserAuthority userAuth authorities.addAll(authzMap.get(idTokenIssuer)); authorities.addAll(grantGroupAuthorities(userAuthority)); - authorities.add(new JwtIssuerAuthority(userAuthority.getIdToken().getIssuer().toString())); - authorities.add(new JwtSubjectAuthority(userAuthority.getIdToken().getIssuer().toString(), - userAuthority.getIdToken().getSubject())); + authorities.add(new JwtIssuerAuthority(idTokenIssuer)); + authorities.add(new JwtSubjectAuthority(idTokenIssuer, userAuthority.getIdToken().getSubject())); + Optional clientIdClaim = + Optional.ofNullable(userAuthority.getIdToken().getClaim("client_id")); + if (clientIdClaim.isPresent()) { + authorities.add(new JwtClientAuthority(idTokenIssuer, clientIdClaim.get())); + } return authorities; } diff --git a/src/main/java/org/italiangrid/storm/webdav/redirector/RedirectConstants.java b/src/main/java/org/italiangrid/storm/webdav/redirector/RedirectConstants.java index fa1d9fa6..90a24b21 100644 --- a/src/main/java/org/italiangrid/storm/webdav/redirector/RedirectConstants.java +++ b/src/main/java/org/italiangrid/storm/webdav/redirector/RedirectConstants.java @@ -15,8 +15,10 @@ */ package org.italiangrid.storm.webdav.redirector; -public interface RedirectConstants { +public final class RedirectConstants { - String ACCESS_TOKEN_PARAMETER = "access_token"; + public static final String ACCESS_TOKEN_PARAMETER = "access_token"; + + private RedirectConstants() {} } diff --git a/src/main/java/org/italiangrid/storm/webdav/redirector/RedirectFilter.java b/src/main/java/org/italiangrid/storm/webdav/redirector/RedirectFilter.java index 12390af5..7150b8e4 100644 --- a/src/main/java/org/italiangrid/storm/webdav/redirector/RedirectFilter.java +++ b/src/main/java/org/italiangrid/storm/webdav/redirector/RedirectFilter.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.nio.file.Path; -import java.util.Objects; import javax.servlet.Filter; import javax.servlet.FilterChain; @@ -36,7 +35,7 @@ import com.google.common.base.Strings; -public class RedirectFilter implements Filter, TpcUtils, RedirectConstants { +public class RedirectFilter implements Filter, TpcUtils { public static final String LOCATION = "Location"; public static final String NO_REDIRECT_QUERY_PARAM = "no_redirect"; @@ -60,7 +59,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha SecurityContext context = resolveSecurityContext(); if (isRedirectable(req)) { - + res.setHeader(LOCATION, service.buildRedirect(context.getAuthentication(), req, res)); res.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT); @@ -73,7 +72,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha private SecurityContext resolveSecurityContext() { SecurityContext context = SecurityContextHolder.getContext(); - if (Objects.isNull(context)) { + if (context == null) { throw new RedirectError("Failed to enstabilish a valid security context"); } @@ -94,7 +93,7 @@ private boolean isGetOrPutRequest(HttpServletRequest req) { private boolean requestDoesNotHaveAccessToken(HttpServletRequest req) { - String accessToken = req.getParameter(ACCESS_TOKEN_PARAMETER); + String accessToken = req.getParameter(RedirectConstants.ACCESS_TOKEN_PARAMETER); return Strings.isNullOrEmpty(accessToken); } @@ -103,7 +102,7 @@ private boolean requestedResourceExistsAndIsAFile(HttpServletRequest req) { String path = getSerlvetRequestPath(req); Path p = pathResolver.getPath(path); - if (Objects.isNull(p)) { + if (p == null) { return false; } diff --git a/src/main/java/org/italiangrid/storm/webdav/scitag/SciTag.java b/src/main/java/org/italiangrid/storm/webdav/scitag/SciTag.java new file mode 100644 index 00000000..1d39d2d1 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/scitag/SciTag.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.scitag; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SciTag { + + public static final Logger LOG = LoggerFactory.getLogger(SciTag.class); + public static final String SCITAG_HEADER = "SciTag"; + public static final String SCITAG_ATTRIBUTE = "scitag"; + + private final int experimentId; + private final int activityId; + private final boolean remoteAddressIsSource; + + public SciTag(int experimentId, int activityId, boolean remoteAddressIsSource) { + this.experimentId = experimentId; + this.activityId = activityId; + this.remoteAddressIsSource = remoteAddressIsSource; + } + + public int experimentId() { + return experimentId; + } + + public int activityId() { + return activityId; + } + + public boolean remoteAddressIsSource() { + return remoteAddressIsSource; + } + +} diff --git a/src/main/java/org/italiangrid/storm/webdav/scitag/SciTagTransfer.java b/src/main/java/org/italiangrid/storm/webdav/scitag/SciTagTransfer.java new file mode 100644 index 00000000..5a99d53a --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/scitag/SciTagTransfer.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.scitag; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SciTagTransfer { + + public static final Logger LOG = LoggerFactory.getLogger(SciTagTransfer.class); + public static final String SCITAG_TRANSFER_ATTRIBUTE = "scitagTransfer"; + + private static final String FLOWD_PIPE_NAME = "/var/run/flowd"; + private SciTag scitag; + private String sourceAddress; + private int sourcePort; + private String destinationAddress; + private int destinationPort; + private File flowdPipeFile; + + public SciTagTransfer(SciTag scitag, String localAddress, int localPort, String remoteAddress, + int remotePort) { + this(scitag, localAddress, localPort, remoteAddress, remotePort, new File(FLOWD_PIPE_NAME)); + } + + public SciTagTransfer(SciTag scitag, String localAddress, int localPort, String remoteAddress, + int remotePort, File flowdPipeFile) { + this.scitag = scitag; + if (scitag.remoteAddressIsSource()) { + this.sourceAddress = remoteAddress; + this.sourcePort = remotePort; + this.destinationAddress = localAddress; + this.destinationPort = localPort; + } else { + this.sourceAddress = localAddress; + this.sourcePort = localPort; + this.destinationAddress = remoteAddress; + this.destinationPort = remotePort; + } + this.flowdPipeFile = flowdPipeFile; + } + + private String flowdEntry() { + return " tcp " + sourceAddress + " " + sourcePort + " " + destinationAddress + " " + + destinationPort + " " + scitag.experimentId() + " " + scitag.activityId() + "\n"; + } + + public void writeStart() { + try (RandomAccessFile flowdPipe = new RandomAccessFile(flowdPipeFile, "rw")) { + flowdPipe.writeBytes("start" + this.flowdEntry()); + } catch (IOException e) { + LOG.warn(e.getMessage(), e); + } + } + + public void writeEnd() { + try (RandomAccessFile flowdPipe = new RandomAccessFile(flowdPipeFile, "rw")) { + flowdPipe.writeBytes("end" + this.flowdEntry()); + } catch (IOException e) { + LOG.warn(e.getMessage(), e); + } + } + +} diff --git a/src/main/java/org/italiangrid/storm/webdav/server/DefaultJettyServerCustomizer.java b/src/main/java/org/italiangrid/storm/webdav/server/DefaultJettyServerCustomizer.java index 4e798c4e..82c028e0 100644 --- a/src/main/java/org/italiangrid/storm/webdav/server/DefaultJettyServerCustomizer.java +++ b/src/main/java/org/italiangrid/storm/webdav/server/DefaultJettyServerCustomizer.java @@ -32,7 +32,6 @@ import org.italiangrid.storm.webdav.config.ServiceConfiguration; import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties; import org.italiangrid.storm.webdav.config.StorageAreaConfiguration; -import org.italiangrid.utils.jetty.TLSServerConnectorBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.web.ServerProperties; diff --git a/src/main/java/org/italiangrid/storm/webdav/server/DefaultPathResolver.java b/src/main/java/org/italiangrid/storm/webdav/server/DefaultPathResolver.java index c56d551d..a4415917 100644 --- a/src/main/java/org/italiangrid/storm/webdav/server/DefaultPathResolver.java +++ b/src/main/java/org/italiangrid/storm/webdav/server/DefaultPathResolver.java @@ -16,7 +16,6 @@ package org.italiangrid.storm.webdav.server; import static java.nio.file.LinkOption.NOFOLLOW_LINKS; -import static java.util.Objects.isNull; import java.nio.file.Files; import java.nio.file.Path; @@ -67,13 +66,13 @@ protected String stripContextPath(String context, String path) { @Override public String resolvePath(String pathInContext) { - if (isNull(pathInContext)) { + if (pathInContext == null) { return null; } Path p = getPath(pathInContext); - if (!isNull(p)) { + if (p != null) { return p.toString(); } @@ -102,7 +101,7 @@ public StorageAreaInfo resolveStorageArea(String pathInContext) { public boolean pathExists(String pathInContext) { String resolvedPath = resolvePath(pathInContext); - if (isNull(resolvedPath)) { + if (resolvedPath == null) { return false; } @@ -112,7 +111,7 @@ public boolean pathExists(String pathInContext) { @Override public Path getPath(String pathInContext) { - if (isNull(pathInContext)) { + if (pathInContext == null) { return null; } diff --git a/src/main/java/org/italiangrid/storm/webdav/server/DefaultWebServerFactory.java b/src/main/java/org/italiangrid/storm/webdav/server/DefaultWebServerFactory.java index b2fb8dfb..56d286cf 100644 --- a/src/main/java/org/italiangrid/storm/webdav/server/DefaultWebServerFactory.java +++ b/src/main/java/org/italiangrid/storm/webdav/server/DefaultWebServerFactory.java @@ -15,14 +15,16 @@ */ package org.italiangrid.storm.webdav.server; +import java.util.concurrent.ArrayBlockingQueue; + import org.italiangrid.storm.webdav.config.ServiceConfiguration; -import org.italiangrid.utils.jetty.ThreadPoolBuilder; import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.web.embedded.jetty.JettyServerCustomizer; import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory; import org.springframework.boot.web.server.WebServerFactoryCustomizer; import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.jetty9.InstrumentedQueuedThreadPool; public class DefaultWebServerFactory implements WebServerFactoryCustomizer { @@ -42,18 +44,19 @@ public DefaultWebServerFactory(ServiceConfiguration configuration, this.metricRegistry = registry; } + private InstrumentedQueuedThreadPool getInstrumentedThreadPool() { + InstrumentedQueuedThreadPool tPool = + new InstrumentedQueuedThreadPool(metricRegistry, configuration.getMaxConnections(), + configuration.getMinConnections(), configuration.getThreadPoolMaxIdleTimeInMsec(), + new ArrayBlockingQueue<>(configuration.getMaxQueueSize()), "storm.http"); + tPool.setName("thread-pool"); + return tPool; + } + @Override public void customize(JettyServletWebServerFactory factory) { - factory.setThreadPool(ThreadPoolBuilder.instance() - .withMaxRequestQueueSize(configuration.getMaxQueueSize()) - .withMaxThreads(serverProperties.getJetty().getThreads().getMax()) - .withMinThreads(serverProperties.getJetty().getThreads().getMin()) - .registry(metricRegistry) - .withPrefix("storm.http") - .withName("thread-pool") - .build()); - + factory.setThreadPool(getInstrumentedThreadPool()); factory.addServerCustomizers(serverCustomizer); } diff --git a/src/main/java/org/italiangrid/storm/webdav/server/TLSConnectorBuilderError.java b/src/main/java/org/italiangrid/storm/webdav/server/TLSConnectorBuilderError.java new file mode 100644 index 00000000..8d05acb8 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/server/TLSConnectorBuilderError.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.server; + +public class TLSConnectorBuilderError extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public TLSConnectorBuilderError(Throwable cause) { + super(cause); + } + + public TLSConnectorBuilderError(String message, Throwable cause) { + super(message, cause); + } + + public TLSConnectorBuilderError(String message) { + super(message); + } + +} diff --git a/src/main/java/org/italiangrid/storm/webdav/server/TLSServerConnectorBuilder.java b/src/main/java/org/italiangrid/storm/webdav/server/TLSServerConnectorBuilder.java new file mode 100644 index 00000000..5d735f3d --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/server/TLSServerConnectorBuilder.java @@ -0,0 +1,662 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.server; + +import java.io.File; +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import java.security.cert.CertificateException; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.conscrypt.OpenSSLProvider; +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http2.HTTP2Cipher; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.jetty9.InstrumentedConnectionFactory; + +import eu.emi.security.authn.x509.X509CertChainValidatorExt; +import eu.emi.security.authn.x509.helpers.ssl.SSLTrustManager; +import eu.emi.security.authn.x509.impl.PEMCredential; + +/** + * A builder that configures a Jetty server TLS connector integrated with CANL + * {@link X509CertChainValidatorExt} certificate validation services. + * + */ +public class TLSServerConnectorBuilder { + + /** + * Conscrypt provider name. + */ + public static final String CONSCRYPT_PROVIDER = "Conscrypt"; + + /** + * Default service certificate file. + */ + public static final String DEFAULT_CERTIFICATE_FILE = "/etc/grid-security/hostcert.pem"; + + /** + * Default service certificate private key file. + */ + public static final String DEFAULT_CERTIFICATE_KEY_FILE = "/etc/grid-security/hostcert.pem"; + + /** + * The port for this connector. + */ + private int port; + + /** + * The certificate file location. + */ + private String certificateFile = DEFAULT_CERTIFICATE_FILE; + + /** + * The certificate private key file location. + */ + private String certificateKeyFile = DEFAULT_CERTIFICATE_KEY_FILE; + + /** + * The password to decrypt the certificate private key file. + */ + private char[] certicateKeyPassword = null; + + /** + * The certificate validator used by this connector builder. + */ + private final X509CertChainValidatorExt certificateValidator; + + /** + * Whether client auth will be required for this connector. + */ + private boolean tlsNeedClientAuth = false; + + /** + * Whether cluent auth is supported for this connector. + */ + private boolean tlsWantClientAuth = true; + + /** + * Supported SSL protocols. + */ + private String[] includeProtocols; + + /** + * Disabled SSL protocols. + */ + private String[] excludeProtocols; + + /** + * Supported cipher suites. + */ + private String[] includeCipherSuites; + + /** + * Disabled cipher suites. + */ + private String[] excludeCipherSuites; + + /** + * The HTTP configuration for the connector being created. + */ + private HttpConfiguration httpConfiguration; + + /** + * The key manager to use for the connector being created. + */ + private KeyManager keyManager; + + /** + * The server for which the connector is being created. + */ + private final Server server; + + /** + * The metric name to associate to the connector being built. + */ + private String metricName; + + /** + * The metric registry. + */ + private MetricRegistry registry; + + /** + * Whether the Conscrypt provider should be used instead of the default JSSE implementation + */ + private boolean useConscrypt = false; + + + /** + * Whether HTTP/2 should be configured + */ + private boolean enableHttp2 = false; + + /** + * Which TLS protocol string should be used + */ + private String tlsProtocol = "TLSv1.2"; + + /** + * Custom TLS hostname verifier + */ + private HostnameVerifier hostnameVerifier = null; + + /** + * Disable JSSE hostname verification + */ + private boolean disableJsseHostnameVerification = false; + + /** + * Number of acceptors threads for the connector + */ + private int acceptors = -1; + + /** + * Number of selector threads for the connector + */ + private int selectors = -1; + + /** + * Returns an instance of the {@link TLSServerConnectorBuilder}. + * + * @param s the {@link Server} for which the connector is being created + * @param certificateValidator a {@link X509CertChainValidatorExt} used to validate certificates + * @return an instance of the {@link TLSServerConnectorBuilder} + */ + public static TLSServerConnectorBuilder instance(Server s, + X509CertChainValidatorExt certificateValidator) { + + return new TLSServerConnectorBuilder(s, certificateValidator); + } + + /** + * Private ctor. + * + * @param s the {@link Server} for which the connector is being created + * @param certificateValidator a {@link X509CertChainValidatorExt} used to validate certificates + */ + private TLSServerConnectorBuilder(Server s, X509CertChainValidatorExt certificateValidator) { + + if (s == null) { + throw new IllegalArgumentException("Server cannot be null"); + } + + if (certificateValidator == null) { + throw new IllegalArgumentException("certificateValidator cannot be null"); + } + + this.server = s; + this.certificateValidator = certificateValidator; + } + + private void credentialsSanityChecks() { + + checkFileExistsAndIsReadable(new File(certificateFile), "Error accessing certificate file"); + + checkFileExistsAndIsReadable(new File(certificateKeyFile), + "Error accessing certificate key file"); + + } + + private void loadCredentials() { + + credentialsSanityChecks(); + + PEMCredential serviceCredentials = null; + + try { + + serviceCredentials = + new PEMCredential(certificateKeyFile, certificateFile, certicateKeyPassword); + + } catch (KeyStoreException | CertificateException | IOException e) { + + throw new TLSConnectorBuilderError("Error setting up service credentials", e); + } + + keyManager = serviceCredentials.getKeyManager(); + } + + /** + * Configures SSL session parameters for the jetty {@link SslContextFactory}. + * + * @param contextFactory the {@link SslContextFactory} being configured + */ + private void configureContextFactory(SslContextFactory.Server contextFactory) { + + if (excludeProtocols != null) { + contextFactory.setExcludeProtocols(excludeProtocols); + } + + if (includeProtocols != null) { + contextFactory.setIncludeProtocols(includeProtocols); + } + + if (excludeCipherSuites != null) { + contextFactory.setExcludeCipherSuites(excludeCipherSuites); + } + + if (includeCipherSuites != null) { + contextFactory.setIncludeCipherSuites(includeCipherSuites); + } + + contextFactory.setWantClientAuth(tlsWantClientAuth); + contextFactory.setNeedClientAuth(tlsNeedClientAuth); + + if (useConscrypt) { + contextFactory.setProvider(CONSCRYPT_PROVIDER); + } else { + contextFactory.setProvider(BouncyCastleProvider.PROVIDER_NAME); + } + + if (hostnameVerifier != null) { + contextFactory.setHostnameVerifier(hostnameVerifier); + } + + if (disableJsseHostnameVerification) { + contextFactory.setEndpointIdentificationAlgorithm(null); + } + + } + + /** + * Builds a default {@link HttpConfiguration} for the TLS-enabled connector being created + * + * @return the default {@link HttpConfiguration} + */ + private HttpConfiguration defaultHttpConfiguration() { + + HttpConfiguration httpsConfig = new HttpConfiguration(); + + httpsConfig.setSecureScheme("https"); + + httpsConfig.setSecurePort(port); + + httpsConfig.setOutputBufferSize(32768); + httpsConfig.setRequestHeaderSize(8192); + httpsConfig.setResponseHeaderSize(8192); + + httpsConfig.setSendServerVersion(true); + httpsConfig.setSendDateHeader(false); + + httpsConfig.addCustomizer(new SecureRequestCustomizer()); + + return httpsConfig; + + } + + /** + * Gives access to the {@link HttpConfiguration} used for the TLS-enabled connector being created. + * If the configuration is not set, it creates it using {@link #defaultHttpConfiguration()}. + * + * @return the {@link HttpConfiguration} being used for the TLS-enabled connector. + */ + public HttpConfiguration httpConfiguration() { + + if (httpConfiguration == null) { + httpConfiguration = defaultHttpConfiguration(); + } + + return httpConfiguration; + + } + + /** + * Sets the port for the connector being created. + * + * @param port the port for the connector + * @return this builder + */ + public TLSServerConnectorBuilder withPort(int port) { + + this.port = port; + return this; + } + + /** + * Sets the certificate file for the connector being created. + * + * @param certificateFile the certificate file + * @return this builder + */ + public TLSServerConnectorBuilder withCertificateFile(String certificateFile) { + + this.certificateFile = certificateFile; + return this; + } + + /** + * Sets the certificate key file for the connector being created. + * + * @param certificateKeyFile the certificate key file + * @return this builder + */ + public TLSServerConnectorBuilder withCertificateKeyFile(String certificateKeyFile) { + + this.certificateKeyFile = certificateKeyFile; + return this; + } + + /** + * The the certificate key password for the connector being built + * + * @param certificateKeyPassword the certificate key password + * @return this builder + */ + public TLSServerConnectorBuilder withCertificateKeyPassword(char[] certificateKeyPassword) { + + this.certicateKeyPassword = certificateKeyPassword; + return this; + } + + /** + * Sets the {@link SslContextFactory#setNeedClientAuth(boolean)} parameter for the connector being + * created. + * + * @param needClientAuth true if client authentication is required + * @return this builder + */ + public TLSServerConnectorBuilder withNeedClientAuth(boolean needClientAuth) { + + this.tlsNeedClientAuth = needClientAuth; + return this; + } + + /** + * Sets the {@link SslContextFactory#setWantClientAuth(boolean)} parameter for the connector being + * created. + * + * @param wantClientAuth true if client authentication is wanted + * @return this builder + */ + public TLSServerConnectorBuilder withWantClientAuth(boolean wantClientAuth) { + + this.tlsWantClientAuth = wantClientAuth; + return this; + } + + /** + * Sets SSL included protocols. See {@link SslContextFactory#setIncludeProtocols(String...)}. + * + * @param includeProtocols the array of included protocol names + * @return this builder + */ + public TLSServerConnectorBuilder withIncludeProtocols(String... includeProtocols) { + + this.includeProtocols = includeProtocols; + return this; + } + + /** + * Sets SSL excluded protocols. See {@link SslContextFactory#setExcludeProtocols(String...)}. + * + * @param excludeProtocols the array of excluded protocol names + * @return this builder + */ + public TLSServerConnectorBuilder withExcludeProtocols(String... excludeProtocols) { + + this.excludeProtocols = excludeProtocols; + return this; + } + + /** + * Sets the SSL included cipher suites. + * + * @param includeCipherSuites the array of included cipher suites. + * @return this builder + */ + public TLSServerConnectorBuilder withIncludeCipherSuites(String... includeCipherSuites) { + + this.includeCipherSuites = includeCipherSuites; + return this; + } + + /** + * Sets the SSL ecluded cipher suites. + * + * @param excludeCipherSuites the array of excluded cipher suites. + * @return this builder + */ + public TLSServerConnectorBuilder withExcludeCipherSuites(String... excludeCipherSuites) { + + this.excludeCipherSuites = excludeCipherSuites; + return this; + } + + /** + * Sets the {@link HttpConfiguration} for the connector being built. + * + * @param conf the {@link HttpConfiguration} to use + * @return this builder + */ + public TLSServerConnectorBuilder withHttpConfiguration(HttpConfiguration conf) { + + this.httpConfiguration = conf; + return this; + } + + /** + * Sets the {@link KeyManager} for the connector being built. + * + * @param km the {@link KeyManager} to use + * @return this builder + */ + public TLSServerConnectorBuilder withKeyManager(KeyManager km) { + + this.keyManager = km; + return this; + } + + public TLSServerConnectorBuilder withConscrypt(boolean conscryptEnabled) { + this.useConscrypt = conscryptEnabled; + return this; + } + + public TLSServerConnectorBuilder withHttp2(boolean http2Enabled) { + this.enableHttp2 = http2Enabled; + return this; + } + + public TLSServerConnectorBuilder metricRegistry(MetricRegistry registry) { + this.registry = registry; + return this; + } + + public TLSServerConnectorBuilder metricName(String metricName) { + this.metricName = metricName; + return this; + } + + public TLSServerConnectorBuilder withTlsProtocol(String tlsProtocol) { + this.tlsProtocol = tlsProtocol; + return this; + } + + public TLSServerConnectorBuilder withHostnameVerifier(HostnameVerifier verifier) { + this.hostnameVerifier = verifier; + return this; + } + + public TLSServerConnectorBuilder withDisableJsseHostnameVerification( + boolean disableJsseHostnameVerification) { + this.disableJsseHostnameVerification = disableJsseHostnameVerification; + return this; + } + + public TLSServerConnectorBuilder withAcceptors(int acceptors) { + this.acceptors = acceptors; + return this; + } + + public TLSServerConnectorBuilder withSelectors(int selectors) { + this.selectors = selectors; + return this; + } + + private SSLContext buildSSLContext() { + + SSLContext sslCtx; + + try { + + KeyManager[] kms = new KeyManager[] {keyManager}; + SSLTrustManager tm = new SSLTrustManager(certificateValidator); + + if (useConscrypt) { + + if (Security.getProvider(CONSCRYPT_PROVIDER) == null) { + Security.addProvider(new OpenSSLProvider()); + } + + sslCtx = SSLContext.getInstance(tlsProtocol, CONSCRYPT_PROVIDER); + } else { + sslCtx = SSLContext.getInstance(tlsProtocol); + } + + sslCtx.init(kms, new TrustManager[] {tm}, null); + + } catch (NoSuchAlgorithmException e) { + + throw new TLSConnectorBuilderError("TLS protocol not supported: " + e.getMessage(), e); + } catch (KeyManagementException e) { + throw new TLSConnectorBuilderError(e); + } catch (NoSuchProviderException e) { + throw new TLSConnectorBuilderError("TLS provider error: " + e.getMessage(), e); + } + + return sslCtx; + } + + /** + * Builds a {@link ServerConnector} based on the {@link TLSServerConnectorBuilder} parameters + * + * @return a {@link ServerConnector} + */ + public ServerConnector build() { + + if (keyManager == null) { + loadCredentials(); + } + + SSLContext sslContext = buildSSLContext(); + SslContextFactory.Server cf = new SslContextFactory.Server(); + + cf.setSslContext(sslContext); + + + configureContextFactory(cf); + + if (httpConfiguration == null) { + httpConfiguration = defaultHttpConfiguration(); + } + + + HttpConnectionFactory httpConnFactory = new HttpConnectionFactory(httpConfiguration); + ConnectionFactory connFactory = null; + + if (registry != null) { + connFactory = new InstrumentedConnectionFactory(httpConnFactory, registry.timer(metricName)); + } else { + connFactory = httpConnFactory; + } + + + ConnectionFactory h2ConnFactory = null; + ServerConnector connector = null; + + if (enableHttp2) { + + HTTP2ServerConnectionFactory h2cf = new HTTP2ServerConnectionFactory(httpConfiguration); + + if (registry != null) { + h2ConnFactory = new InstrumentedConnectionFactory(h2cf, registry.timer(metricName)); + } else { + h2ConnFactory = h2cf; + } + ALPNServerConnectionFactory alpn = createAlpnProtocolFactory(httpConnFactory); + cf.setCipherComparator(HTTP2Cipher.COMPARATOR); + cf.setUseCipherSuitesOrder(true); + + SslConnectionFactory sslCf = new SslConnectionFactory(cf, alpn.getProtocol()); + + connector = new ServerConnector(server, acceptors, selectors, sslCf, alpn, h2ConnFactory, + httpConnFactory); + + } else { + + connector = new ServerConnector(server, acceptors, selectors, + new SslConnectionFactory(cf, HttpVersion.HTTP_1_1.asString()), connFactory); + } + + connector.setPort(port); + return connector; + } + + private ALPNServerConnectionFactory createAlpnProtocolFactory( + HttpConnectionFactory httpConnectionFactory) { + ALPNServerConnectionFactory alpn = new ALPNServerConnectionFactory(); + alpn.setDefaultProtocol(httpConnectionFactory.getProtocol()); + return alpn; + } + + + /** + * Checks that file exists and is readable. + * + * @param f the {@link File} to be checked + * @param prefix A prefix string for the error message, in case the file does not exist and is not + * readable + * @throws RuntimeException if the file does not exist or is not readable + */ + private void checkFileExistsAndIsReadable(File f, String prefix) { + + String errorMessage = null; + + if (!f.exists()) { + errorMessage = "File does not exists"; + } else if (!f.canRead()) { + errorMessage = "File is not readable"; + } else if (f.isDirectory()) { + errorMessage = "File is a directory"; + } + + if (errorMessage != null) { + String msg = String.format("%s: %s [%s]", prefix, errorMessage, f.getAbsolutePath()); + throw new TLSConnectorBuilderError(msg); + } + + } +} diff --git a/src/main/java/org/italiangrid/storm/webdav/server/servlet/LogRequestFilter.java b/src/main/java/org/italiangrid/storm/webdav/server/servlet/LogRequestFilter.java index 9334759e..1c3b3abd 100644 --- a/src/main/java/org/italiangrid/storm/webdav/server/servlet/LogRequestFilter.java +++ b/src/main/java/org/italiangrid/storm/webdav/server/servlet/LogRequestFilter.java @@ -16,6 +16,8 @@ package org.italiangrid.storm.webdav.server.servlet; import java.io.IOException; +import java.util.List; +import java.util.Objects; import java.util.Optional; import javax.servlet.Filter; @@ -33,10 +35,15 @@ import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; +import com.google.common.collect.Lists; + public class LogRequestFilter implements Filter { public static final Logger log = LoggerFactory.getLogger(LogRequestFilter.class); + private static final List IP_HEADERS = Lists.newArrayList("X-Forwarded-For", + "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"); + @Override public void destroy() {} @@ -56,7 +63,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; - String resMsg = String.format("%s %s %s %d [user:<%s>, authorities:<%s>]", req.getRemoteAddr(), + String resMsg = String.format("%s %s %s %d [user:<%s>, authorities:<%s>]", getClientIpAddr(req), req.getMethod(), req.getRequestURI(), res.getStatus(), authn.isPresent() ? authn.get().getName() : null, authn.isPresent() ? authn.get().getAuthorities() : null); @@ -64,6 +71,21 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha log.debug(resMsg); } + + public static String getClientIpAddr(HttpServletRequest request) { + + String remoteIp = request.getRemoteAddr(); + if (remoteIp != null) { + return remoteIp; + } + return IP_HEADERS.stream() + .map(request::getHeader) + .filter(Objects::nonNull) + .filter(ip -> !ip.isEmpty() && !ip.equalsIgnoreCase("unknown")) + .findFirst() + .orElse("???.???.???.???"); + } + @Override public void init(FilterConfig config) throws ServletException {} diff --git a/src/main/java/org/italiangrid/storm/webdav/server/servlet/MiltonFilter.java b/src/main/java/org/italiangrid/storm/webdav/server/servlet/MiltonFilter.java index 666ea2a4..813c805a 100644 --- a/src/main/java/org/italiangrid/storm/webdav/server/servlet/MiltonFilter.java +++ b/src/main/java/org/italiangrid/storm/webdav/server/servlet/MiltonFilter.java @@ -15,10 +15,7 @@ */ package org.italiangrid.storm.webdav.server.servlet; -import static java.util.Objects.isNull; - import java.io.IOException; -import java.util.HashSet; import java.util.Set; import javax.servlet.Filter; @@ -38,9 +35,12 @@ import org.italiangrid.storm.webdav.milton.StoRMResourceFactory; import org.italiangrid.storm.webdav.milton.util.ReplaceContentStrategy; import org.italiangrid.storm.webdav.server.PathResolver; +import org.italiangrid.storm.webdav.scitag.SciTag; +import org.italiangrid.storm.webdav.scitag.SciTagTransfer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; + +import com.google.common.collect.Sets; import io.milton.http.HttpManager; import io.milton.http.Request; @@ -51,7 +51,7 @@ public class MiltonFilter implements Filter { public static final Logger LOG = LoggerFactory.getLogger(MiltonFilter.class); - static final Set WEBDAV_METHOD_SET = new HashSet(); + static final Set WEBDAV_METHOD_SET = Sets.newHashSet(); static final String SA_ROOT_PATH = "sa-root"; static { @@ -72,7 +72,6 @@ public class MiltonFilter implements Filter { private final ReplaceContentStrategy rcs; - @Autowired public MiltonFilter(FilesystemAccess fsAccess, ExtendedAttributesHelper attrsHelper, PathResolver resolver, ReplaceContentStrategy rcs) { @@ -100,7 +99,7 @@ public void init(FilterConfig config) throws ServletException { servletContext = config.getServletContext(); - if (isNull(miltonHTTPManager)) { + if (miltonHTTPManager == null) { initMiltonHTTPManager(servletContext); } @@ -119,7 +118,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha doMilton((HttpServletRequest) request, (HttpServletResponse) response); } else chain.doFilter(request, response); - } public void doMilton(HttpServletRequest request, HttpServletResponse response) { @@ -128,16 +126,28 @@ public void doMilton(HttpServletRequest request, HttpServletResponse response) { try { // Is this really needed? - MiltonServlet.setThreadlocals((HttpServletRequest) request, (HttpServletResponse) response); + MiltonServlet.setThreadlocals(request, response); Request miltonReq = new StoRMMiltonRequest(request, servletContext); Response miltonRes = new io.milton.servlet.ServletResponse(response); + SciTag scitag = (SciTag) request.getAttribute(SciTag.SCITAG_ATTRIBUTE); + if (scitag != null) { + SciTagTransfer scitagTransfer = new SciTagTransfer(scitag, request.getLocalAddr(), + request.getLocalPort(), request.getRemoteAddr(), request.getRemotePort()); + scitagTransfer.writeStart(); + request.setAttribute(SciTagTransfer.SCITAG_TRANSFER_ATTRIBUTE, scitagTransfer); + } miltonHTTPManager.process(miltonReq, miltonRes); } finally { MiltonServlet.clearThreadlocals(); + SciTagTransfer scitagTransfer = + (SciTagTransfer) request.getAttribute(SciTagTransfer.SCITAG_TRANSFER_ATTRIBUTE); + if (scitagTransfer != null) { + scitagTransfer.writeEnd(); + } try { diff --git a/src/main/java/org/italiangrid/storm/webdav/server/servlet/SciTagFilter.java b/src/main/java/org/italiangrid/storm/webdav/server/servlet/SciTagFilter.java new file mode 100644 index 00000000..f01c0af1 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/server/servlet/SciTagFilter.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.server.servlet; + +import java.io.IOException; +import java.util.Optional; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; + +import org.italiangrid.storm.webdav.scitag.SciTag; +import org.italiangrid.storm.webdav.tpc.TransferConstants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SciTagFilter implements Filter { + + public static final Logger logger = LoggerFactory.getLogger(SciTagFilter.class); + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + if (req.getHeader(SciTag.SCITAG_HEADER) != null) { + Optional source = Optional.ofNullable(req.getHeader(TransferConstants.SOURCE_HEADER)); + boolean remoteAddressIsSource = + req.getMethod().equals("PUT") || (req.getMethod().equals("COPY") && source.isPresent()); + // state prot src_ip src_port dst_ip dst_port exp act + // If the active party receives an HTTP-TPC COPY request with a SciTag request header with + // a valid value then the server SHOULD mark the resulting network traffic with the + // experiment ID and activity ID encoded in the value. + int scitagValue = Integer.parseInt(req.getHeader(SciTag.SCITAG_HEADER)); + // Valid value is a single positive integer > 64 and <65536 (16bit). Any other value is + // considered invalid. + if (scitagValue > 64 && scitagValue < 65536) { + request.setAttribute(SciTag.SCITAG_ATTRIBUTE, + new SciTag(scitagValue >> 6, scitagValue & ((1 << 6) - 1), remoteAddressIsSource)); + } else { + // If the active party receives an HTTP-TPC COPY request with a SciTag request header + // with an invalid value then the server SHOULD mark the resulting network traffic with + // the 0 as the experiment ID and the activity ID. + request.setAttribute(SciTag.SCITAG_ATTRIBUTE, new SciTag(0, 0, remoteAddressIsSource)); + } + } + chain.doFilter(request, response); + } +} diff --git a/src/main/java/org/italiangrid/storm/webdav/server/servlet/StoRMServlet.java b/src/main/java/org/italiangrid/storm/webdav/server/servlet/StoRMServlet.java index 03f83598..68145aa7 100644 --- a/src/main/java/org/italiangrid/storm/webdav/server/servlet/StoRMServlet.java +++ b/src/main/java/org/italiangrid/storm/webdav/server/servlet/StoRMServlet.java @@ -34,7 +34,7 @@ public class StoRMServlet extends DefaultServlet { /** - * + * */ private static final long serialVersionUID = 4204673943980786498L; @@ -74,6 +74,12 @@ public Resource getResource(String pathInContext) { } + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + resourceService.doGet(request, response); + } + @Override protected void doHead(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { diff --git a/src/main/java/org/italiangrid/storm/webdav/server/servlet/resource/StormResourceService.java b/src/main/java/org/italiangrid/storm/webdav/server/servlet/resource/StormResourceService.java index 64684415..aa606090 100644 --- a/src/main/java/org/italiangrid/storm/webdav/server/servlet/resource/StormResourceService.java +++ b/src/main/java/org/italiangrid/storm/webdav/server/servlet/resource/StormResourceService.java @@ -18,6 +18,7 @@ import java.io.FileNotFoundException; import java.io.IOException; +import javax.servlet.ServletException; import javax.servlet.RequestDispatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -25,6 +26,8 @@ import org.eclipse.jetty.http.HttpContent; import org.eclipse.jetty.server.ResourceService; import org.eclipse.jetty.util.URIUtil; +import org.italiangrid.storm.webdav.scitag.SciTag; +import org.italiangrid.storm.webdav.scitag.SciTagTransfer; public class StormResourceService extends ResourceService { @@ -50,6 +53,23 @@ private String pathInContext(HttpServletRequest request) { return URIUtil.addPaths(servletPath, pathInfo); } + @Override + public boolean doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + SciTag scitag = (SciTag) request.getAttribute(SciTag.SCITAG_ATTRIBUTE); + SciTagTransfer scitagTransfer = null; + if (scitag != null) { + scitagTransfer = new SciTagTransfer(scitag, request.getLocalAddr(), request.getLocalPort(), + request.getRemoteAddr(), request.getRemotePort()); + scitagTransfer.writeStart(); + } + boolean result = super.doGet(request, response); + if (scitagTransfer != null) { + scitagTransfer.writeEnd(); + } + return result; + } + public boolean doHead(HttpServletRequest request, HttpServletResponse response) throws IOException { diff --git a/src/main/java/org/italiangrid/storm/webdav/server/util/CANLListener.java b/src/main/java/org/italiangrid/storm/webdav/server/util/CANLListener.java index 59b5f57e..872eb24e 100644 --- a/src/main/java/org/italiangrid/storm/webdav/server/util/CANLListener.java +++ b/src/main/java/org/italiangrid/storm/webdav/server/util/CANLListener.java @@ -35,13 +35,13 @@ public void loadingNotification(String location, String type, Severity level, Ex location = location.substring(5, location.length()); if (level.equals(Severity.ERROR)) { - LOG.error("Error for {} {}: {}.", new Object[] {type, location, cause.getMessage()}); + LOG.error("Error for {} {}: {}.", type, location, cause.getMessage()); } else if (level.equals(Severity.WARNING)) { - LOG.debug("Warning for {} {}: {}.", new Object[] {type, location, cause.getMessage()}); + LOG.debug("Warning for {} {}: {}.", type, location, cause.getMessage()); } else if (level.equals(Severity.NOTIFICATION)) { - LOG.debug("Loading {} {}.", new Object[] {type, location}); + LOG.debug("Loading {} {}.", type, location); } } diff --git a/src/main/java/org/italiangrid/storm/webdav/spring/AppConfig.java b/src/main/java/org/italiangrid/storm/webdav/spring/AppConfig.java index ce9a7a88..08b344a8 100644 --- a/src/main/java/org/italiangrid/storm/webdav/spring/AppConfig.java +++ b/src/main/java/org/italiangrid/storm/webdav/spring/AppConfig.java @@ -15,8 +15,7 @@ */ package org.italiangrid.storm.webdav.spring; -import static java.util.Objects.isNull; -import static org.italiangrid.utils.jetty.TLSServerConnectorBuilder.CONSCRYPT_PROVIDER; +import static org.italiangrid.storm.webdav.server.TLSServerConnectorBuilder.CONSCRYPT_PROVIDER; import java.io.IOException; import java.net.MalformedURLException; @@ -45,8 +44,6 @@ import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.LayeredConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; @@ -92,6 +89,8 @@ import org.italiangrid.storm.webdav.server.util.CANLListener; import org.italiangrid.storm.webdav.tpc.LocalURLService; import org.italiangrid.storm.webdav.tpc.StaticHostListLocalURLService; +import org.italiangrid.storm.webdav.tpc.TpcPlainConnectionSocketFactory; +import org.italiangrid.storm.webdav.tpc.TpcSSLConnectionSocketFactory; import org.italiangrid.storm.webdav.tpc.TransferConstants; import org.italiangrid.storm.webdav.tpc.http.SuperLaxRedirectStrategy; import org.italiangrid.voms.util.CertificateValidatorBuilder; @@ -126,7 +125,7 @@ import eu.emi.security.authn.x509.impl.PEMCredential; @Configuration -public class AppConfig implements TransferConstants { +public class AppConfig { public static final Logger LOG = LoggerFactory.getLogger(AppConfig.class); @@ -247,7 +246,7 @@ HttpClientConnectionManager tpcClientConnectionManager(ThirdPartyCopyProperties SSLContext ctx; if (props.isUseConscrypt()) { - if (isNull(Security.getProvider(CONSCRYPT_PROVIDER))) { + if (Security.getProvider(CONSCRYPT_PROVIDER) == null) { Security.addProvider(new OpenSSLProvider()); } ctx = SSLContext.getInstance(props.getTlsProtocol(), CONSCRYPT_PROVIDER); @@ -263,14 +262,14 @@ HttpClientConnectionManager tpcClientConnectionManager(ThirdPartyCopyProperties ctx.init(null, new TrustManager[] {tm}, null); } - ConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory(); - LayeredConnectionSocketFactory tlsSf = new SSLConnectionSocketFactory(ctx); + ConnectionSocketFactory sf = TpcPlainConnectionSocketFactory.getSocketFactory(); + LayeredConnectionSocketFactory tlsSf = new TpcSSLConnectionSocketFactory(ctx); Registry r = RegistryBuilder.create() - .register(HTTP, sf) - .register(HTTPS, tlsSf) - .register(DAV, sf) - .register(DAVS, tlsSf) + .register(TransferConstants.HTTP, sf) + .register(TransferConstants.HTTPS, tlsSf) + .register(TransferConstants.DAV, sf) + .register(TransferConstants.DAVS, tlsSf) .build(); PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(r); @@ -332,7 +331,7 @@ ClientRegistrationRepository clientRegistrationRepository( LOG.info("OpenID providers configuration will be refreshed every {} minutes", props.getRefreshPeriodMinutes()); - return (k) -> { + return k -> { try { return clients.get(k); } catch (ExecutionException e) { @@ -429,7 +428,7 @@ PathAuthorizationPdp fineGrainedAuthzPdpd(PathAuthorizationPolicyRepository repo @Bean @ConditionalOnProperty(name = "oauth.enable-oidc", havingValue = "false") ClientRegistrationRepository emptyClientRegistrationRepository() { - return (id) -> null; + return id -> null; } diff --git a/src/main/java/org/italiangrid/storm/webdav/spring/web/SecurityConfig.java b/src/main/java/org/italiangrid/storm/webdav/spring/web/SecurityConfig.java index 1ce8148b..0e85dbf0 100644 --- a/src/main/java/org/italiangrid/storm/webdav/spring/web/SecurityConfig.java +++ b/src/main/java/org/italiangrid/storm/webdav/spring/web/SecurityConfig.java @@ -44,6 +44,7 @@ import org.italiangrid.storm.webdav.authz.voters.FineGrainedAuthzVoter; import org.italiangrid.storm.webdav.authz.voters.FineGrainedCopyMoveAuthzVoter; import org.italiangrid.storm.webdav.authz.voters.LocalAuthzVoter; +import org.italiangrid.storm.webdav.authz.voters.MacaroonAuthzVoter; import org.italiangrid.storm.webdav.authz.voters.UnanimousDelegatedVoter; import org.italiangrid.storm.webdav.authz.voters.WlcgScopeAuthzCopyMoveVoter; import org.italiangrid.storm.webdav.authz.voters.WlcgScopeAuthzVoter; @@ -86,7 +87,7 @@ public class SecurityConfig { private static final Logger LOG = LoggerFactory.getLogger(SecurityConfig.class); - private final static List ALLOWED_METHODS; + private static final List ALLOWED_METHODS; static { ALLOWED_METHODS = Lists.newArrayList(); @@ -219,7 +220,7 @@ protected void addAnonymousAccessRules(HttpSecurity http) throws Exception { final List anonymousAccessPermissions = new ArrayList<>(); for (StorageAreaInfo sa : saConfiguration.getStorageAreaInfo()) { - if (Boolean.TRUE.equals(sa.anonymousReadEnabled())) { + if (sa.anonymousReadEnabled()) { anonymousAccessPermissions.add(SAPermission.canRead(sa.name())); } } @@ -282,6 +283,10 @@ protected AccessDecisionManager fineGrainedAccessDecisionManager() throws Malfor voters.add(new LocalAuthzVoter(serviceConfigurationProperties, pathResolver, new LocalAuthorizationPdp(serviceConfigurationProperties), localURLService)); } + if (serviceConfigurationProperties.getAuthzServer().isEnabled() + && serviceConfigurationProperties.getMacaroonFilter().isEnabled()) { + voters.add(new MacaroonAuthzVoter()); + } voters.add(new WebExpressionVoter()); voters.add(fineGrainedVoters); voters.add(wlcgVoters); diff --git a/src/main/java/org/italiangrid/storm/webdav/spring/web/ServletConfiguration.java b/src/main/java/org/italiangrid/storm/webdav/spring/web/ServletConfiguration.java index b6813811..23ce8b49 100644 --- a/src/main/java/org/italiangrid/storm/webdav/spring/web/ServletConfiguration.java +++ b/src/main/java/org/italiangrid/storm/webdav/spring/web/ServletConfiguration.java @@ -39,6 +39,7 @@ import org.italiangrid.storm.webdav.server.servlet.MiltonFilter; import org.italiangrid.storm.webdav.server.servlet.MoveRequestSanityChecksFilter; import org.italiangrid.storm.webdav.server.servlet.SAIndexServlet; +import org.italiangrid.storm.webdav.server.servlet.SciTagFilter; import org.italiangrid.storm.webdav.server.servlet.StoRMServlet; import org.italiangrid.storm.webdav.server.servlet.resource.StormResourceService; import org.italiangrid.storm.webdav.server.tracing.LogbackAccessAuthnInfoFilter; @@ -69,11 +70,12 @@ public class ServletConfiguration { public static final Logger LOG = LoggerFactory.getLogger(ServletConfiguration.class); static final int REQUEST_ID_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1000; - static final int LOGBACK_ACCESS_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1002; - static final int LOG_REQ_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1003; - static final int REDIRECT_REQ_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1004; - static final int CHECKSUM_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1005; - static final int MACAROON_REQ_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1006; + static final int LOGBACK_ACCESS_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1001; + static final int LOG_REQ_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1002; + static final int REDIRECT_REQ_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1003; + static final int CHECKSUM_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1004; + static final int MACAROON_REQ_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1005; + static final int SCITAG_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1006; static final int TPC_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1007; static final int MOVE_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1008; static final int DELETE_FILTER_ORDER = DEFAULT_FILTER_ORDER + 1009; @@ -119,8 +121,8 @@ FilterRegistrationBean redirectFilter(PathResolver pathResolver, RedirectionService redirectionService) { LOG.info("Redirector filter enabled"); - FilterRegistrationBean filter = new FilterRegistrationBean( - new RedirectFilter(pathResolver, redirectionService)); + FilterRegistrationBean filter = + new FilterRegistrationBean<>(new RedirectFilter(pathResolver, redirectionService)); filter.addUrlPatterns("/*"); filter.setOrder(REDIRECT_REQ_FILTER_ORDER); @@ -151,6 +153,15 @@ FilterRegistrationBean macaroonRequestFilter(ObjectMapper return filter; } + @Bean + @ConditionalOnProperty(name = "storm.scitag.enabled", havingValue = "true") + FilterRegistrationBean scitagFilter() { + LOG.info("SciTag filter enabled"); + FilterRegistrationBean filter = new FilterRegistrationBean<>(new SciTagFilter()); + filter.setOrder(SCITAG_FILTER_ORDER); + return filter; + } + @Bean FilterRegistrationBean miltonFilter(FilesystemAccess fsAccess, ExtendedAttributesHelper attrsHelper, PathResolver resolver, ReplaceContentStrategy rcs) { @@ -189,9 +200,9 @@ FilterRegistrationBean tpcFilter(Clock clock, FilesystemAccess f TransferClient metricsClient = new HttpTransferClientMetricsWrapper(registry, client); - FilterRegistrationBean tpcFilter = new FilterRegistrationBean<>( - new TransferFilter(clock, metricsClient, resolver, lus, props.isVerifyChecksum(), - props.getEnableExpectContinueThreshold())); + FilterRegistrationBean tpcFilter = + new FilterRegistrationBean<>(new TransferFilter(clock, metricsClient, resolver, lus, + props.isVerifyChecksum(), props.getEnableExpectContinueThreshold())); tpcFilter.addUrlPatterns("/*"); tpcFilter.setOrder(TPC_FILTER_ORDER); return tpcFilter; @@ -202,8 +213,7 @@ FilterRegistrationBean statsFilter(MetricRegistry regist PathResolver resolver) { FilterRegistrationBean filter = - new FilterRegistrationBean( - new StorageAreaStatsFilter(registry, resolver)); + new FilterRegistrationBean<>(new StorageAreaStatsFilter(registry, resolver)); filter.addUrlPatterns("/*"); filter.setOrder(STATS_FILTER_ORDER); return filter; diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/StaticHostListLocalURLService.java b/src/main/java/org/italiangrid/storm/webdav/tpc/StaticHostListLocalURLService.java index 10240e45..d405041f 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/StaticHostListLocalURLService.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/StaticHostListLocalURLService.java @@ -17,7 +17,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.Objects.isNull; import java.net.URI; import java.net.URISyntaxException; @@ -42,8 +41,7 @@ public boolean isLocalURL(String url) { URI uri = new URI(url); - if (isNull(uri.getScheme()) - || (!isNull(uri.getHost()) && uri.getHost().equals("localhost"))) { + if (uri.getScheme() == null || (uri.getHost() != null && uri.getHost().equals("localhost"))) { return true; } diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/TpcPlainConnectionSocketFactory.java b/src/main/java/org/italiangrid/storm/webdav/tpc/TpcPlainConnectionSocketFactory.java new file mode 100644 index 00000000..04289a9d --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/TpcPlainConnectionSocketFactory.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.tpc; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import org.apache.http.HttpHost; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.protocol.HttpContext; +import org.italiangrid.storm.webdav.scitag.SciTag; +import org.italiangrid.storm.webdav.scitag.SciTagTransfer; + +public class TpcPlainConnectionSocketFactory extends PlainConnectionSocketFactory { + + public static final TpcPlainConnectionSocketFactory INSTANCE = + new TpcPlainConnectionSocketFactory(); + + public static TpcPlainConnectionSocketFactory getSocketFactory() { + return INSTANCE; + } + + public TpcPlainConnectionSocketFactory() { + super(); + } + + @Override + public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, + InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) + throws IOException { + Socket s = + super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context); + SciTag scitag = (SciTag) context.getAttribute(SciTag.SCITAG_ATTRIBUTE); + if (scitag != null) { + SciTagTransfer scitagTransfer = + new SciTagTransfer(scitag, s.getLocalAddress().getHostAddress(), s.getLocalPort(), + s.getInetAddress().getHostAddress(), s.getPort()); + scitagTransfer.writeStart(); + context.setAttribute(SciTagTransfer.SCITAG_TRANSFER_ATTRIBUTE, scitagTransfer); + } + return s; + } +} diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/TpcSSLConnectionSocketFactory.java b/src/main/java/org/italiangrid/storm/webdav/tpc/TpcSSLConnectionSocketFactory.java new file mode 100644 index 00000000..d7660cb6 --- /dev/null +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/TpcSSLConnectionSocketFactory.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.tpc; + +import java.io.IOException; +import java.net.Socket; +import javax.net.ssl.SSLContext; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.protocol.HttpContext; +import org.italiangrid.storm.webdav.scitag.SciTag; +import org.italiangrid.storm.webdav.scitag.SciTagTransfer; + +public class TpcSSLConnectionSocketFactory extends SSLConnectionSocketFactory { + + public TpcSSLConnectionSocketFactory(SSLContext sslContext) { + super(sslContext); + } + + @Override + public Socket createLayeredSocket(Socket socket, String target, int port, HttpContext context) + throws IOException { + Socket s = super.createLayeredSocket(socket, target, port, context); + SciTag scitag = (SciTag) context.getAttribute(SciTag.SCITAG_ATTRIBUTE); + if (scitag != null) { + SciTagTransfer scitagTransfer = + new SciTagTransfer(scitag, s.getLocalAddress().getHostAddress(), s.getLocalPort(), + s.getInetAddress().getHostAddress(), s.getPort()); + scitagTransfer.writeStart(); + context.setAttribute(SciTagTransfer.SCITAG_TRANSFER_ATTRIBUTE, scitagTransfer); + } + return s; + } +} diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/TpcUtils.java b/src/main/java/org/italiangrid/storm/webdav/tpc/TpcUtils.java index 57ae36bc..5e7a1ec0 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/TpcUtils.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/TpcUtils.java @@ -30,7 +30,7 @@ import org.italiangrid.storm.webdav.error.ResourceNotFound; import org.italiangrid.storm.webdav.server.PathResolver; -public interface TpcUtils extends TransferConstants { +public interface TpcUtils { default Supplier resourceNotFoundError(String path) { return () -> new ResourceNotFound(format("No storage area found matching path: %s", path)); @@ -47,27 +47,29 @@ default String getSerlvetRequestPath(HttpServletRequest request) { } default String destinationHeader(HttpServletRequest request) { - return request.getHeader(DESTINATION_HEADER); + return request.getHeader(TransferConstants.DESTINATION_HEADER); } default boolean requestHasSourceHeader(HttpServletRequest request) { - return Optional.ofNullable(request.getHeader(SOURCE_HEADER)).isPresent(); + return Optional.ofNullable(request.getHeader(TransferConstants.SOURCE_HEADER)).isPresent(); } default boolean requestHasDestinationHeader(HttpServletRequest request) { - return Optional.ofNullable(request.getHeader(DESTINATION_HEADER)).isPresent(); + return Optional.ofNullable(request.getHeader(TransferConstants.DESTINATION_HEADER)).isPresent(); } default boolean requestHasLocalDestinationHeader(HttpServletRequest request, LocalURLService localURLService) { - Optional destination = Optional.ofNullable(request.getHeader(DESTINATION_HEADER)); + Optional destination = + Optional.ofNullable(request.getHeader(TransferConstants.DESTINATION_HEADER)); return (destination.isPresent() && localURLService.isLocalURL(destination.get())); } default boolean requestHasRemoteDestinationHeader(HttpServletRequest request, LocalURLService localURLService) { - Optional destination = Optional.ofNullable(request.getHeader(DESTINATION_HEADER)); + Optional destination = + Optional.ofNullable(request.getHeader(TransferConstants.DESTINATION_HEADER)); return (destination.isPresent() && !localURLService.isLocalURL(destination.get())); } @@ -112,7 +114,7 @@ default boolean requestHasTranferHeader(HttpServletRequest request) { Enumeration headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); - if (headerName.toLowerCase().startsWith(TRANSFER_HEADER_LC)) { + if (headerName.toLowerCase().startsWith(TransferConstants.TRANSFER_HEADER_LC)) { return true; } } @@ -130,7 +132,7 @@ default boolean isCopyOrMoveRequest(HttpServletRequest request) { } default String dropSlashWebdavFromPath(String path) { - Matcher m = WEBDAV_PATH_PATTERN.matcher(path); + Matcher m = TransferConstants.WEBDAV_PATH_PATTERN.matcher(path); if (m.matches()) { return String.format("/%s", m.group(1)); diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/TransferConstants.java b/src/main/java/org/italiangrid/storm/webdav/tpc/TransferConstants.java index fa6a55fb..64f0c25a 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/TransferConstants.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/TransferConstants.java @@ -20,32 +20,34 @@ import com.google.common.collect.ImmutableSet; -public interface TransferConstants { +public final class TransferConstants { - String AUTHORIZATION_HEADER = "Authorization"; - String CLIENT_INFO_HEADER = "ClientInfo"; - String SOURCE_HEADER = "Source"; - String DESTINATION_HEADER = "Destination"; - String OVERWRITE_HEADER = "Overwrite"; - String REQUIRE_CHECKSUM_HEADER = "RequireChecksumVerification"; - String CREDENTIAL_HEADER = "Credential"; + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String CLIENT_INFO_HEADER = "ClientInfo"; + public static final String SOURCE_HEADER = "Source"; + public static final String DESTINATION_HEADER = "Destination"; + public static final String OVERWRITE_HEADER = "Overwrite"; + public static final String REQUIRE_CHECKSUM_HEADER = "RequireChecksumVerification"; + public static final String CREDENTIAL_HEADER = "Credential"; - String CREDENTIAL_HEADER_NONE_VALUE = "none"; + public static final String CREDENTIAL_HEADER_NONE_VALUE = "none"; - String TRANSFER_HEADER = "TransferHeader"; - String TRANSFER_HEADER_LC = TRANSFER_HEADER.toLowerCase(); - int TRANFER_HEADER_LENGTH = TRANSFER_HEADER.length(); + public static final String TRANSFER_HEADER = "TransferHeader"; + public static final String TRANSFER_HEADER_LC = TRANSFER_HEADER.toLowerCase(); + public static final int TRANFER_HEADER_LENGTH = TRANSFER_HEADER.length(); - String HTTP = "http"; - String HTTPS = "https"; + public static final String HTTP = "http"; + public static final String HTTPS = "https"; - String DAV = "dav"; - String DAVS = "davs"; + public static final String DAV = "dav"; + public static final String DAVS = "davs"; - Set SUPPORTED_PROTOCOLS = + public static final Set SUPPORTED_PROTOCOLS = ImmutableSet.builder().add(HTTP, HTTPS, DAV, DAVS).build(); - String WEBDAV_PATH_REGEX = "/webdav/(.*)$"; - Pattern WEBDAV_PATH_PATTERN = Pattern.compile(WEBDAV_PATH_REGEX); + public static final String WEBDAV_PATH_REGEX = "/webdav/(.*)$"; + public static final Pattern WEBDAV_PATH_PATTERN = Pattern.compile(WEBDAV_PATH_REGEX); + + private TransferConstants() {} } diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilter.java b/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilter.java index d2dec61d..c40d2a56 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilter.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilter.java @@ -36,6 +36,7 @@ import org.apache.http.client.HttpResponseException; import org.italiangrid.storm.webdav.error.BadRequest; import org.italiangrid.storm.webdav.error.ResourceNotFound; +import org.italiangrid.storm.webdav.scitag.SciTag; import org.italiangrid.storm.webdav.server.PathResolver; import org.italiangrid.storm.webdav.server.tracing.RequestIdHolder; import org.italiangrid.storm.webdav.tpc.transfer.GetTransferRequest; @@ -107,11 +108,13 @@ protected void handleTpc(HttpServletRequest request, HttpServletResponse respons try { if (validRequest(request, response)) { - Optional source = Optional.ofNullable(request.getHeader(SOURCE_HEADER)); + Optional source = + Optional.ofNullable(request.getHeader(TransferConstants.SOURCE_HEADER)); + SciTag scitag = (SciTag) request.getAttribute(SciTag.SCITAG_ATTRIBUTE); if (source.isPresent()) { - handlePullCopy(request, response); + handlePullCopy(request, response, scitag); } else { - handlePushCopy(request, response); + handlePushCopy(request, response, scitag); } } @@ -121,7 +124,8 @@ protected void handleTpc(HttpServletRequest request, HttpServletResponse respons } private void setupLoggingContext(HttpServletRequest request) { - Optional clientInfoHeader = Optional.ofNullable(request.getHeader(CLIENT_INFO_HEADER)); + Optional clientInfoHeader = + Optional.ofNullable(request.getHeader(TransferConstants.CLIENT_INFO_HEADER)); if (clientInfoHeader.isPresent()) { try { @@ -145,8 +149,8 @@ protected void logTransferStart(GetTransferRequest req) { LOG.info( "Pull third-party transfer requested: Source: {}, Destination: {}, hasAuthorizationHeader: {}, id: {}", - req.remoteURI(), req.path(), req.transferHeaders().containsKey(AUTHORIZATION_HEADER), - req.uuid()); + req.remoteURI(), req.path(), + req.transferHeaders().containsKey(TransferConstants.AUTHORIZATION_HEADER), req.uuid()); if (LOG.isDebugEnabled()) { LOG.debug("{}", req); } @@ -155,8 +159,8 @@ protected void logTransferStart(GetTransferRequest req) { protected void logTransferStart(PutTransferRequest req) { LOG.info( "Push third-party transfer requested: Source: {}, Destination: {}, hasAuthorizationHeader: {}, id: {}", - req.path(), req.remoteURI(), req.transferHeaders().containsKey(AUTHORIZATION_HEADER), - req.uuid()); + req.path(), req.remoteURI(), + req.transferHeaders().containsKey(TransferConstants.AUTHORIZATION_HEADER), req.uuid()); if (LOG.isDebugEnabled()) { LOG.debug("{}", req); } @@ -210,10 +214,10 @@ protected void logTransferException(TransferRequest request, Exception e) { } } - protected void handlePullCopy(HttpServletRequest request, HttpServletResponse response) - throws IOException { + protected void handlePullCopy(HttpServletRequest request, HttpServletResponse response, + SciTag scitag) throws IOException { - URI uri = URI.create(request.getHeader(SOURCE_HEADER)); + URI uri = URI.create(request.getHeader(TransferConstants.SOURCE_HEADER)); String path = getScopedPathInfo(request); GetTransferRequest xferRequest = GetTransferRequestBuilder.create() @@ -221,6 +225,7 @@ protected void handlePullCopy(HttpServletRequest request, HttpServletResponse re .uri(uri) .path(path) .headers(getTransferHeaders(request, response)) + .scitag(scitag) .verifyChecksum(verifyChecksum && verifyChecksumRequested(request)) .overwrite(overwriteRequested(request)) .build(); @@ -249,9 +254,9 @@ protected void handlePullCopy(HttpServletRequest request, HttpServletResponse re } } - protected void handlePushCopy(HttpServletRequest request, HttpServletResponse response) - throws IOException { - URI uri = URI.create(request.getHeader(DESTINATION_HEADER)); + protected void handlePushCopy(HttpServletRequest request, HttpServletResponse response, + SciTag scitag) throws IOException { + URI uri = URI.create(request.getHeader(TransferConstants.DESTINATION_HEADER)); String path = getScopedPathInfo(request); PutTransferRequest xferRequest = PutTransferRequestBuilder.create() @@ -259,6 +264,7 @@ protected void handlePushCopy(HttpServletRequest request, HttpServletResponse re .uri(uri) .path(path) .headers(getTransferHeaders(request, response)) + .scitag(scitag) .verifyChecksum(verifyChecksum && verifyChecksumRequested(request)) .overwrite(overwriteRequested(request)) .build(); diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilterSupport.java b/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilterSupport.java index 5a087e9e..ad0b487d 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilterSupport.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/TransferFilterSupport.java @@ -16,7 +16,6 @@ package org.italiangrid.storm.webdav.tpc; import static java.lang.String.format; -import static java.util.Objects.isNull; import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; import static org.springframework.http.HttpStatus.BAD_REQUEST; @@ -34,6 +33,7 @@ import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpResponseException; +import org.italiangrid.storm.webdav.scitag.SciTag; import org.italiangrid.storm.webdav.server.PathResolver; import org.italiangrid.storm.webdav.tpc.transfer.TransferRequest; import org.italiangrid.storm.webdav.tpc.transfer.TransferStatus; @@ -46,7 +46,7 @@ import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; -public class TransferFilterSupport implements TransferConstants, TpcUtils { +public class TransferFilterSupport implements TpcUtils { public static final Logger LOG = LoggerFactory.getLogger(TransferFilterSupport.class); @@ -81,12 +81,20 @@ protected Multimap getTransferHeaders(HttpServletRequest request while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); - if (headerName.toLowerCase().startsWith(TRANSFER_HEADER_LC)) { - String xferHeaderName = headerName.substring(TRANFER_HEADER_LENGTH); + if (headerName.toLowerCase().startsWith(TransferConstants.TRANSFER_HEADER_LC)) { + String xferHeaderName = headerName.substring(TransferConstants.TRANFER_HEADER_LENGTH); if (xferHeaderName.trim().length() == 0) { LOG.warn("Ignoring invalid transfer header {}", headerName); continue; } + if (xferHeaderName.trim().equalsIgnoreCase(SciTag.SCITAG_HEADER) + && request.getHeader(SciTag.SCITAG_HEADER) != null) { + // If the active party receives an HTTP-TPC COPY request with both a SciTag request header + // and a TransferHeaderSciTag request header then it SHOULD ignore the + // TransferHeaderSciTag and continue to process the request. + LOG.warn("Ignoring TransferHeaderSciTag header because SciTag header is present"); + continue; + } xferHeaders.put(xferHeaderName.trim(), request.getHeader(headerName)); } } @@ -101,7 +109,7 @@ protected Multimap getTransferHeaders(HttpServletRequest request protected boolean verifyChecksumRequested(HttpServletRequest request) { Optional verifyChecksumFromHeader = - Optional.ofNullable(request.getHeader(REQUIRE_CHECKSUM_HEADER)); + Optional.ofNullable(request.getHeader(TransferConstants.REQUIRE_CHECKSUM_HEADER)); if (verifyChecksumFromHeader.isPresent()) { return "true".equals(verifyChecksumFromHeader.get()); @@ -111,7 +119,8 @@ protected boolean verifyChecksumRequested(HttpServletRequest request) { } protected boolean overwriteRequested(HttpServletRequest request) { - Optional overwrite = Optional.ofNullable(request.getHeader(OVERWRITE_HEADER)); + Optional overwrite = + Optional.ofNullable(request.getHeader(TransferConstants.OVERWRITE_HEADER)); if (overwrite.isPresent()) { return "T".equalsIgnoreCase(overwrite.get()); @@ -122,7 +131,7 @@ protected boolean overwriteRequested(HttpServletRequest request) { protected boolean isSupportedTransferURI(URI uri) { - return SUPPORTED_PROTOCOLS.contains(uri.getScheme()) && !isNull(uri.getPath()); + return TransferConstants.SUPPORTED_PROTOCOLS.contains(uri.getScheme()) && uri.getPath() != null; } protected boolean validTransferURI(String xferUri) { @@ -234,26 +243,33 @@ protected boolean validLocalDestinationPath(HttpServletRequest request, protected boolean validRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { - Optional source = Optional.ofNullable(request.getHeader(SOURCE_HEADER)); - Optional dest = Optional.ofNullable(request.getHeader(DESTINATION_HEADER)); - Optional overwrite = Optional.ofNullable(request.getHeader(OVERWRITE_HEADER)); - Optional checksum = Optional.ofNullable(request.getHeader(REQUIRE_CHECKSUM_HEADER)); - Optional credential = Optional.ofNullable(request.getHeader(CREDENTIAL_HEADER)); + Optional source = + Optional.ofNullable(request.getHeader(TransferConstants.SOURCE_HEADER)); + Optional dest = + Optional.ofNullable(request.getHeader(TransferConstants.DESTINATION_HEADER)); + Optional overwrite = + Optional.ofNullable(request.getHeader(TransferConstants.OVERWRITE_HEADER)); + Optional checksum = + Optional.ofNullable(request.getHeader(TransferConstants.REQUIRE_CHECKSUM_HEADER)); + Optional credential = + Optional.ofNullable(request.getHeader(TransferConstants.CREDENTIAL_HEADER)); if (source.isPresent() && dest.isPresent()) { invalidRequest(response, "Source and Destination headers are both present!"); return false; } - if (source.isPresent() && !validTransferURI(request.getHeader(SOURCE_HEADER))) { - invalidRequest(response, - format("Invalid %s header: %s", SOURCE_HEADER, request.getHeader(SOURCE_HEADER))); + if (source.isPresent() + && !validTransferURI(request.getHeader(TransferConstants.SOURCE_HEADER))) { + invalidRequest(response, format("Invalid %s header: %s", TransferConstants.SOURCE_HEADER, + request.getHeader(TransferConstants.SOURCE_HEADER))); return false; } - if (dest.isPresent() && !validTransferURI(request.getHeader(DESTINATION_HEADER))) { - invalidRequest(response, format("Invalid %s header: %s", DESTINATION_HEADER, - request.getHeader(DESTINATION_HEADER))); + if (dest.isPresent() + && !validTransferURI(request.getHeader(TransferConstants.DESTINATION_HEADER))) { + invalidRequest(response, format("Invalid %s header: %s", TransferConstants.DESTINATION_HEADER, + request.getHeader(TransferConstants.DESTINATION_HEADER))); return false; } @@ -276,7 +292,8 @@ protected boolean validRequest(HttpServletRequest request, HttpServletResponse r } if (invalidOverwrite) { - invalidRequest(response, format("Invalid %s header value: %s", OVERWRITE_HEADER, val)); + invalidRequest(response, + format("Invalid %s header value: %s", TransferConstants.OVERWRITE_HEADER, val)); return false; } } @@ -293,12 +310,13 @@ protected boolean validRequest(HttpServletRequest request, HttpServletResponse r if (invalidChecksum) { invalidRequest(response, - format("Invalid %s header value: %s", REQUIRE_CHECKSUM_HEADER, val)); + format("Invalid %s header value: %s", TransferConstants.REQUIRE_CHECKSUM_HEADER, val)); return false; } } - if (credential.isPresent() && !CREDENTIAL_HEADER_NONE_VALUE.equals(credential.get())) { + if (credential.isPresent() + && !TransferConstants.CREDENTIAL_HEADER_NONE_VALUE.equals(credential.get())) { invalidRequest(response, "Unsupported Credential header value: " + credential.get()); return false; } diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/http/GetResponseHandler.java b/src/main/java/org/italiangrid/storm/webdav/tpc/http/GetResponseHandler.java index 1a2941d6..98b16e66 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/http/GetResponseHandler.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/http/GetResponseHandler.java @@ -15,8 +15,6 @@ */ package org.italiangrid.storm.webdav.tpc.http; -import static java.util.Objects.isNull; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -70,7 +68,7 @@ private void writeEntityToStream(HttpEntity entity, OutputStream os) final InputStream inStream = entity.getContent(); - if (!isNull(inStream)) { + if (inStream != null) { try { int l; final byte[] tmp = new byte[bufferSize]; diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClient.java b/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClient.java index f89f0154..a4f77887 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClient.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/http/HttpTransferClient.java @@ -39,11 +39,14 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.impl.client.CloseableHttpClient; import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties; import org.italiangrid.storm.webdav.config.ThirdPartyCopyProperties; import org.italiangrid.storm.webdav.fs.attrs.ExtendedAttributesHelper; import org.italiangrid.storm.webdav.server.PathResolver; +import org.italiangrid.storm.webdav.scitag.SciTag; +import org.italiangrid.storm.webdav.scitag.SciTagTransfer; import org.italiangrid.storm.webdav.tpc.transfer.GetTransferRequest; import org.italiangrid.storm.webdav.tpc.transfer.PutTransferRequest; import org.italiangrid.storm.webdav.tpc.transfer.TransferClient; @@ -113,7 +116,7 @@ HttpGet prepareRequest(GetTransferRequest request) { return get; } - HttpPut prepareRequest(PutTransferRequest request, HttpEntity cfe) throws IOException { + HttpPut prepareRequest(PutTransferRequest request, HttpEntity cfe) { request.setTransferStatus(TransferStatus.builder(clock).inProgress(0)); @@ -165,15 +168,17 @@ public void handle(GetTransferRequest request, TransferStatusCallback cb) { StormCountingOutputStream os = prepareOutputStream(resolver.resolvePath(request.path())); HttpGet get = prepareRequest(request); + HttpClientContext context = HttpClientContext.create(); - ScheduledFuture reportTask = executorService.scheduleAtFixedRate(() -> { - reportStatus(cb, request, statusBuilder.inProgress(os.getCount())); - }, reportDelaySec, reportDelaySec, TimeUnit.SECONDS); + ScheduledFuture reportTask = executorService.scheduleAtFixedRate( + () -> reportStatus(cb, request, statusBuilder.inProgress(os.getCount())), reportDelaySec, + reportDelaySec, TimeUnit.SECONDS); try { + context.setAttribute(SciTag.SCITAG_ATTRIBUTE, request.scitag()); httpClient.execute(get, new GetResponseHandler(request, os, attributesHelper, - MDC.getCopyOfContextMap(), socketBufferSize, true)); + MDC.getCopyOfContextMap(), socketBufferSize, true), context); reportTask.cancel(true); reportStatus(cb, request, statusBuilder.done(os.getCount())); @@ -196,6 +201,11 @@ public void handle(GetTransferRequest request, TransferStatusCallback cb) { if (!reportTask.isCancelled()) { reportTask.cancel(true); } + SciTagTransfer scitagTransfer = + (SciTagTransfer) context.getAttribute(SciTagTransfer.SCITAG_TRANSFER_ATTRIBUTE); + if (scitagTransfer != null) { + scitagTransfer.writeEnd(); + } } } @@ -221,14 +231,9 @@ public void handle(PutTransferRequest request, TransferStatusCallback cb) { CountingFileEntity cfe = prepareFileEntity(resolver.resolvePath(request.path())); HttpPut put = null; + HttpClientContext context = HttpClientContext.create(); - try { - put = prepareRequest(request, cfe); - } catch (IOException e) { - logException(e); - reportStatus(cb, request, statusBuilder - .error(format("Error pushing %s: %s", request.remoteURI().toString(), e.getMessage()))); - } + put = prepareRequest(request, cfe); ScheduledFuture reportTask = executorService.scheduleAtFixedRate( () -> reportStatus(cb, request, statusBuilder.inProgress(cfe.getCount())), reportDelaySec, @@ -236,7 +241,8 @@ public void handle(PutTransferRequest request, TransferStatusCallback cb) { try { checkOverwrite(request); - httpClient.execute(put, new PutResponseHandler(MDC.getCopyOfContextMap())); + context.setAttribute(SciTag.SCITAG_ATTRIBUTE, request.scitag()); + httpClient.execute(put, new PutResponseHandler(MDC.getCopyOfContextMap()), context); reportTask.cancel(true); reportStatus(cb, request, statusBuilder.done(cfe.getCount())); } catch (HttpResponseException e) { @@ -255,6 +261,11 @@ public void handle(PutTransferRequest request, TransferStatusCallback cb) { if (!reportTask.isCancelled()) { reportTask.cancel(true); } + SciTagTransfer scitagTransfer = + (SciTagTransfer) context.getAttribute(SciTagTransfer.SCITAG_TRANSFER_ATTRIBUTE); + if (scitagTransfer != null) { + scitagTransfer.writeEnd(); + } } } diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/http/ResponseHandlerSupport.java b/src/main/java/org/italiangrid/storm/webdav/tpc/http/ResponseHandlerSupport.java index 759cd55d..7ef4bb7d 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/http/ResponseHandlerSupport.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/http/ResponseHandlerSupport.java @@ -15,8 +15,6 @@ */ package org.italiangrid.storm.webdav.tpc.http; -import static java.util.Objects.isNull; - import java.util.Map; import org.apache.http.StatusLine; @@ -30,9 +28,9 @@ public abstract class ResponseHandlerSupport { protected ResponseHandlerSupport(Map mdcContextMap) { this.mdcContextMap = mdcContextMap; } - + protected void setupMDC() { - if (!isNull(mdcContextMap)) { + if (mdcContextMap != null) { MDC.setContextMap(mdcContextMap); } } diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/GetTransferRequestBuilder.java b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/GetTransferRequestBuilder.java index a993ff9e..ea9107ec 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/GetTransferRequestBuilder.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/GetTransferRequestBuilder.java @@ -21,7 +21,7 @@ public class GetTransferRequestBuilder extends RequestBuilder { String uuid; @@ -28,6 +30,8 @@ public abstract class RequestBuilder { URI uri; + SciTag scitag; + boolean verifyChecksum = true; boolean overwrite = true; @@ -63,6 +67,11 @@ public RequestBuilder addHeader(String header, String value) { return this; } + public RequestBuilder scitag(SciTag scitag) { + this.scitag = scitag; + return this; + } + public RequestBuilder overwrite(boolean o) { overwrite = o; return this; diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/TransferRequest.java b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/TransferRequest.java index 5b149147..70e480e4 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/TransferRequest.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/TransferRequest.java @@ -22,6 +22,8 @@ import com.google.common.collect.Multimap; +import org.italiangrid.storm.webdav.scitag.SciTag; + public interface TransferRequest { String uuid(); @@ -32,6 +34,8 @@ public interface TransferRequest { Multimap transferHeaders(); + SciTag scitag(); + boolean verifyChecksum(); boolean overwrite(); diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/GetTransferRequestImpl.java b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/GetTransferRequestImpl.java index 7cb4e2e5..f0368061 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/GetTransferRequestImpl.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/GetTransferRequestImpl.java @@ -19,6 +19,7 @@ import java.net.URI; +import org.italiangrid.storm.webdav.scitag.SciTag; import org.italiangrid.storm.webdav.tpc.transfer.GetTransferRequest; import com.google.common.collect.Multimap; @@ -26,9 +27,9 @@ public class GetTransferRequestImpl extends TransferRequestImpl implements GetTransferRequest { public GetTransferRequestImpl(String uuid, String path, URI uri, - Multimap xferHeaders, - boolean verifyChecksum, boolean overwrite) { - super(uuid, path, uri, xferHeaders, verifyChecksum, overwrite); + Multimap xferHeaders, SciTag scitag, boolean verifyChecksum, + boolean overwrite) { + super(uuid, path, uri, xferHeaders, scitag, verifyChecksum, overwrite); } @Override diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/PutTransferRequestImpl.java b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/PutTransferRequestImpl.java index b2f4d37c..60f273eb 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/PutTransferRequestImpl.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/PutTransferRequestImpl.java @@ -19,6 +19,7 @@ import java.net.URI; +import org.italiangrid.storm.webdav.scitag.SciTag; import org.italiangrid.storm.webdav.tpc.transfer.PutTransferRequest; import com.google.common.collect.Multimap; @@ -26,8 +27,9 @@ public class PutTransferRequestImpl extends TransferRequestImpl implements PutTransferRequest { public PutTransferRequestImpl(String uuid, String path, URI uri, - Multimap xferHeaders, boolean verifyChecksum, boolean overwrite) { - super(uuid, path, uri, xferHeaders, verifyChecksum, overwrite); + Multimap xferHeaders, SciTag scitag, boolean verifyChecksum, + boolean overwrite) { + super(uuid, path, uri, xferHeaders, scitag, verifyChecksum, overwrite); } @Override diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/TransferRequestImpl.java b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/TransferRequestImpl.java index a1d29eb4..67100293 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/TransferRequestImpl.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/transfer/impl/TransferRequestImpl.java @@ -18,9 +18,9 @@ import java.net.URI; import java.time.Duration; import java.time.Instant; -import java.util.Objects; import java.util.Optional; +import org.italiangrid.storm.webdav.scitag.SciTag; import org.italiangrid.storm.webdav.tpc.transfer.TransferRequest; import org.italiangrid.storm.webdav.tpc.transfer.TransferStatus; import org.italiangrid.storm.webdav.tpc.transfer.TransferStatus.Status; @@ -40,6 +40,8 @@ public abstract class TransferRequestImpl implements TransferRequest { final Multimap xferHeaders; + final SciTag scitag; + final boolean verifyChecksum; final boolean overwrite; @@ -51,12 +53,13 @@ public abstract class TransferRequestImpl implements TransferRequest { private Optional lastTransferStatus = Optional.empty(); TransferRequestImpl(String uuid, String path, URI uri, Multimap xferHeaders, - boolean verifyChecksum, boolean overwrite) { + SciTag scitag, boolean verifyChecksum, boolean overwrite) { this.uuid = uuid; this.path = path; this.uri = uri; this.xferHeaders = xferHeaders; + this.scitag = scitag; this.verifyChecksum = verifyChecksum; this.overwrite = overwrite; } @@ -76,6 +79,11 @@ public Multimap transferHeaders() { return xferHeaders; } + @Override + public SciTag scitag() { + return scitag; + } + @Override public boolean verifyChecksum() { return verifyChecksum; @@ -156,7 +164,7 @@ public long bytesTransferred() { @Override public Duration duration() { - if (Objects.isNull(startTime) || Objects.isNull(endTime)) { + if (startTime == null || endTime == null) { LOG.debug("Duration called before end of trasnfer, will return ZERO"); return Duration.ZERO; } diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/utils/Adler32DigestHeaderHelper.java b/src/main/java/org/italiangrid/storm/webdav/tpc/utils/Adler32DigestHeaderHelper.java index 6c6d8f32..c75c991f 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/utils/Adler32DigestHeaderHelper.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/utils/Adler32DigestHeaderHelper.java @@ -31,6 +31,8 @@ public class Adler32DigestHeaderHelper { public static final String DIGEST_HEADER_REGEX = "^\\s*adler32\\s*=\\s*([0-9a-zA-Z]{8})\\s*"; public static final Pattern DIGEST_HEADER_PATTERN = Pattern.compile(DIGEST_HEADER_REGEX); + private Adler32DigestHeaderHelper() {} + public static Optional extractAdler32DigestFromResponse(HttpResponse response) { checkNotNull(response); @@ -40,7 +42,7 @@ public static Optional extractAdler32DigestFromResponse(HttpResponse res if (digestHeader.isPresent()) { String digestHeaderValue = digestHeader.get().getValue(); - + if (!isNullOrEmpty(digestHeaderValue)) { Matcher m = DIGEST_HEADER_PATTERN.matcher(digestHeaderValue); diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/utils/ClientInfo.java b/src/main/java/org/italiangrid/storm/webdav/tpc/utils/ClientInfo.java index a3ceca4d..1e099704 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/utils/ClientInfo.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/utils/ClientInfo.java @@ -27,8 +27,10 @@ public class ClientInfo { + private static final String INVALID_CLIENTINFO_HEADER_MESSAGE = "Invalid ClientInfo header: %s"; + public static final String CLIENT_INFO_MDC_KEY = "tpc.clientInfo"; - + public static final String JOB_ID_KEY = "job-id"; public static final String FILE_ID_KEY = "file-id"; public static final String RETRY_COUNT_KEY = "retry"; @@ -59,13 +61,13 @@ public int getRetryCount() { } public static ClientInfo fromHeaderString(String headerString) { - checkArgument(!isNullOrEmpty(headerString), "Invalid ClientInfo header: %s", headerString); + checkArgument(!isNullOrEmpty(headerString), INVALID_CLIENTINFO_HEADER_MESSAGE, headerString); Map splitResult = SPLITTER.split(headerString); - checkArgument(splitResult.containsKey(JOB_ID_KEY), "Invalid ClientInfo header: %s", + checkArgument(splitResult.containsKey(JOB_ID_KEY), INVALID_CLIENTINFO_HEADER_MESSAGE, headerString); - checkArgument(splitResult.containsKey(FILE_ID_KEY), "Invalid ClientInfo header: %s", + checkArgument(splitResult.containsKey(FILE_ID_KEY), INVALID_CLIENTINFO_HEADER_MESSAGE, headerString); - checkArgument(splitResult.containsKey(RETRY_COUNT_KEY), "Invalid ClientInfo header: %s", + checkArgument(splitResult.containsKey(RETRY_COUNT_KEY), INVALID_CLIENTINFO_HEADER_MESSAGE, headerString); return new ClientInfo(splitResult.get(JOB_ID_KEY), splitResult.get(FILE_ID_KEY), Integer.parseInt(splitResult.get(RETRY_COUNT_KEY))); @@ -74,6 +76,6 @@ public static ClientInfo fromHeaderString(String headerString) { public void addToMDC() { final String ciStr = String.format("job-id:%s,file-id:%s,retry:%d", jobId, fileId, retryCount); MDC.put(CLIENT_INFO_MDC_KEY, ciStr); - + } } diff --git a/src/main/java/org/italiangrid/storm/webdav/tpc/utils/UrlHelper.java b/src/main/java/org/italiangrid/storm/webdav/tpc/utils/UrlHelper.java index 5462e239..6dc72458 100644 --- a/src/main/java/org/italiangrid/storm/webdav/tpc/utils/UrlHelper.java +++ b/src/main/java/org/italiangrid/storm/webdav/tpc/utils/UrlHelper.java @@ -15,8 +15,6 @@ */ package org.italiangrid.storm.webdav.tpc.utils; -import static java.util.Objects.isNull; - import java.net.URI; import java.net.URISyntaxException; @@ -31,11 +29,7 @@ public static boolean isRemoteUrl(String url) { URI uri = new URI(url); - if (isNull(uri.getScheme())) { - return false; - } - - return true; + return uri.getScheme() != null; } catch (URISyntaxException e) { return false; diff --git a/src/main/java/org/italiangrid/storm/webdav/utils/RangeCopyHelper.java b/src/main/java/org/italiangrid/storm/webdav/utils/RangeCopyHelper.java index c442df09..cef6fa60 100644 --- a/src/main/java/org/italiangrid/storm/webdav/utils/RangeCopyHelper.java +++ b/src/main/java/org/italiangrid/storm/webdav/utils/RangeCopyHelper.java @@ -28,6 +28,8 @@ public class RangeCopyHelper { + private RangeCopyHelper() {} + public static long rangeCopy(InputStream is, File f, long rangeStart, long rangeCount) throws IOException { checkNotNull(is); diff --git a/src/main/java/org/italiangrid/storm/webdav/web/ViewUtilsInterceptor.java b/src/main/java/org/italiangrid/storm/webdav/web/ViewUtilsInterceptor.java index f4df47dc..f1b54e2b 100644 --- a/src/main/java/org/italiangrid/storm/webdav/web/ViewUtilsInterceptor.java +++ b/src/main/java/org/italiangrid/storm/webdav/web/ViewUtilsInterceptor.java @@ -25,9 +25,9 @@ import org.italiangrid.storm.webdav.server.servlet.SAIndexServlet; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import org.springframework.web.servlet.HandlerInterceptor; -public class ViewUtilsInterceptor extends HandlerInterceptorAdapter { +public class ViewUtilsInterceptor implements HandlerInterceptor { final ServiceConfigurationProperties serviceConfig; final StorageAreaConfiguration saConfig; diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 6b1c7356..8d9d02e0 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -2,8 +2,19 @@ server: jetty: accesslog: enabled: false + +management: + # endpoint: + # env: + # additional-keys-to-sanitize: client-secret + endpoints: + web: + exposure: + include: env + oauth: enable-oidc: false + storm: connector: port: 8086 diff --git a/src/main/resources/application-fga.yml b/src/main/resources/application-fga.yml new file mode 100644 index 00000000..7a207fb3 --- /dev/null +++ b/src/main/resources/application-fga.yml @@ -0,0 +1,51 @@ +server: + jetty: + accesslog: + enabled: false + +management: + endpoints: + web: + exposure: + include: env + +oauth: + enable-oidc: false + issuers: + - name: iam-dev + issuer: https://iam-dev.cloud.cnaf.infn.it/ + +storm: + connector: + port: 8086 + securePort: 9443 + sa: + config-dir: src/test/resources/conf/sa.d + tls: + trust-anchors-dir: src/test/resources/trust-anchors + certificate-path: src/test/resources/hostcert/hostcert.pem + private-key-path: src/test/resources/hostcert/hostkey.pem + authz-server: + enabled: true + voms: + trust-store: + dir: src/test/resources/vomsdir + tape: + well-known: + source: src/test/resources/well-known/wlcg-tape-rest-api.json + + authz: + policies: + - sa: fga + actions: + - all + effect: permit + description: Grant read/write access to a specific client + paths: + - /** + principals: + - type: jwt-client + params: + iss: https://iam-dev.cloud.cnaf.infn.it/ + id: 42999a63-7449-43fb-952e-42f2d75b865b + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6eaaa236..e18c7e51 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,16 +11,19 @@ spring: issuer-uri: https://iam-escape.cloud.cnaf.infn.it/ indigo: issuer-uri: https://iam-test.indigo-datacloud.eu/ + session: store-type: none server: - # StoRM webdav will bind on this address + # StoRM WebDAV will bind on this address address: ${STORM_WEBDAV_SERVER_ADDRESS:0.0.0.0} + # StoRM WebDAV server should support graceful shutdown, allowing active requests time to complete, or shut down immediately + # Values: graceful, immediate + shutdown: ${STORM_WEBDAV_SERVER_SHUTDOWN:graceful} error: whitelabel: enabled: false - jetty: threads: max: ${storm.connector.max-connections} @@ -28,7 +31,7 @@ server: management: health: redis: - enabled: false + enabled: false tpc: tls-protocol: ${STORM_WEBDAV_TPC_TLS_PROTOCOL:TLSv1.2} @@ -50,7 +53,8 @@ tpc: enable-expect-continue-threshold: ${STORM_WEBDAV_TPC_ENABLE_EXPECT_CONTINUE_THRESHOLD:1048576} oauth: - refresh-period-minutes: 60 + refresh-period-minutes: ${STORM_WEBDAV_OAUTH_REFRESH_PERIOD_MINUTES:60} + refresh-timeout-seconds: ${STORM_WEBDAV_OAUTH_REFRESH_TIMEOUT_SECONDS:30} issuers: storm: @@ -81,18 +85,31 @@ storm: macaroon-filter: enabled: ${STORM_WEBDAV_MACAROON_FILTER_ENABLED:true} + scitag: + enabled: ${STORM_WEBDAV_SCITAG_ENABLED:false} + + server: + # Jetty Thread-Pool maximum idle time (in milliseconds) + max-idle-time-msec: ${STORM_WEBDAV_SERVER_MAX_IDLE_TIME:3600000} + connector: # HTTP connector port port: ${STORM_WEBDAV_HTTP_PORT:8085} # HTTPS connector port secure-port: ${STORM_WEBDAV_HTTPS_PORT:8443} + # Min concurrent connections + min-connections: ${STORM_WEBDAV_MIN_CONNECTIONS:50} # Max concurrent connections max-connections: ${STORM_WEBDAV_MAX_CONNECTIONS:300} # Connection queue size max-queue-size: ${STORM_WEBDAV_MAX_QUEUE_SIZE:900} # Connector Maximum idle time (in milliseconds) - max-idle-time-msec: ${STORM_WEBDAV_CONNECTOR_MAX_IDLE_TIME:30000} + max-idle-time-msec: ${STORM_WEBDAV_CONNECTOR_MAX_IDLE_TIME:30000} output-buffer-size-bytes: ${storm.buffer.file-buffer-size-bytes} + # Number of acceptor threads to use. When the value is -1, the default, the number of acceptors is derived from the operating environment. + jetty-acceptors: ${STORM_WEBDAV_CONNECTOR_ACCEPTORS:-1} + # Number of selector threads to use. When the value is -1, the default, the number of selectors is derived from the operating environment. + jetty-selectors: ${STORM_WEBDAV_CONNECTOR_SELECTORS:-1} tls: # Path to the service certificate. @@ -158,4 +175,5 @@ storm: tape: well-known: - source: ${STORM_WEBDAV_TAPE_WELLKNOWN_SOURCE:/etc/storm/webdav/wlcg-tape-rest-api.json} \ No newline at end of file + source: ${STORM_WEBDAV_TAPE_WELLKNOWN_SOURCE:/etc/storm/webdav/wlcg-tape-rest-api.json} + diff --git a/src/main/resources/templates/400.html b/src/main/resources/templates/400.html index 14d78a16..9bf8a90e 100644 --- a/src/main/resources/templates/400.html +++ b/src/main/resources/templates/400.html @@ -1,5 +1,5 @@ - Bad request! @@ -10,4 +10,4 @@

Bad request!

The request you submitted is malformed!

Go back to the storage area index page - \ No newline at end of file + diff --git a/src/main/resources/templates/401.html b/src/main/resources/templates/401.html index dee6c977..b6cf5c92 100644 --- a/src/main/resources/templates/401.html +++ b/src/main/resources/templates/401.html @@ -1,5 +1,5 @@ - Access denied! @@ -10,4 +10,4 @@

Unauthorized!

You are not authenticated and are trying to access content that requires authenticated access!

Go back to the storage area index page - \ No newline at end of file + diff --git a/src/main/resources/templates/403.html b/src/main/resources/templates/403.html index bdf4065d..3b7c6306 100644 --- a/src/main/resources/templates/403.html +++ b/src/main/resources/templates/403.html @@ -1,5 +1,5 @@ - Access denied! @@ -10,4 +10,4 @@

Forbidden!

You do not have the rights to access the requested resources!

Go back to the storage area index page - \ No newline at end of file + diff --git a/src/main/resources/templates/404.html b/src/main/resources/templates/404.html index a895f025..18bc284a 100644 --- a/src/main/resources/templates/404.html +++ b/src/main/resources/templates/404.html @@ -1,5 +1,5 @@ - Not found! @@ -10,4 +10,4 @@

Not found!

We could not find the resource you're looking for...

Go back to the storage area index page - \ No newline at end of file + diff --git a/src/main/resources/templates/405.html b/src/main/resources/templates/405.html index 4a228df8..6200622f 100644 --- a/src/main/resources/templates/405.html +++ b/src/main/resources/templates/405.html @@ -1,5 +1,5 @@ - Access denied! @@ -10,4 +10,4 @@

Request method not allowed!

The http method in your request is not allowed for the target resource!

Go back to the storage area index page - \ No newline at end of file + diff --git a/src/main/resources/templates/authn-info.html b/src/main/resources/templates/authn-info.html index 8c9570cc..3980a10a 100644 --- a/src/main/resources/templates/authn-info.html +++ b/src/main/resources/templates/authn-info.html @@ -1,5 +1,5 @@ - Authentication info @@ -26,4 +26,4 @@ Go back to the storage area index page - \ No newline at end of file + diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html index 3e127e9b..438b6d21 100644 --- a/src/main/resources/templates/error.html +++ b/src/main/resources/templates/error.html @@ -1,5 +1,5 @@ - Error! @@ -10,4 +10,4 @@

Ops!

Something went wrong, and this page is not very informative

Go back to the storage area index page - \ No newline at end of file + diff --git a/src/main/resources/templates/jetty-dir.html b/src/main/resources/templates/jetty-dir.html index 6963c475..b06c5b35 100644 --- a/src/main/resources/templates/jetty-dir.html +++ b/src/main/resources/templates/jetty-dir.html @@ -1,5 +1,5 @@ - Directory @@ -31,4 +31,4 @@

Directory

- \ No newline at end of file + diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index 8f38e1f5..b5689f46 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -1,5 +1,5 @@ - + StoRM WebDAV @@ -39,4 +39,4 @@

- \ No newline at end of file + diff --git a/src/main/resources/templates/oidc-login.html b/src/main/resources/templates/oidc-login.html index 0cdb116e..4b4843bd 100644 --- a/src/main/resources/templates/oidc-login.html +++ b/src/main/resources/templates/oidc-login.html @@ -1,5 +1,5 @@ - StoRM WebDAV service - Login @@ -24,4 +24,4 @@

Login error

Go back to the storage area index page - \ No newline at end of file + diff --git a/src/main/resources/templates/sa-index.html b/src/main/resources/templates/sa-index.html index 560b8174..2f2820a9 100644 --- a/src/main/resources/templates/sa-index.html +++ b/src/main/resources/templates/sa-index.html @@ -1,5 +1,5 @@ - Storage areas index @@ -18,4 +18,4 @@

Storage areas:

- \ No newline at end of file + diff --git a/src/test/java/org/italiangrid/storm/webdav/server/TLSConnectorBuilderTest.java b/src/test/java/org/italiangrid/storm/webdav/server/TLSConnectorBuilderTest.java new file mode 100644 index 00000000..b7fcbba9 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/server/TLSConnectorBuilderTest.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.server; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertThrows; + +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.KeyManager; + +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.italiangrid.storm.webdav.server.util.CANLListener; +import org.italiangrid.voms.util.CertificateValidatorBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import eu.emi.security.authn.x509.CrlCheckingMode; +import eu.emi.security.authn.x509.NamespaceCheckingMode; +import eu.emi.security.authn.x509.OCSPCheckingMode; +import eu.emi.security.authn.x509.X509CertChainValidatorExt; + +@ExtendWith(MockitoExtension.class) +class TLSConnectorBuilderTest { + + @Test + void tlsConnectorBuilderErrorTests() { + + TLSConnectorBuilderError e = new TLSConnectorBuilderError("This is an error!"); + assertThat(e.getMessage(), is("This is an error!")); + e = new TLSConnectorBuilderError("This is an error!", new RuntimeException()); + assertThat(e.getMessage(), is("This is an error!")); + e = new TLSConnectorBuilderError(new RuntimeException("This is an error!")); + assertThat(e.getCause() instanceof RuntimeException, is(true)); + assertThat(e.getMessage(), containsString("This is an error!")); + } + + @Test + void illegalArgumentExceptionThrown() { + + Server server = Mockito.mock(Server.class); + X509CertChainValidatorExt validator = Mockito.mock(X509CertChainValidatorExt.class); + + assertThrows(IllegalArgumentException.class, () -> { + TLSServerConnectorBuilder.instance(null, validator); + }); + assertThrows(IllegalArgumentException.class, () -> { + TLSServerConnectorBuilder.instance(server, null); + }); + assertThrows(IllegalArgumentException.class, () -> { + TLSServerConnectorBuilder.instance(null, null); + }); + } + + private X509CertChainValidatorExt getValidator() { + + CANLListener l = new org.italiangrid.storm.webdav.server.util.CANLListener(); + CertificateValidatorBuilder builder = new CertificateValidatorBuilder(); + + long refreshInterval = TimeUnit.SECONDS.toMillis(3600); + + return builder.namespaceChecks(NamespaceCheckingMode.EUGRIDPMA_AND_GLOBUS_REQUIRE) + .crlChecks(CrlCheckingMode.IF_VALID) + .ocspChecks(OCSPCheckingMode.IGNORE) + .lazyAnchorsLoading(false) + .storeUpdateListener(l) + .validationErrorListener(l) + .trustAnchorsDir("src/test/resources/trust-anchors") + .trustAnchorsUpdateInterval(refreshInterval) + .build(); + } + + @Test + void tlsConnectorBuilderTests() { + + Server server = new Server(); + X509CertChainValidatorExt validator = getValidator(); + TLSServerConnectorBuilder builder = TLSServerConnectorBuilder.instance(server, validator); + HttpConfiguration httpConfiguration = builder.httpConfiguration(); + KeyManager keyManager = Mockito.mock(KeyManager.class); + builder.withPort(1234) + .withCertificateFile("fake-certificate") + .withCertificateKeyFile("fake-key") + .withCertificateKeyPassword("secret".toCharArray()) + .withHttpConfiguration(httpConfiguration) + .withKeyManager(keyManager) + .withExcludeCipherSuites("one", "two") + .withIncludeCipherSuites("three", "four") + .withIncludeProtocols("protocol", "another-protocol") + .withExcludeProtocols("another-more-protocol") + .withHostnameVerifier(new NoopHostnameVerifier()) + .withConscrypt(false); + + ServerConnector c = builder.build(); + assertThat(c.getPort(), is(1234)); + } +} \ No newline at end of file diff --git a/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/AuthorizationIntegrationTests.java b/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/AuthorizationIntegrationTests.java index 2bf656b5..eddc1680 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/AuthorizationIntegrationTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/AuthorizationIntegrationTests.java @@ -57,6 +57,9 @@ public class AuthorizationIntegrationTests { public static final String UNKNOWN_ISSUER = "https://unknown.example"; public static final String EXAMPLE_ISSUER = "https://issuer.example"; + public static final String AUTHORIZED_JWT_CLIENT_ID = "1234"; + public static final String UNAUTHORIZED_JWT_CLIENT_ID = "5678"; + @Autowired private MockMvc mvc; @@ -127,11 +130,8 @@ void issuerChecksAreEnforcedForWlcgScopeBasedAuthz() throws Exception { @Test void getAccessAsJwtUserWithoutScopeLeadsToAccessDenied() throws Exception { - Jwt token = Jwt.withTokenValue("test") - .header("kid", "rsa1") - .issuer(WLCG_ISSUER) - .subject("123") - .build(); + Jwt token = + Jwt.withTokenValue("test").header("kid", "rsa1").issuer(WLCG_ISSUER).subject("123").build(); mvc.perform(get(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token))) .andExpect(status().isForbidden()); @@ -180,6 +180,123 @@ void getAccessAsJwtWithWriteCapabilityResultsInAccessDenied() throws Exception { } + @Test + void readAccessAsJwtWithAllowedClient() throws Exception { + Jwt token = Jwt.withTokenValue("test") + .header("kid", "rsa1") + .issuer(EXAMPLE_ISSUER) + .claim("client_id", AUTHORIZED_JWT_CLIENT_ID) + .build(); + + mvc.perform(get(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isNotFound()); + + } + + @Test + void readAccessWithoutMatchedJWTIsDenied() throws Exception { + Jwt token = Jwt.withTokenValue("test") + .header("kid", "rsa1") + .issuer(EXAMPLE_ISSUER) + .claim("client_id", UNAUTHORIZED_JWT_CLIENT_ID) + .build(); + + mvc.perform(get(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isForbidden()); + + token = Jwt.withTokenValue("test") + .header("kid", "rsa1") + .issuer(UNKNOWN_ISSUER) + .claim("client_id", AUTHORIZED_JWT_CLIENT_ID) + .build(); + + mvc.perform(get(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isForbidden()); + + token = Jwt.withTokenValue("test") + .header("kid", "rsa1") + .issuer(UNKNOWN_ISSUER) + .claim("client_id", UNAUTHORIZED_JWT_CLIENT_ID) + .build(); + + mvc.perform(get(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isForbidden()); + + token = Jwt.withTokenValue("test").header("kid", "rsa1").issuer(UNKNOWN_ISSUER).build(); + + mvc.perform(get(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isForbidden()); + + } + + @Test + void writeAccessAsJwtWithAllowedClient() throws Exception { + Jwt token = Jwt.withTokenValue("test") + .header("kid", "rsa1") + .issuer(EXAMPLE_ISSUER) + .claim("client_id", AUTHORIZED_JWT_CLIENT_ID) + .build(); + + mvc.perform(put(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isOk()); + + } + + @Test + void writeAccessWithoutMatchedJWTIsDenied() throws Exception { + Jwt token = Jwt.withTokenValue("test") + .header("kid", "rsa1") + .issuer(EXAMPLE_ISSUER) + .claim("client_id", UNAUTHORIZED_JWT_CLIENT_ID) + .build(); + + mvc.perform(put(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isForbidden()); + + token = Jwt.withTokenValue("test") + .header("kid", "rsa1") + .issuer(UNKNOWN_ISSUER) + .claim("client_id", AUTHORIZED_JWT_CLIENT_ID) + .build(); + + mvc.perform(put(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isForbidden()); + + token = Jwt.withTokenValue("test") + .header("kid", "rsa1") + .issuer(UNKNOWN_ISSUER) + .claim("client_id", UNAUTHORIZED_JWT_CLIENT_ID) + .build(); + + mvc.perform(put(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isForbidden()); + + token = Jwt.withTokenValue("test").header("kid", "rsa1").issuer(UNKNOWN_ISSUER).build(); + + mvc.perform(put(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isForbidden()); + } + + @Test + void readWriteAccessAsJwtWithAllowedGroup() throws Exception { + + final String[] OAUTH_GROUP_CLAIM_NAMES = {"groups", "wlcg.groups", "entitlements"}; + + for (String groupClaim : OAUTH_GROUP_CLAIM_NAMES) { + Jwt token = Jwt.withTokenValue("test") + .header("kid", "rsa1") + .issuer(EXAMPLE_ISSUER) + .claim(groupClaim, "/example/admins") + .build(); + + mvc.perform(get(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isNotFound()); + + mvc.perform(put(SLASH_WLCG_SLASH_FILE).with(jwt().jwt(token).authorities(authConverter))) + .andExpect(status().isOk()); + } + } + @WithMockVOMSUser(vos = "wlcg", saReadPermissions = {"wlcg"}) @Test void localVomsCopyRequiresWithReadPermissionsGetsAccessDenied() throws Exception { diff --git a/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/LocalAuthzIntegrationTests.java b/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/LocalAuthzIntegrationTests.java index 2935fa19..d59a1bbe 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/LocalAuthzIntegrationTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/authz/integration/LocalAuthzIntegrationTests.java @@ -77,12 +77,12 @@ public void setup() { } @Test - public void testLocalAuthz() throws Exception { + void testLocalAuthz() throws Exception { mvc.perform(put(SLASH_WLCG_SLASH_FILE)).andExpect(status().isUnauthorized()); } @Test - public void testInvalidTokenAuthz() throws Exception { + void testInvalidTokenAuthz() throws Exception { Jwt token = Jwt.withTokenValue("test") .header("kid", "rsa1") .issuer(UNKNOWN_ISSUER) @@ -104,7 +104,7 @@ public void testInvalidTokenAuthz() throws Exception { } @Test - public void testValidLocalTokenAuthz() throws Exception { + void testValidLocalTokenAuthz() throws Exception { Jwt token = Jwt.withTokenValue("test") .header("kid", "rsa1") .issuer(LOCAL_ISSUER) @@ -123,7 +123,7 @@ public void testValidLocalTokenAuthz() throws Exception { } @Test - public void testInvalidPathLocalTokenAuthz() throws Exception { + void testInvalidPathLocalTokenAuthz() throws Exception { Jwt token = Jwt.withTokenValue("test") .header("kid", "rsa1") .issuer(LOCAL_ISSUER) @@ -139,7 +139,7 @@ public void testInvalidPathLocalTokenAuthz() throws Exception { } @Test - public void testInvalidLocalToken() throws Exception { + void testInvalidLocalToken() throws Exception { Jwt token = Jwt.withTokenValue("test") .header("kid", "rsa1") .issuer(LOCAL_ISSUER) diff --git a/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/AuthzPdpTests.java b/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/AuthzPdpTests.java index 04347431..342326fc 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/AuthzPdpTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/AuthzPdpTests.java @@ -37,6 +37,7 @@ import org.italiangrid.storm.webdav.authz.pdp.PathAuthorizationResult; import org.italiangrid.storm.webdav.authz.pdp.principal.Anyone; import org.italiangrid.storm.webdav.authz.pdp.principal.AuthorityHolder; +import org.italiangrid.storm.webdav.oauth.authority.JwtClientAuthority; import org.italiangrid.storm.webdav.oauth.authority.JwtGroupAuthority; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; @@ -57,6 +58,8 @@ public class AuthzPdpTests { public static final String TEST_ISSUER = "https://test.example"; public static final String TEST2_ISSUER = "https://test2.example"; + public static final String AUTHORIZED_JWT_CLIENT_ID = "1234"; + public static final String UNAUTHORIZED_JWT_CLIENT_ID = "5678"; @Mock PathAuthorizationPolicyRepository repo; @@ -83,7 +86,7 @@ public void setup() { @Test - public void notApplicableWithEmptyPolicies() { + void notApplicableWithEmptyPolicies() { PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, authentication)); @@ -92,7 +95,7 @@ public void notApplicableWithEmptyPolicies() { @Test - public void denyPolicyApplied() { + void denyPolicyApplied() { PathAuthorizationPolicy denyAllPolicy = PathAuthorizationPolicy.builder() .withDeny() @@ -112,7 +115,7 @@ public void denyPolicyApplied() { } @Test - public void firstApplicablePolicyApplied() { + void firstApplicablePolicyApplied() { PathAuthorizationPolicy denyAllPolicy = PathAuthorizationPolicy.builder() .withDeny() @@ -138,7 +141,7 @@ public void firstApplicablePolicyApplied() { } @Test - public void oauthGroupHolderPolicyNotAppliedAsItDoesNotMatchPath() { + void oauthGroupHolderPolicyNotAppliedAsItDoesNotMatchPath() { PathAuthorizationPolicy oauthTestPolicy = PathAuthorizationPolicy.builder() .withPermit() @@ -164,7 +167,7 @@ public void oauthGroupHolderPolicyNotAppliedAsItDoesNotMatchPath() { } @Test - public void oauthGroupHolderPolicyNotAppliedDueToWrongGroup() { + void oauthGroupHolderPolicyNotAppliedDueToWrongGroup() { when(authentication.getAuthorities()) .thenReturn(authorities(new JwtGroupAuthority(TEST_ISSUER, "/test/subgroup"))); @@ -197,7 +200,7 @@ public void oauthGroupHolderPolicyNotAppliedDueToWrongGroup() { } @Test - public void oauthGroupHolderPolicyNotAppliedDueToWrongIssuer() { + void oauthGroupHolderPolicyNotAppliedDueToWrongIssuer() { when(authentication.getAuthorities()) .thenReturn(authorities(new JwtGroupAuthority(TEST2_ISSUER, "/test"))); @@ -228,7 +231,7 @@ public void oauthGroupHolderPolicyNotAppliedDueToWrongIssuer() { } @Test - public void oauthGroupHolderPolicyApplied() { + void oauthGroupHolderPolicyApplied() { when(request.getServletPath()).thenReturn("/test/file0"); when(request.getMethod()).thenReturn("GET"); @@ -258,9 +261,47 @@ public void oauthGroupHolderPolicyApplied() { assertThat(result.getPolicy().isPresent(), is(true)); assertThat(result.getPolicy().get(), is(oauthTestPolicy)); } + + @Test + void oauthClientHolderPolicyApplied() { + + when(request.getServletPath()).thenReturn("/test/file0"); + when(request.getMethod()).thenReturn("GET"); + + when(authentication.getAuthorities()) + .thenReturn(authorities(new JwtClientAuthority(TEST_ISSUER, AUTHORIZED_JWT_CLIENT_ID))); + + PathAuthorizationPolicy oauthTestPolicy = PathAuthorizationPolicy.builder() + .withPermit() + .withPrincipalMatcher(AuthorityHolder + .fromAuthority(new JwtClientAuthority(TEST_ISSUER, AUTHORIZED_JWT_CLIENT_ID))) + .withRequestMatcher(new AntPathRequestMatcher("/test/**", "GET")) + .build(); + + PathAuthorizationPolicy denyAllPolicy = PathAuthorizationPolicy.builder() + .withDeny() + .withPrincipalMatcher(new Anyone()) + .withRequestMatcher(new AntPathRequestMatcher("/**")) + .build(); + + List policies = Lists.newArrayList(oauthTestPolicy, denyAllPolicy); + when(repo.getPolicies()).thenReturn(policies); + + PathAuthorizationResult result = + pdp.authorizeRequest(newAuthorizationRequest(request, authentication)); + assertThat(result.getDecision(), is(PERMIT)); + assertThat(result.getPolicy().isPresent(), is(true)); + assertThat(result.getPolicy().get(), is(oauthTestPolicy)); + + when(authentication.getAuthorities()) + .thenReturn(authorities(new JwtClientAuthority(TEST_ISSUER, UNAUTHORIZED_JWT_CLIENT_ID))); + + result = pdp.authorizeRequest(newAuthorizationRequest(request, authentication)); + assertThat(result.getDecision(), is(DENY)); + } @Test - public void multiplePrincipalMatchersWorkAsExpected() { + void multiplePrincipalMatchersWorkAsExpected() { when(request.getServletPath()).thenReturn("/test/file0"); when(request.getMethod()).thenReturn("GET"); @@ -310,7 +351,7 @@ public void multiplePrincipalMatchersWorkAsExpected() { } @Test - public void multiplePathsMatchersWorkAsExpected() { + void multiplePathsMatchersWorkAsExpected() { when(request.getServletPath()).thenReturn("/test/file0"); when(request.getMethod()).thenReturn("GET"); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/LocalAuthzPdpTests.java b/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/LocalAuthzPdpTests.java index 128e5ee3..3f082100 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/LocalAuthzPdpTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/LocalAuthzPdpTests.java @@ -100,7 +100,7 @@ public void setup() throws MalformedURLException { } @Test - public void noPathRaisesException() throws Exception { + void noPathRaisesException() { when(jwt.getClaimAsString("path")).thenReturn(null); Exception e = assertThrows(IllegalArgumentException.class, () -> { @@ -110,7 +110,7 @@ public void noPathRaisesException() throws Exception { } @Test - public void noPermsRaisesException() throws Exception { + void noPermsRaisesException() { when(jwt.getClaimAsString("perms")).thenReturn(null); Exception e = assertThrows(IllegalArgumentException.class, () -> { @@ -120,7 +120,7 @@ public void noPermsRaisesException() throws Exception { } @Test - public void pathMismatchYeldsDeny() throws Exception { + void pathMismatchYeldsDeny() { when(jwt.getClaimAsString("path")).thenReturn("/test/another"); PathAuthorizationResult result = @@ -130,7 +130,7 @@ public void pathMismatchYeldsDeny() throws Exception { } @Test - public void permMismatchYeldsDeny() throws Exception { + void permMismatchYeldsDeny() throws Exception { when(request.getMethod()).thenReturn("PUT"); PathAuthorizationResult result = @@ -140,7 +140,7 @@ public void permMismatchYeldsDeny() throws Exception { } @Test - public void originMismatchYeldsDeny() throws Exception { + void originMismatchYeldsDeny() { when(request.getRemoteAddr()).thenReturn(REMOTE_ADDR); when(jwt.getClaimAsString(ORIGIN_CLAIM)).thenReturn(ANOTHER_REMOTE_ADDR); @@ -151,7 +151,7 @@ public void originMismatchYeldsDeny() throws Exception { } @Test - public void testPermit() throws Exception { + void testPermit() throws Exception { PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/PolicyParserTests.java b/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/PolicyParserTests.java index 1fdf6d43..e65eec29 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/PolicyParserTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/PolicyParserTests.java @@ -43,7 +43,7 @@ import com.google.common.collect.Lists; @ExtendWith(MockitoExtension.class) -public class PolicyParserTests { +class PolicyParserTests { ServiceConfigurationProperties properties = new ServiceConfigurationProperties(); @@ -65,13 +65,13 @@ public void setup() { } @Test - public void testNoPolicyParsing() throws Exception { + void testNoPolicyParsing() throws Exception { assertThat(parser.parsePolicies(), empty()); } @Test - public void testSimplePolicyParsing() throws Exception { + void testSimplePolicyParsing() throws Exception { FineGrainedAuthzPolicyProperties.PrincipalProperties anonymous = new FineGrainedAuthzPolicyProperties.PrincipalProperties(); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/PolicyPropertiesValidationTests.java b/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/PolicyPropertiesValidationTests.java index e2912024..31cfea4f 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/PolicyPropertiesValidationTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/PolicyPropertiesValidationTests.java @@ -42,7 +42,7 @@ import com.google.common.collect.Lists; @ExtendWith(MockitoExtension.class) -public class PolicyPropertiesValidationTests { +class PolicyPropertiesValidationTests { private Validator validator; @@ -69,7 +69,7 @@ public FineGrainedAuthzPolicyProperties minimalValidPolicy() { } @Test - public void testValidAuthzPolicyPassesValidation() throws Exception { + void testValidAuthzPolicyPassesValidation() throws Exception { FineGrainedAuthzPolicyProperties props = minimalValidPolicy(); @@ -81,7 +81,7 @@ public void testValidAuthzPolicyPassesValidation() throws Exception { } @Test - public void testDescriptionRequired() throws Exception { + void testDescriptionRequired() throws Exception { FineGrainedAuthzPolicyProperties props = minimalValidPolicy(); props.setDescription(null); @@ -96,7 +96,7 @@ public void testDescriptionRequired() throws Exception { } @Test - public void testSaRequired() throws Exception { + void testSaRequired() throws Exception { FineGrainedAuthzPolicyProperties props = minimalValidPolicy(); props.setSa(null); @@ -111,7 +111,7 @@ public void testSaRequired() throws Exception { } @Test - public void testPrincipalsNotEmpty() throws Exception { + void testPrincipalsNotEmpty() throws Exception { FineGrainedAuthzPolicyProperties props = minimalValidPolicy(); props.setPrincipals(emptyList()); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/ScopePathAuthzPdpTests.java b/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/ScopePathAuthzPdpTests.java index 215122ea..803f7f59 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/ScopePathAuthzPdpTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/authz/pdp/ScopePathAuthzPdpTests.java @@ -21,6 +21,7 @@ import static org.italiangrid.storm.webdav.authz.pdp.PathAuthorizationRequest.newAuthorizationRequest; import static org.italiangrid.storm.webdav.authz.pdp.WlcgStructuredPathAuthorizationPdp.SCOPE_CLAIM; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -55,7 +56,8 @@ @ExtendWith(MockitoExtension.class) public class ScopePathAuthzPdpTests { - public static final String[] READ_METHODS = {"GET", "PROPFIND", "OPTIONS", "HEAD"}; + public static final String[] CATCHALL_METHODS = {"HEAD", "OPTIONS"}; + public static final String[] READ_METHODS = {"GET", "PROPFIND"}; public static final String[] REPLACE_METHODS = {"PUT", "MKCOL"}; public static final String[] MODIFY_METHODS = {"DELETE", "PATCH"}; public static final String COPY_METHOD = "COPY"; @@ -94,13 +96,14 @@ public void setup() throws MalformedURLException { lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("storage.read:/"); lenient().when(request.getServletPath()).thenReturn("/"); lenient().when(request.getPathInfo()).thenReturn("test/example"); + lenient().when(sa.rootPath()).thenReturn("/storage"); lenient().when(sa.accessPoints()).thenReturn(Lists.newArrayList("/test")); lenient().when(sa.orgs()).thenReturn(Sets.newHashSet("https://issuer.example")); lenient().when(pathResolver.resolveStorageArea("/test/example")).thenReturn(sa); } @Test - public void invalidAuthentication() { + void invalidAuthentication() { Authentication auth = mock(Authentication.class); assertThrows(IllegalArgumentException.class, () -> { @@ -109,8 +112,8 @@ public void invalidAuthentication() { } @Test - public void noScopeClaimYeldsIndeterminate() throws Exception { - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn(null); + void noScopeClaimYeldsIndeterminate() { + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn(null); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.INDETERMINATE)); @@ -119,9 +122,9 @@ public void noScopeClaimYeldsIndeterminate() throws Exception { } @Test - public void noSaYeldsIndeterminate() throws Exception { + void noSaYeldsIndeterminate() { - when(pathResolver.resolveStorageArea("/test/example")).thenReturn(null); + lenient().when(pathResolver.resolveStorageArea("/test/example")).thenReturn(null); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.INDETERMINATE)); @@ -130,8 +133,8 @@ public void noSaYeldsIndeterminate() throws Exception { } @Test - public void noStorageScopesYeldsDeny() throws Exception { - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid profile storage.read"); + void noStorageScopesYeldsDeny() { + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid profile storage.read"); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); @@ -141,13 +144,66 @@ public void noStorageScopesYeldsDeny() throws Exception { assertThat(result.getMessage().get(), containsString("Insufficient token scope")); } + @Test + void noStorageScopesYeldsDenyForCatchallMethods() { + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid profile"); + + for (String m : CATCHALL_METHODS) { + lenient().when(request.getMethod()).thenReturn(m); + PathAuthorizationResult result = + pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + + assertThat(result.getDecision(), is(Decision.INDETERMINATE)); + assertThat(result.getMessage().isPresent(), is(true)); + assertThat(result.getMessage().get(), containsString("Insufficient token scope")); + } + } @Test - public void readMethodsRequestsRequireStorageRead() throws Exception { - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); + void catchallMethodsRequestsAtLeastOneStorageScope() { + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); + + for (String m : CATCHALL_METHODS) { + lenient().when(request.getMethod()).thenReturn(m); + PathAuthorizationResult result = + pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.PERMIT)); + } + + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); + + for (String m : CATCHALL_METHODS) { + lenient().when(request.getMethod()).thenReturn(m); + PathAuthorizationResult result = + pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.PERMIT)); + } + + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.create:/"); + + for (String m : CATCHALL_METHODS) { + lenient().when(request.getMethod()).thenReturn(m); + PathAuthorizationResult result = + pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.PERMIT)); + } + + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.stage:/"); + + for (String m : CATCHALL_METHODS) { + lenient().when(request.getMethod()).thenReturn(m); + PathAuthorizationResult result = + pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.PERMIT)); + } + } + + @Test + void readMethodsRequestsRequireStorageReadOrStage() { + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); for (String m : READ_METHODS) { - when(request.getMethod()).thenReturn(m); + lenient().when(request.getMethod()).thenReturn(m); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.DENY)); @@ -155,10 +211,19 @@ public void readMethodsRequestsRequireStorageRead() throws Exception { assertThat(result.getMessage().get(), containsString("Insufficient token scope")); } - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); for (String m : READ_METHODS) { - when(request.getMethod()).thenReturn(m); + lenient().when(request.getMethod()).thenReturn(m); + PathAuthorizationResult result = + pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.PERMIT)); + } + + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.stage:/"); + + for (String m : READ_METHODS) { + lenient().when(request.getMethod()).thenReturn(m); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.PERMIT)); @@ -166,11 +231,11 @@ public void readMethodsRequestsRequireStorageRead() throws Exception { } @Test - public void replaceMethodsRequestsRequireStorageModifyOrCreate() throws Exception { - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); + void replaceMethodsRequestsRequireStorageModifyOrCreate() { + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); for (String m : REPLACE_METHODS) { - when(request.getMethod()).thenReturn(m); + lenient().when(request.getMethod()).thenReturn(m); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.DENY)); @@ -178,11 +243,12 @@ public void replaceMethodsRequestsRequireStorageModifyOrCreate() throws Exceptio assertThat(result.getMessage().get(), containsString("Insufficient token scope")); } - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/ storage.create:/"); - when(pathResolver.pathExists("/test/example")).thenReturn(true); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.read:/ storage.create:/"); + lenient().when(pathResolver.pathExists("/test/example")).thenReturn(true); for (String m : REPLACE_METHODS) { - when(request.getMethod()).thenReturn(m); + lenient().when(request.getMethod()).thenReturn(m); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.DENY)); @@ -190,7 +256,7 @@ public void replaceMethodsRequestsRequireStorageModifyOrCreate() throws Exceptio assertThat(result.getMessage().get(), containsString("Insufficient token scope")); } - when(pathResolver.pathExists("/test/example")).thenReturn(false); + lenient().when(pathResolver.pathExists("/test/example")).thenReturn(false); for (String m : REPLACE_METHODS) { when(request.getMethod()).thenReturn(m); @@ -199,10 +265,10 @@ public void replaceMethodsRequestsRequireStorageModifyOrCreate() throws Exceptio assertThat(result.getDecision(), is(Decision.PERMIT)); } - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); for (String m : REPLACE_METHODS) { - when(request.getMethod()).thenReturn(m); + lenient().when(request.getMethod()).thenReturn(m); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.PERMIT)); @@ -210,11 +276,12 @@ public void replaceMethodsRequestsRequireStorageModifyOrCreate() throws Exceptio } @Test - public void modifyMethodsRequestsRequireStorageModifyOrCreate() throws Exception { - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/ storage.create:/"); + void modifyMethodsRequestsRequireStorageModifyOrCreate() { + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.read:/ storage.create:/"); for (String m : MODIFY_METHODS) { - when(request.getMethod()).thenReturn(m); + lenient().when(request.getMethod()).thenReturn(m); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.DENY)); @@ -222,10 +289,11 @@ public void modifyMethodsRequestsRequireStorageModifyOrCreate() throws Exception assertThat(result.getMessage().get(), containsString("Insufficient token scope")); } - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/ storage.modify:/"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.read:/ storage.modify:/"); for (String m : MODIFY_METHODS) { - when(request.getMethod()).thenReturn(m); + lenient().when(request.getMethod()).thenReturn(m); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.PERMIT)); @@ -233,10 +301,10 @@ public void modifyMethodsRequestsRequireStorageModifyOrCreate() throws Exception } @Test - public void testLocalCopyRequiresStorageCreateOrModify() throws Exception { + void testLocalCopyRequiresStorageCreateOrModify() { - when(request.getMethod()).thenReturn(COPY_METHOD); - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); + lenient().when(request.getMethod()).thenReturn(COPY_METHOD); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); @@ -244,17 +312,18 @@ public void testLocalCopyRequiresStorageCreateOrModify() throws Exception { assertThat(result.getMessage().isPresent(), is(true)); assertThat(result.getMessage().get(), containsString("Insufficient token scope")); - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/ storage.write:/"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.read:/ storage.write:/"); result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.PERMIT)); } @Test - public void testPullTpcRequiresCreateOrModify() throws Exception { - when(request.getMethod()).thenReturn(COPY_METHOD); - when(request.getHeader("Source")).thenReturn("https://remote.example/test/example"); - when(pathResolver.pathExists("/test/example")).thenReturn(true); - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); + void testPullTpcRequiresCreateOrModify() { + lenient().when(request.getMethod()).thenReturn(COPY_METHOD); + lenient().when(request.getHeader("Source")).thenReturn("https://remote.example/test/example"); + lenient().when(pathResolver.pathExists("/test/example")).thenReturn(true); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); @@ -262,25 +331,26 @@ public void testPullTpcRequiresCreateOrModify() throws Exception { assertThat(result.getMessage().isPresent(), is(true)); assertThat(result.getMessage().get(), containsString("Insufficient token scope")); - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.create:/"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.create:/"); result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.DENY)); assertThat(result.getMessage().isPresent(), is(true)); assertThat(result.getMessage().get(), containsString("Insufficient token scope")); - when(pathResolver.pathExists("/test/example")).thenReturn(false); + lenient().when(pathResolver.pathExists("/test/example")).thenReturn(false); result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.PERMIT)); - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.PERMIT)); } @Test - public void testPushTpcRequiresRead() throws Exception { - when(request.getMethod()).thenReturn(COPY_METHOD); - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.create:/ storage.modify:/"); + void testPushTpcRequiresRead() { + lenient().when(request.getMethod()).thenReturn(COPY_METHOD); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.create:/ storage.modify:/"); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); @@ -288,15 +358,16 @@ public void testPushTpcRequiresRead() throws Exception { assertThat(result.getMessage().isPresent(), is(true)); assertThat(result.getMessage().get(), containsString("Insufficient token scope")); - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.PERMIT)); } @Test - public void testMoveRequiresModify() throws Exception { - when(request.getMethod()).thenReturn(MOVE_METHOD); - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/ storage.create:/"); + void testMoveRequiresModify() { + lenient().when(request.getMethod()).thenReturn(MOVE_METHOD); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.read:/ storage.create:/"); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); @@ -304,15 +375,16 @@ public void testMoveRequiresModify() throws Exception { assertThat(result.getMessage().isPresent(), is(true)); assertThat(result.getMessage().get(), containsString("Insufficient token scope")); - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.modify:/"); result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.PERMIT)); } @Test - public void testModifyImpliesCreate() throws Exception { - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/ storage.modify:/"); - when(pathResolver.pathExists("/test/example")).thenReturn(false); + void testModifyImpliesCreate() { + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.read:/ storage.modify:/"); + lenient().when(pathResolver.pathExists("/test/example")).thenReturn(false); for (String m : REPLACE_METHODS) { when(request.getMethod()).thenReturn(m); @@ -323,37 +395,89 @@ public void testModifyImpliesCreate() throws Exception { } @Test - public void testUnsupportedMethod() throws Exception { - when(request.getMethod()).thenReturn("TRACE"); + void testUnsupportedMethod() { + lenient().when(request.getMethod()).thenReturn("TRACE"); assertThrows(IllegalArgumentException.class, () -> { pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); }); } @Test - public void testPathAuthzIsEnforced() throws Exception { - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/subfolder"); - when(request.getMethod()).thenReturn("GET"); + void testPathAuthzIsEnforced() { + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/subfolder"); + lenient().when(request.getMethod()).thenReturn("GET"); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.DENY)); assertThat(result.getMessage().isPresent(), is(true)); assertThat(result.getMessage().get(), containsString("Insufficient token scope")); - when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)).thenReturn("openid storage.read:/"); result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.PERMIT)); } @Test - public void issuerChecksAreEnforced() throws Exception { - when(jwt.getIssuer()).thenReturn(new URL("https://unknown.example")); - when(request.getMethod()).thenReturn("GET"); + void issuerChecksAreEnforced() throws Exception { + lenient().when(jwt.getIssuer()).thenReturn(new URL("https://unknown.example")); + lenient().when(request.getMethod()).thenReturn("GET"); PathAuthorizationResult result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); assertThat(result.getDecision(), is(Decision.DENY)); assertThat(result.getMessage().isPresent(), is(true)); assertThat(result.getMessage().get(), containsString("Unknown token issuer")); } + + @Test + void parentDirCreationIsAllowedWithStorageCreateOrModify() { + + lenient().when(pathResolver.resolveStorageArea(anyString())).thenReturn(sa); + lenient().when(request.getPathInfo()).thenReturn("test/dir/subdir"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.create:/dir/subdir"); + lenient().when(request.getMethod()).thenReturn("MKCOL"); + PathAuthorizationResult result = + pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.PERMIT)); + + lenient().when(request.getPathInfo()).thenReturn("test/dir"); + result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.PERMIT)); + + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.modify:/dir/subdir"); + result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.PERMIT)); + + lenient().when(request.getPathInfo()).thenReturn("test/dir"); + result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.PERMIT)); + } + + @Test + void parentDirCreationIsNotAllowedWithWrongScopes() { + + lenient().when(pathResolver.resolveStorageArea(anyString())).thenReturn(sa); + lenient().when(request.getPathInfo()).thenReturn("test/dir/subdir"); + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.read:/dir/subdir"); + lenient().when(request.getMethod()).thenReturn("MKCOL"); + PathAuthorizationResult result = + pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.DENY)); + + lenient().when(request.getPathInfo()).thenReturn("test/dir"); + result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.DENY)); + + lenient().when(jwt.getClaimAsString(SCOPE_CLAIM)) + .thenReturn("openid storage.stage:/dir/subdir"); + result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.DENY)); + + lenient().when(request.getPathInfo()).thenReturn("test/dir"); + result = pdp.authorizeRequest(newAuthorizationRequest(request, jwtAuth)); + assertThat(result.getDecision(), is(Decision.DENY)); + } } diff --git a/src/test/java/org/italiangrid/storm/webdav/test/authz/vomap/PathResolverTests.java b/src/test/java/org/italiangrid/storm/webdav/test/authz/vomap/PathResolverTests.java index 5031c0f1..cb11dfad 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/authz/vomap/PathResolverTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/authz/vomap/PathResolverTests.java @@ -36,7 +36,7 @@ public class PathResolverTests { - private final String ROOTDIR = "/storage"; + private static final String ROOTDIR = "/storage"; /* * Map SA-name -> VO name @@ -93,29 +93,29 @@ private StorageAreaInfo getMockSAInfo(String name, String voname) { } @Test - public void checkResolvedRootPath() { + void checkResolvedRootPath() { for (String name : input.keySet()) { String pathToTest = "/".concat(name).concat("/testdir"); String expectedRootPath = ROOTDIR.concat("/").concat(input.get(name)) .concat("/testdir"); - + String rootPath = pathResolver.resolvePath(pathToTest); Assert.assertEquals(expectedRootPath, rootPath); } } - + @Test - public void checkResolvedStorageArea() { + void checkResolvedStorageArea() { for (String name : input.keySet()) { String pathToTest = "/".concat(name).concat("/testdir"); String expectedRootPath = ROOTDIR.concat("/").concat(input.get(name)) .concat("/testdir"); - + StorageAreaInfo sa = pathResolver.resolveStorageArea(pathToTest); Assert.assertEquals(expectedRootPath, sa.rootPath() + "/testdir"); } diff --git a/src/test/java/org/italiangrid/storm/webdav/test/authz/vomap/VOMSMapTests.java b/src/test/java/org/italiangrid/storm/webdav/test/authz/vomap/VOMSMapTests.java index 9f59ef0c..e9f1d369 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/authz/vomap/VOMSMapTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/authz/vomap/VOMSMapTests.java @@ -24,18 +24,28 @@ public class VOMSMapTests { - public static final String mySubject = "CN=Andrea Ceccanti,L=CNAF,OU=Personal Certificate,O=INFN,C=IT"; - @Test - public void VOMapParserTest() { - - MapfileVOMembershipSource m = new MapfileVOMembershipSource("testers", - new File("src/test/resources/vomsmap/testers.map")); - - Assert.assertEquals("testers",m.getVOName()); - Assert.assertTrue(m.getVOMembers().contains(mySubject)); - - Assert.assertFalse(m.getVOMembers().contains("CN=I am not Real, L=CNAF")); - - } + public static final String AC_SUBJECT = + "CN=Andrea Ceccanti,L=CNAF,OU=Personal Certificate,O=INFN,C=IT"; + public static final String EV_SUBJECT = + "CN=Enrico Vianello,L=CNAF,OU=Personal Certificate,O=INFN,C=IT"; + public static final String COMMA_SUBJECT = + "CN=Federica Agostini,L=CNAF,Bologna,OU=Personal Certificate,O=INFN,C=IT"; + public static final String RM_SUBJECT = + "CN=Roberta Miccoli,L=CNAF,OU=Personal Certificate,O=INFN,C=IT"; + + @Test + void VOMapParserTest() { + + MapfileVOMembershipSource m = new MapfileVOMembershipSource("testers", + new File("src/test/resources/vomsmap/testers.map")); + + Assert.assertEquals("testers", m.getVOName()); + Assert.assertTrue(m.getVOMembers().contains(AC_SUBJECT)); + Assert.assertFalse(m.getVOMembers().contains(EV_SUBJECT)); + Assert.assertFalse(m.getVOMembers().contains(COMMA_SUBJECT)); + Assert.assertTrue(m.getVOMembers().contains(RM_SUBJECT)); + Assert.assertFalse(m.getVOMembers().contains("CN=I am not Real, L=CNAF")); + + } } diff --git a/src/test/java/org/italiangrid/storm/webdav/test/checksum/ChecksumHelperTest.java b/src/test/java/org/italiangrid/storm/webdav/test/checksum/ChecksumHelperTest.java index 09c7287c..97d40ee8 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/checksum/ChecksumHelperTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/checksum/ChecksumHelperTest.java @@ -32,7 +32,7 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -public class ChecksumHelperTest { +class ChecksumHelperTest { private File testFile; private Adler32ChecksumInputStream cis; @@ -60,12 +60,12 @@ public void setup() throws IOException { } @AfterEach - public void finalize() throws IOException { + public void cleanup() throws IOException { cis.close(); } @Test - public void testGetChecksumValueFromFile() throws IOException { + void testGetChecksumValueFromFile() { String newChecksum = cis.getChecksumValue(); String oldChecksum = Long.toHexString(cis.getChecksum().getValue()); @@ -76,7 +76,7 @@ public void testGetChecksumValueFromFile() throws IOException { } @Test - public void testChecksumHelperAddLeadingZero() { + void testChecksumHelperAddLeadingZero() { final String CHECKSUM_VALUE = "abcdefgh"; assertEquals(CHECKSUM_VALUE, ChecksumHelper.addLeadingZero(CHECKSUM_VALUE, 8)); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/macaroon/MacaroonRequestIntegrationTests.java b/src/test/java/org/italiangrid/storm/webdav/test/macaroon/MacaroonRequestIntegrationTests.java index c2c87b7d..5c85f963 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/macaroon/MacaroonRequestIntegrationTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/macaroon/MacaroonRequestIntegrationTests.java @@ -95,19 +95,19 @@ public MacaroonRequestIntegrationTests() { } @Test - public void getNotSupported() throws Exception { + void getNotSupported() throws Exception { mvc.perform(get("/whatever").contentType(MacaroonRequestFilter.MACAROON_REQUEST_CONTENT_TYPE)) .andExpect(status().isMethodNotAllowed()); } @Test - public void emptyRequestFails() throws Exception { + void emptyRequestFails() throws Exception { mvc.perform(post("/whatever").contentType(MacaroonRequestFilter.MACAROON_REQUEST_CONTENT_TYPE)) .andExpect(status().isBadRequest()); } @Test - public void vomsRequired() throws Exception { + void vomsRequired() throws Exception { mvc .perform(post("/whatever").contentType(MacaroonRequestFilter.MACAROON_REQUEST_CONTENT_TYPE) .content(EMPTY_JSON_OBJECT)) @@ -116,7 +116,7 @@ public void vomsRequired() throws Exception { @Test @WithMockVOMSUser - public void macaroonIssued() throws Exception { + void macaroonIssued() throws Exception { mvc .perform(post("/whatever").contentType(MacaroonRequestFilter.MACAROON_REQUEST_CONTENT_TYPE) .content(EMPTY_JSON_OBJECT)) @@ -127,7 +127,7 @@ public void macaroonIssued() throws Exception { @Test @WithMockVOMSUser(acExpirationSecs = 43200) - public void validityEnforced() throws Exception { + void validityEnforced() throws Exception { MacaroonRequestDTO dto = new MacaroonRequestDTO(); dto.setValidity("PT2H"); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/macaroon/MacaroonRequestLhcbIntegrationTests.java b/src/test/java/org/italiangrid/storm/webdav/test/macaroon/MacaroonRequestLhcbIntegrationTests.java new file mode 100644 index 00000000..0333e561 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/macaroon/MacaroonRequestLhcbIntegrationTests.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.test.macaroon; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +import org.italiangrid.storm.webdav.authz.VOMSAuthenticationFilter; +import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties; +import org.italiangrid.storm.webdav.macaroon.MacaroonRequestFilter; +import org.italiangrid.storm.webdav.test.utils.voms.WithMockVOMSUser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; + +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles({"dev", "lhcb"}) +@WithAnonymousUser +public class MacaroonRequestLhcbIntegrationTests { + + public static final Instant NOW = Instant.parse("2018-01-01T00:00:00.00Z"); + public static final Instant NOW_PLUS_2H = + NOW.plusSeconds(TimeUnit.HOURS.toSeconds(2)).truncatedTo(ChronoUnit.SECONDS); + + public static final String EMPTY_JSON_OBJECT = "{}"; + + @TestConfiguration + static class Configuration { + @Bean + @Primary + Clock mockClock() { + return Clock.fixed(NOW, ZoneId.systemDefault()); + } + } + + @Autowired + MockMvc mvc; + + @Autowired + VOMSAuthenticationFilter filter; + + @Autowired + ServiceConfigurationProperties props; + + @Autowired + ObjectMapper mapper; + + @BeforeEach + public void setup() { + filter.setCheckForPrincipalChanges(false); + } + + @Test + @WithMockVOMSUser(saReadPermissions = {"lhcb_disk"}, vos = {"lhcb"}) + void macaroonIssuedWithNoWritePermissions() throws Exception { + mvc + .perform(post("/disk/lhcb/source").contentType(MacaroonRequestFilter.MACAROON_REQUEST_CONTENT_TYPE) + .content(EMPTY_JSON_OBJECT)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.macaroon").exists()); + } + +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/integration/OAuthAuthzServerIntegrationTests.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/integration/OAuthAuthzServerIntegrationTests.java index c1e8e6b6..41eaaac3 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/oauth/integration/OAuthAuthzServerIntegrationTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/integration/OAuthAuthzServerIntegrationTests.java @@ -93,27 +93,27 @@ public void setup() { } @Test - public void getNotSupported() throws Exception { + void getNotSupported() throws Exception { mvc.perform(get("/oauth/token").contentType(APPLICATION_FORM_URLENCODED)) .andExpect(status().isMethodNotAllowed()); } @Test - public void postNotSupportedForAnonymous() throws Exception { + void postNotSupportedForAnonymous() throws Exception { mvc.perform(post("/oauth/token").contentType(APPLICATION_FORM_URLENCODED)) .andExpect(status().isUnauthorized()); } @Test @WithMockUser(username = "test") - public void postNotSupportedForAuthenticatedNonVomsUsers() throws Exception { + void postNotSupportedForAuthenticatedNonVomsUsers() throws Exception { mvc.perform(post("/oauth/token").content(CONTENT).contentType(APPLICATION_FORM_URLENCODED)) .andExpect(status().isForbidden()); } @Test @WithMockVOMSUser(acExpirationSecs = 200) - public void postSupportedForAuthenticatedVomsUsers() throws Exception { + void postSupportedForAuthenticatedVomsUsers() throws Exception { mvc.perform(post("/oauth/token").content(CONTENT).contentType(APPLICATION_FORM_URLENCODED)) .andExpect(status().isOk()) .andExpect(jsonPath("$.access_token").exists()) @@ -123,7 +123,7 @@ public void postSupportedForAuthenticatedVomsUsers() throws Exception { @Test @WithMockVOMSUser - public void invalidGrantTypeRejected() throws Exception { + void invalidGrantTypeRejected() throws Exception { mvc .perform( post("/oauth/token").content(CONTENT_CUSTOM).contentType(APPLICATION_FORM_URLENCODED)) @@ -135,7 +135,7 @@ public void invalidGrantTypeRejected() throws Exception { @Test @WithMockVOMSUser - public void requestedLifetimeHonoured() throws Exception { + void requestedLifetimeHonoured() throws Exception { mvc .perform(post("/oauth/token").content(format("%s&lifetime=50", CONTENT)) .contentType(APPLICATION_FORM_URLENCODED)) @@ -147,7 +147,7 @@ public void requestedLifetimeHonoured() throws Exception { @Test @WithMockVOMSUser(acExpirationSecs = 200) - public void requestedLifetimeLimited() throws Exception { + void requestedLifetimeLimited() throws Exception { mvc .perform(post("/oauth/token").content(format("%s&lifetime=200000", CONTENT)) .contentType(APPLICATION_FORM_URLENCODED)) @@ -159,7 +159,7 @@ public void requestedLifetimeLimited() throws Exception { @Test @WithMockVOMSUser - public void scopeLengthIsChecked() throws Exception { + void scopeLengthIsChecked() throws Exception { String randomAlphabetic = randomAlphabetic(AccessTokenRequest.MAX_SCOPE_LENGTH); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/OidcConfigurationFetcherTest.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/OidcConfigurationFetcherTest.java new file mode 100644 index 00000000..0725f914 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/OidcConfigurationFetcherTest.java @@ -0,0 +1,307 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.test.oauth.jwk; + +import static java.lang.String.format; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.italiangrid.storm.webdav.oauth.utils.DefaultOidcConfigurationFetcher.ISSUER_MISMATCH_ERROR_TEMPLATE; +import static org.italiangrid.storm.webdav.oauth.utils.DefaultOidcConfigurationFetcher.NO_JWKS_URI_ERROR_TEMPLATE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.OK; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.text.ParseException; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.italiangrid.storm.webdav.config.OAuthProperties; +import org.italiangrid.storm.webdav.oauth.utils.DefaultOidcConfigurationFetcher; +import org.italiangrid.storm.webdav.oauth.utils.OidcConfigurationFetcher; +import org.italiangrid.storm.webdav.oauth.utils.OidcConfigurationResolutionError; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import com.google.common.collect.Maps; +import com.nimbusds.jose.KeySourceException; +import com.nimbusds.jose.RemoteKeySourceException; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.KeyType; + +@ExtendWith(MockitoExtension.class) +class OidcConfigurationFetcherTest { + + static final String ISSUER = "https://iam-dev.cloud.cnaf.infn.it/"; + static final String JWK_URI = ISSUER + "jwk"; + + static final String ANOTHER_ISSUER = "https://iam.cloud.infn.it/"; + static final String ANOTHER_JWK_URI = ANOTHER_ISSUER + "jwk"; + + static final String KID = "rsa1"; + + + final ParameterizedTypeReference> typeReference = + new ParameterizedTypeReference>() {}; + + @Mock + RestTemplate restTemplate; + @Mock + RestTemplateBuilder restBuilder; + @Mock + OAuthProperties oAuthProperties; + + private Map getMapWithIssuerAndJwkUri(String issuer, String jwkUri) { + Map m = Maps.newHashMap(); + m.put("issuer", issuer); + m.put("jwks_uri", jwkUri); + return m; + } + + @SuppressWarnings("unchecked") + private ResponseEntity> getWellKnownResponse(HttpStatus status, + Map map) { + + ResponseEntity> mockedEntity = + (ResponseEntity>) Mockito.mock(ResponseEntity.class); + lenient().when(mockedEntity.getStatusCode()).thenReturn(status); + lenient().when(mockedEntity.getStatusCodeValue()).thenReturn(status.value()); + lenient().when(mockedEntity.getBody()).thenReturn(map); + return mockedEntity; + } + + private String loadJwkFromFile() throws IOException { + + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("jwk/test-keystore.jwks").getFile()); + return FileUtils.readFileToString(file, "UTF-8"); + } + + @SuppressWarnings("unchecked") + private ResponseEntity getJWKURIResponse(HttpStatus status, String data) { + + ResponseEntity mockedEntity = + (ResponseEntity) Mockito.mock(ResponseEntity.class); + lenient().when(mockedEntity.getBody()).thenReturn(data); + lenient().when(mockedEntity.getStatusCode()).thenReturn(status); + lenient().when(mockedEntity.getStatusCodeValue()).thenReturn(status.value()); + return mockedEntity; + } + + private OidcConfigurationFetcher getFetcher(ResponseEntity> wellKnownResponse, + ResponseEntity jwkResponse) { + + lenient().when(restTemplate.exchange(any(), eq(typeReference))).thenReturn(wellKnownResponse); + lenient().when(restTemplate.exchange(any(), eq(String.class))).thenReturn(jwkResponse); + return getFetcher(restTemplate); + } + + private OidcConfigurationFetcher getFetcherWithException(ResponseEntity> wellKnownResponse) { + + lenient().when(restTemplate.exchange(any(), eq(typeReference))).thenReturn(wellKnownResponse); + lenient().when(restTemplate.exchange(any(), eq(String.class))).thenThrow(new RuntimeException("ERROR")); + return getFetcher(restTemplate); + } + + private OidcConfigurationFetcher getFetcher(RestTemplate restTemplate) { + + lenient().when(restBuilder.build()).thenReturn(restTemplate); + lenient().when(restBuilder.setConnectTimeout(any())).thenReturn(restBuilder); + lenient().when(restBuilder.setReadTimeout(any())).thenReturn(restBuilder); + lenient().when(oAuthProperties.getRefreshTimeoutSeconds()).thenReturn(30); + lenient().when(oAuthProperties.getRefreshPeriodMinutes()).thenReturn(1); + return new DefaultOidcConfigurationFetcher(restBuilder, oAuthProperties); + } + + private OidcConfigurationFetcher getSuccessfulFetcher() throws RestClientException, IOException { + + ResponseEntity> mockedResponseMapEntity = + getWellKnownResponse(OK, getMapWithIssuerAndJwkUri(ISSUER, JWK_URI)); + ResponseEntity mockedResponseStringEntity = getJWKURIResponse(OK, loadJwkFromFile()); + return getFetcher(mockedResponseMapEntity, mockedResponseStringEntity); + } + + private OidcConfigurationFetcher getSuccessfulFetcherWithWrongIssuer() + throws RestClientException, IOException { + + ResponseEntity> mockedResponseMapEntity = + getWellKnownResponse(OK, getMapWithIssuerAndJwkUri(ANOTHER_ISSUER, ANOTHER_JWK_URI)); + ResponseEntity mockedResponseStringEntity = getJWKURIResponse(OK, loadJwkFromFile()); + return getFetcher(mockedResponseMapEntity, mockedResponseStringEntity); + } + + private OidcConfigurationFetcher getSuccessfulFetcherWithNoIssuer() + throws RestClientException, IOException { + + Map map = getMapWithIssuerAndJwkUri(ANOTHER_ISSUER, ANOTHER_JWK_URI); + map.remove("issuer"); + ResponseEntity> mockedResponseMapEntity = getWellKnownResponse(OK, map); + ResponseEntity mockedResponseStringEntity = getJWKURIResponse(OK, loadJwkFromFile()); + return getFetcher(mockedResponseMapEntity, mockedResponseStringEntity); + } + + private OidcConfigurationFetcher getSuccessfulFetcherWithNoJwk() + throws RestClientException, IOException { + + Map map = getMapWithIssuerAndJwkUri(ISSUER, JWK_URI); + map.remove("jwks_uri"); + ResponseEntity> mockedResponseMapEntity = getWellKnownResponse(OK, map); + ResponseEntity mockedResponseStringEntity = getJWKURIResponse(OK, loadJwkFromFile()); + return getFetcher(mockedResponseMapEntity, mockedResponseStringEntity); + } + + private OidcConfigurationFetcher getFetcherWithErrorOnFetch() throws RestClientException { + + ResponseEntity> mockedResponseMapEntity = + getWellKnownResponse(NOT_FOUND, null); + return getFetcher(mockedResponseMapEntity, null); + } + + private OidcConfigurationFetcher getFetcherWithErrorOnGetJwk() throws RestClientException { + + ResponseEntity> mockedResponseMapEntity = + getWellKnownResponse(OK, getMapWithIssuerAndJwkUri(ISSUER, JWK_URI)); + ResponseEntity mockedResponseStringEntity = getJWKURIResponse(NOT_FOUND, null); + return getFetcher(mockedResponseMapEntity, mockedResponseStringEntity); + } + + private OidcConfigurationFetcher getFetcherWithRuntimeExceptionOnGetJwk() throws RestClientException { + + ResponseEntity> mockedResponseMapEntity = + getWellKnownResponse(OK, getMapWithIssuerAndJwkUri(ISSUER, JWK_URI)); + return getFetcherWithException(mockedResponseMapEntity); + } + + @BeforeEach + public void setDebugLevel() { + System.setProperty("logging.level.org.italiangrid.storm", "DEBUG"); + } + + @Test + void fetchWellKnownEndpointWithSuccessTests() throws RestClientException, IOException { + + OidcConfigurationFetcher fetcher = getSuccessfulFetcher(); + Map conf = fetcher.loadConfigurationForIssuer(ISSUER); + assertNotNull(conf); + assertThat(conf.get("issuer"), is(ISSUER)); + assertThat(conf.get("jwks_uri"), is(JWK_URI)); + } + + @Test + void fetchWellKnownEndpointWithErrorTests() throws RestClientException { + + OidcConfigurationFetcher fetcher = getFetcherWithErrorOnFetch(); + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + fetcher.loadConfigurationForIssuer(ISSUER); + }); + String expectedMessage = "Received status code: 404"; + String actualMessage = exception.getMessage(); + + assertEquals(expectedMessage, actualMessage); + } + + @Test + void fetchWellKnownEndpointWrongIssuerTests() throws RestClientException, IOException { + + OidcConfigurationFetcher fetcher = getSuccessfulFetcherWithWrongIssuer(); + OidcConfigurationResolutionError exception = + assertThrows(OidcConfigurationResolutionError.class, () -> { + fetcher.loadConfigurationForIssuer(ISSUER); + }); + assertEquals(format(ISSUER_MISMATCH_ERROR_TEMPLATE, ANOTHER_ISSUER, ISSUER), + exception.getMessage()); + } + + @Test + void fetchWellKnownEndpointNoIssuerTests() throws RestClientException, IOException { + + OidcConfigurationFetcher fetcher = getSuccessfulFetcherWithNoIssuer(); + OidcConfigurationResolutionError exception = + assertThrows(OidcConfigurationResolutionError.class, () -> { + fetcher.loadConfigurationForIssuer(ISSUER); + }); + assertEquals(format(ISSUER_MISMATCH_ERROR_TEMPLATE, "(unavailable)", ISSUER), + exception.getMessage()); + } + + @Test + void fetchWellKnownEndpointNoJwkTests() throws RestClientException, IOException { + + OidcConfigurationFetcher fetcher = getSuccessfulFetcherWithNoJwk(); + OidcConfigurationResolutionError exception = + assertThrows(OidcConfigurationResolutionError.class, () -> { + fetcher.loadConfigurationForIssuer(ISSUER); + }); + assertEquals(format(NO_JWKS_URI_ERROR_TEMPLATE, ISSUER), exception.getMessage()); + } + + @Test + void fetchJWKEndpointTests() + throws RestClientException, IOException, ParseException, KeySourceException { + + OidcConfigurationFetcher fetcher = getSuccessfulFetcher(); + JWKSet key = JWKSet.parse(fetcher.loadJWKSourceForURL(URI.create(JWK_URI))); + + assertNotNull(key.getKeyByKeyId(KID)); + assertThat(key.getKeyByKeyId(KID).getKeyType(), is(KeyType.RSA)); + } + + @Test + void fetchJWKEndpointWithErrorTests() throws RestClientException { + + OidcConfigurationFetcher fetcher = getFetcherWithErrorOnGetJwk(); + final URI jwkUri = URI.create(JWK_URI); + KeySourceException exception = assertThrows(KeySourceException.class, () -> { + fetcher.loadJWKSourceForURL(jwkUri); + }); + String expectedMessage = + "Unable to get JWK from '" + jwkUri + "': received status code " + NOT_FOUND.value(); + String actualMessage = exception.getMessage(); + + assertEquals(expectedMessage, actualMessage); + } + + @Test + void fetchJWKEndpointWithRuntimeException() throws RestClientException { + + OidcConfigurationFetcher fetcher = getFetcherWithRuntimeExceptionOnGetJwk(); + final URI jwkUri = URI.create(JWK_URI); + RemoteKeySourceException exception = assertThrows(RemoteKeySourceException.class, () -> { + fetcher.loadJWKSourceForURL(jwkUri); + }); + String expectedMessage = "Unable to get JWK from 'https://iam-dev.cloud.cnaf.infn.it/jwk'"; + String actualMessage = exception.getMessage(); + + assertEquals(expectedMessage, actualMessage); + } +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/TrustedJwtDecoderCacheLoaderTest.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/TrustedJwtDecoderCacheLoaderTest.java new file mode 100644 index 00000000..62af0313 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwk/TrustedJwtDecoderCacheLoaderTest.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.test.oauth.jwk; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.lenient; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.apache.commons.io.FileUtils; +import org.italiangrid.storm.webdav.config.OAuthProperties; +import org.italiangrid.storm.webdav.config.OAuthProperties.AuthorizationServer; +import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties; +import org.italiangrid.storm.webdav.config.ServiceConfigurationProperties.AuthorizationServerProperties; +import org.italiangrid.storm.webdav.oauth.utils.OidcConfigurationFetcher; +import org.italiangrid.storm.webdav.oauth.utils.TrustedJwtDecoderCacheLoader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.util.concurrent.ListenableFuture; +import com.nimbusds.jose.KeySourceException; + +@ExtendWith(MockitoExtension.class) +class TrustedJwtDecoderCacheLoaderTest { + + private static final String ISSUER = "https://wlcg.cloud.cnaf.infn.it/"; + private static final String JWK_URI = "https://wlcg.cloud.cnaf.infn.it/jwks"; + + @Mock + ServiceConfigurationProperties properties; + @Mock + OAuthProperties oauthProperties; + @Mock + RestTemplateBuilder builder; + @Mock + OidcConfigurationFetcher fetcher; + + private ExecutorService executor; + private TrustedJwtDecoderCacheLoader jwtLoader; + + @BeforeEach + public void setup() throws IOException, KeySourceException { + + AuthorizationServer as = new AuthorizationServer(); + as.setIssuer(ISSUER); + as.setJwkUri(JWK_URI); + List issuerServers = Lists.newArrayList(as); + lenient().when(oauthProperties.getIssuers()).thenReturn(issuerServers); + + Map oidcConfiguration = Maps.newHashMap(); + oidcConfiguration.put("issuer", ISSUER); + oidcConfiguration.put("jwks_uri", JWK_URI); + + ClassLoader classLoader = getClass().getClassLoader(); + File file = new File(classLoader.getResource("jwk/test-keystore.jwks").getFile()); + String data = FileUtils.readFileToString(file, "UTF-8"); + + lenient().when(fetcher.loadConfigurationForIssuer(ISSUER)).thenReturn(oidcConfiguration); + lenient().when(fetcher.loadJWKSourceForURL(URI.create(JWK_URI))).thenReturn(data); + + AuthorizationServerProperties props = new AuthorizationServerProperties(); + props.setEnabled(false); + props.setIssuer("http://localhost"); + lenient().when(properties.getAuthzServer()).thenReturn(props); + + executor = Executors.newScheduledThreadPool(1); + + jwtLoader = + new TrustedJwtDecoderCacheLoader(properties, oauthProperties, builder, fetcher, executor); + + } + + @Test + void testLoadRemoteIssuerConfiguration() throws Exception { + + JwtDecoder decoder = jwtLoader.load(ISSUER); + assertTrue(decoder instanceof NimbusJwtDecoder); + ListenableFuture reloaded = jwtLoader.reload(ISSUER, decoder); + JwtDecoder newDecoder = reloaded.get(); + assertTrue(newDecoder instanceof NimbusJwtDecoder); + } +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/JwtIssuerTest.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/JwtIssuerTest.java index 73a15f41..3ecd3946 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/JwtIssuerTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/JwtIssuerTest.java @@ -143,7 +143,7 @@ public void setup() { } @Test - public void canCreateSignedJWT() throws ParseException, JOSEException { + void canCreateSignedJWT() throws ParseException, JOSEException { SignedJWT jwt = issuer.createAccessToken(req, authn); assertThat(jwt, notNullValue()); @@ -162,7 +162,7 @@ public void canCreateSignedJWT() throws ParseException, JOSEException { @Test - public void returnsAuthoritiesAsExpected() throws ParseException { + void returnsAuthoritiesAsExpected() throws ParseException { SAPermission canReadTest = SAPermission.canRead("test"); SAPermission canWriteTest = SAPermission.canWrite("test"); @@ -184,7 +184,7 @@ public void returnsAuthoritiesAsExpected() throws ParseException { } @Test - public void tokenIssuerLimitsTokenValidityToAcLifetime() throws ParseException { + void tokenIssuerLimitsTokenValidityToAcLifetime() throws ParseException { SAPermission canReadTest = SAPermission.canRead("test"); SAPermission canWriteTest = SAPermission.canWrite("test"); @@ -201,7 +201,7 @@ public void tokenIssuerLimitsTokenValidityToAcLifetime() throws ParseException { } @Test - public void tokenIssuerLimitsTokenValidtyWithRequestedLifetime() throws ParseException { + void tokenIssuerLimitsTokenValidtyWithRequestedLifetime() throws ParseException { SAPermission canReadTest = SAPermission.canRead("test"); SAPermission canWriteTest = SAPermission.canWrite("test"); when(ps.getSAPermissions(authn)).thenReturn(Sets.newHashSet(canReadTest, canWriteTest)); @@ -218,7 +218,7 @@ public void tokenIssuerLimitsTokenValidtyWithRequestedLifetime() throws ParseExc @Test - public void tokenIssuerIgnoresRequestedLifetimeWhenExceedsInternalLimit() throws ParseException { + void tokenIssuerIgnoresRequestedLifetimeWhenExceedsInternalLimit() throws ParseException { SAPermission canReadTest = SAPermission.canRead("test"); SAPermission canWriteTest = SAPermission.canWrite("test"); when(ps.getSAPermissions(authn)).thenReturn(Sets.newHashSet(canReadTest, canWriteTest)); @@ -234,7 +234,7 @@ public void tokenIssuerIgnoresRequestedLifetimeWhenExceedsInternalLimit() throws } @Test - public void tokenIssuerCreatesResourceAccessToken() throws ParseException { + void tokenIssuerCreatesResourceAccessToken() throws ParseException { when(resourceAtRequest.getPath()).thenReturn("/example/resource"); when(resourceAtRequest.getPermission()).thenReturn(Permission.r); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/NoExpirationStringCacheTest.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/NoExpirationStringCacheTest.java new file mode 100644 index 00000000..a5c8653c --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/NoExpirationStringCacheTest.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.test.oauth.jwt; + +import static org.junit.Assert.assertEquals; + +import org.italiangrid.storm.webdav.oauth.utils.NoExpirationStringCache; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class NoExpirationStringCacheTest { + + private static final String CACHED_VALUE = "this-is-my-cached-value"; + private static final String FAKE_ISSUER = "http://localhost"; + + @Test + void noExpirationCacheWorks() { + + NoExpirationStringCache cache = new NoExpirationStringCache(CACHED_VALUE); + + assertEquals("NoExpirationCache", cache.getName()); + assertEquals(cache, cache.getNativeCache()); + assertEquals(CACHED_VALUE, cache.get(FAKE_ISSUER).get()); + cache.clear(); + cache.put(FAKE_ISSUER, CACHED_VALUE); + assertEquals(CACHED_VALUE, cache.get(FAKE_ISSUER).get()); + cache.evict(FAKE_ISSUER); + assertEquals(CACHED_VALUE, cache.get(FAKE_ISSUER).get()); + } +} \ No newline at end of file diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/TokenServiceTest.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/TokenServiceTest.java index 89fbdd8c..9a70ae63 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/TokenServiceTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/jwt/TokenServiceTest.java @@ -82,7 +82,7 @@ public void setup() throws ParseException { } @Test - public void canGenerateTokenResponse() { + void canGenerateTokenResponse() { TokenResponseDTO response = issuerService.createAccessToken(request, auth); assertThat(response.getTokenType(), is(BEARER_TOKEN_TYPE)); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/oauth/validator/AudienceValidatorTests.java b/src/test/java/org/italiangrid/storm/webdav/test/oauth/validator/AudienceValidatorTests.java index 39159bb5..c0e8419d 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/oauth/validator/AudienceValidatorTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/oauth/validator/AudienceValidatorTests.java @@ -35,7 +35,7 @@ import com.google.common.collect.Lists; @ExtendWith(MockitoExtension.class) -public class AudienceValidatorTests { +class AudienceValidatorTests { @Mock AuthorizationServer server; @@ -52,7 +52,7 @@ public void setup() { } @Test - public void testNullAudiences() { + void testNullAudiences() { when(server.getAudiences()).thenReturn(null); assertThrows(IllegalArgumentException.class, () -> { validator = new AudienceValidator(server); @@ -60,7 +60,7 @@ public void testNullAudiences() { } @Test - public void testEmptyAudiences() { + void testEmptyAudiences() { when(server.getAudiences()).thenReturn(emptyList()); assertThrows(IllegalArgumentException.class, () -> { validator = new AudienceValidator(server); @@ -68,28 +68,28 @@ public void testEmptyAudiences() { } @Test - public void testNoAudienceInTokenYeldsSuccess() { + void testNoAudienceInTokenYeldsSuccess() { when(jwt.getAudience()).thenReturn(null); validator = new AudienceValidator(server); assertThat(validator.validate(jwt).hasErrors(), is(false)); } @Test - public void testEmptyAudienceInTokenYeldsSuccess() { + void testEmptyAudienceInTokenYeldsSuccess() { when(jwt.getAudience()).thenReturn(emptyList()); validator = new AudienceValidator(server); assertThat(validator.validate(jwt).hasErrors(), is(false)); } @Test - public void testInvalidAudienceIsError() { + void testInvalidAudienceIsError() { when(jwt.getAudience()).thenReturn(Lists.newArrayList("testAudience")); validator = new AudienceValidator(server); assertThat(validator.validate(jwt).hasErrors(), is(true)); } @Test - public void testAudienceValidationSuccess() { + void testAudienceValidationSuccess() { when(jwt.getAudience()).thenReturn(Lists.newArrayList("any")); validator = new AudienceValidator(server); assertThat(validator.validate(jwt).hasErrors(), is(false)); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/redirector/RandomReplicaSelectorTests.java b/src/test/java/org/italiangrid/storm/webdav/test/redirector/RandomReplicaSelectorTests.java index 1c0cb171..5407e143 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/redirector/RandomReplicaSelectorTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/redirector/RandomReplicaSelectorTests.java @@ -32,7 +32,7 @@ import com.google.common.collect.Sets; @RunWith(MockitoJUnitRunner.class) -public class RandomReplicaSelectorTests extends RedirectorTestSupport { +class RandomReplicaSelectorTests extends RedirectorTestSupport { ServiceConfigurationProperties config; RandomReplicaSelector selector; @@ -44,14 +44,14 @@ public void setup() { } @Test - public void testEmptyOptionalOnEmptyEndpointList() { + void testEmptyOptionalOnEmptyEndpointList() { assertThat(selector.selectReplica().isPresent(), is(false)); } @Test - public void testSingleEndpointList() { + void testSingleEndpointList() { ReplicaEndpointProperties replica = new ReplicaEndpointProperties(); replica.setEndpoint(ENDPOINT_URI_0); @@ -65,7 +65,7 @@ public void testSingleEndpointList() { @Test - public void testDoubleEndpointList() { + void testDoubleEndpointList() { ReplicaEndpointProperties replica0 = new ReplicaEndpointProperties(); replica0.setEndpoint(ENDPOINT_URI_0); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/redirector/RedirectFilterTests.java b/src/test/java/org/italiangrid/storm/webdav/test/redirector/RedirectFilterTests.java index 10110200..ec96bbc7 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/redirector/RedirectFilterTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/redirector/RedirectFilterTests.java @@ -94,7 +94,7 @@ public void setup() { } @Test - public void filterIgnoresPlainHttpRequests() throws IOException, ServletException { + void filterIgnoresPlainHttpRequests() throws IOException, ServletException { when(request.getScheme()).thenReturn("http"); filter.doFilter(request, response, filterChain); @@ -104,7 +104,7 @@ public void filterIgnoresPlainHttpRequests() throws IOException, ServletExceptio } @Test - public void filterIgnoresPlainDavRequests() throws IOException, ServletException { + void filterIgnoresPlainDavRequests() throws IOException, ServletException { when(request.getScheme()).thenReturn("dav"); filter.doFilter(request, response, filterChain); @@ -114,7 +114,7 @@ public void filterIgnoresPlainDavRequests() throws IOException, ServletException } @Test - public void filterIgnoresRequestWithAccessToken() throws IOException, ServletException { + void filterIgnoresRequestWithAccessToken() throws IOException, ServletException { when(request.getParameter("access_token")).thenReturn(RANDOM_TOKEN_STRING); filter.doFilter(request, response, filterChain); @@ -124,7 +124,7 @@ public void filterIgnoresRequestWithAccessToken() throws IOException, ServletExc } @Test - public void filterIgnoresRequestWithNoRedirect() throws IOException, ServletException { + void filterIgnoresRequestWithNoRedirect() throws IOException, ServletException { Map parameterMap = Maps.newHashMap(); parameterMap.put("no_redirect", new String[] {}); @@ -137,7 +137,7 @@ public void filterIgnoresRequestWithNoRedirect() throws IOException, ServletExce } @Test - public void filterIgnoresDirectoryRequest() throws IOException, ServletException { + void filterIgnoresDirectoryRequest() throws IOException, ServletException { when(file.isFile()).thenReturn(false); @@ -148,7 +148,7 @@ public void filterIgnoresDirectoryRequest() throws IOException, ServletException } @Test - public void filterIgnoresResourceNotFound() throws IOException, ServletException { + void filterIgnoresResourceNotFound() throws IOException, ServletException { when(pathResolver.getPath("/example/file")).thenReturn(null); @@ -159,13 +159,13 @@ public void filterIgnoresResourceNotFound() throws IOException, ServletException } @Test - public void filterSendsRedirect() throws IOException, ServletException { + void filterSendsRedirect() throws IOException, ServletException { filter.doFilter(request, response, filterChain); verify(redirectionService).buildRedirect(Mockito.any(), Mockito.eq(request), Mockito.eq(response)); - verify(response).setStatus(Mockito.eq(SC_TEMPORARY_REDIRECT)); + verify(response).setStatus(SC_TEMPORARY_REDIRECT); verify(response).setHeader(Mockito.eq("Location"), redirectUrl.capture()); assertThat(redirectUrl.getValue(), is(REDIRECTED_URL)); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/redirector/RedirectionServiceTests.java b/src/test/java/org/italiangrid/storm/webdav/test/redirector/RedirectionServiceTests.java index 788f1781..6f7d4489 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/redirector/RedirectionServiceTests.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/redirector/RedirectionServiceTests.java @@ -49,7 +49,7 @@ import com.nimbusds.jwt.SignedJWT; @ExtendWith(MockitoExtension.class) -public class RedirectionServiceTests extends RedirectorTestSupport { +class RedirectionServiceTests extends RedirectorTestSupport { @Mock @@ -91,7 +91,7 @@ public void setup() { @Test - public void testRedirectFailureOnEmptyReplica() { + void testRedirectFailureOnEmptyReplica() { Exception e = assertThrows(RedirectError.class, () -> { service.buildRedirect(authentication, request, response); @@ -100,7 +100,7 @@ public void testRedirectFailureOnEmptyReplica() { } @Test - public void testRedirectUriConstruction() { + void testRedirectUriConstruction() { when(selector.selectReplica()).thenReturn(Optional.of(REPLICA_0)); String uriString = service.buildRedirect(authentication, request, response); @@ -121,7 +121,7 @@ public void testRedirectUriConstruction() { } @Test - public void testPutRedirectUriConstruction() { + void testPutRedirectUriConstruction() { when(selector.selectReplica()).thenReturn(Optional.of(REPLICA_0)); when(request.getMethod()).thenReturn("PUT"); @@ -143,7 +143,7 @@ public void testPutRedirectUriConstruction() { } @Test - public void testRedirectUriWithPrefixConstruction() { + void testRedirectUriWithPrefixConstruction() { when(selector.selectReplica()).thenReturn(Optional.of(REPLICA_WITH_PREFIX)); String uriString = service.buildRedirect(authentication, request, response); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/ClientInfoParserTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/ClientInfoParserTest.java index 35afedc9..85aac3e2 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/ClientInfoParserTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/ClientInfoParserTest.java @@ -24,10 +24,10 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -public class ClientInfoParserTest { +class ClientInfoParserTest { @Test - public void testClientInfoHeaderParsing() { + void testClientInfoHeaderParsing() { ClientInfo ci = ClientInfo .fromHeaderString("job-id=34f98a5e-1e49-11e9-ab17-fa163edecedf;file-id=8764139989;retry=0"); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/PullTransferTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/PullTransferTest.java index 3c7e84a6..09527a6a 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/PullTransferTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/PullTransferTest.java @@ -35,25 +35,27 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.italiangrid.storm.webdav.tpc.TransferConstants; import com.google.common.collect.Multimap; @ExtendWith(MockitoExtension.class) -public class PullTransferTest extends TransferFilterTestSupport { +class PullTransferTest extends TransferFilterTestSupport { + @Override @BeforeEach public void setup() throws IOException { super.setup(); lenient().when(request.getMethod()).thenReturn(COPY.name()); lenient().when(request.getServletPath()).thenReturn(SERVLET_PATH); lenient().when(request.getPathInfo()).thenReturn(LOCAL_PATH); - lenient().when(request.getHeader(SOURCE_HEADER)).thenReturn(HTTP_URL); - lenient().when(request.getHeader(OVERWRITE_HEADER)).thenReturn(null); - lenient().when(request.getHeader(DESTINATION_HEADER)).thenReturn(null); - lenient().when(request.getHeader(CLIENT_INFO_HEADER)).thenReturn(null); - lenient().when(request.getHeader(CREDENTIAL_HEADER)).thenReturn(null); - lenient().when(request.getHeader(REQUIRE_CHECKSUM_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(HTTP_URL); + lenient().when(request.getHeader(TransferConstants.OVERWRITE_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.DESTINATION_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.CLIENT_INFO_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.CREDENTIAL_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.REQUIRE_CHECKSUM_HEADER)).thenReturn(null); lenient().when(request.getHeaderNames()).thenReturn(emptyEnumeration()); lenient().when(resolver.pathExists(FULL_LOCAL_PATH)).thenReturn(false); lenient().when(resolver.pathExists(FULL_LOCAL_PATH_PARENT)).thenReturn(true); @@ -73,7 +75,7 @@ void pullEmptyTransferHeaders() throws IOException, ServletException { @Test void overwriteHeaderRecognized() throws IOException, ServletException { - when(request.getHeader(OVERWRITE_HEADER)).thenReturn("F"); + when(request.getHeader(TransferConstants.OVERWRITE_HEADER)).thenReturn("F"); filter.doFilter(request, response, chain); verify(client).handle(getXferRequest.capture(), Mockito.any()); assertThat(getXferRequest.getValue().path(), is(FULL_LOCAL_PATH)); @@ -86,7 +88,7 @@ void overwriteHeaderRecognized() throws IOException, ServletException { @Test void checksumRecognized() throws IOException, ServletException { - when(request.getHeader(REQUIRE_CHECKSUM_HEADER)).thenReturn("false"); + when(request.getHeader(TransferConstants.REQUIRE_CHECKSUM_HEADER)).thenReturn("false"); filter.doFilter(request, response, chain); verify(client).handle(getXferRequest.capture(), Mockito.any()); assertThat(getXferRequest.getValue().path(), is(FULL_LOCAL_PATH)); @@ -104,9 +106,11 @@ void checkTransferHeaderPassing() throws IOException, ServletException { .thenReturn(TRANSFER_HEADER_AUTHORIZATION_VALUE); when(request.getHeader(TRANSFER_HEADER_WHATEVER_KEY)) .thenReturn(TRANSFER_HEADER_WHATEVER_VALUE); + when(request.getHeader(TRANSFER_HEADER_SCITAG)).thenReturn(SCITAG_HEADER_VALUE); + when(request.getHeader(SCITAG_HEADER)).thenReturn(null); - when(request.getHeaderNames()).thenReturn( - enumeration(asList(TRANSFER_HEADER_AUTHORIZATION_KEY, TRANSFER_HEADER_WHATEVER_KEY))); + when(request.getHeaderNames()).thenReturn(enumeration( + asList(TRANSFER_HEADER_AUTHORIZATION_KEY, TRANSFER_HEADER_WHATEVER_KEY, TRANSFER_HEADER_SCITAG))); filter.doFilter(request, response, chain); verify(client).handle(getXferRequest.capture(), Mockito.any()); @@ -118,18 +122,20 @@ void checkTransferHeaderPassing() throws IOException, ServletException { Multimap xferHeaders = getXferRequest.getValue().transferHeaders(); - assertThat(xferHeaders.size(), is(2)); + assertThat(xferHeaders.size(), is(3)); assertThat(xferHeaders.containsKey("Authorization"), is(true)); assertThat(xferHeaders.get("Authorization").iterator().next(), is(TRANSFER_HEADER_AUTHORIZATION_VALUE)); assertThat(xferHeaders.containsKey("Whatever"), is(true)); assertThat(xferHeaders.get("Whatever").iterator().next(), is(TRANSFER_HEADER_WHATEVER_VALUE)); + assertThat(xferHeaders.containsKey("SciTag"), is(true)); + assertThat(xferHeaders.get("SciTag").iterator().next(), is(SCITAG_HEADER_VALUE)); } @Test void emptyTransferHeaderAreIgnored() throws IOException, ServletException { - when(request.getHeaderNames()) - .thenReturn(enumeration(asList(TRANSFER_HEADER, TRANSFER_HEADER_WHATEVER_KEY))); + when(request.getHeaderNames()).thenReturn( + enumeration(asList(TRANSFER_HEADER, TRANSFER_HEADER_WHATEVER_KEY))); when(request.getHeader(TRANSFER_HEADER_WHATEVER_KEY)) .thenReturn(TRANSFER_HEADER_WHATEVER_VALUE); @@ -150,4 +156,26 @@ void emptyTransferHeaderAreIgnored() throws IOException, ServletException { assertThat(xferHeaders.get("Whatever").iterator().next(), is(TRANSFER_HEADER_WHATEVER_VALUE)); } + @Test + void bothSciTagAndTransferHeaderSciTag() throws IOException, ServletException { + when(request.getHeaderNames()) + .thenReturn(enumeration(asList(SCITAG_HEADER, TRANSFER_HEADER_SCITAG))); + + when(request.getHeader(SCITAG_HEADER)).thenReturn(SCITAG_HEADER_VALUE); + + filter.doFilter(request, response, chain); + verify(client).handle(getXferRequest.capture(), Mockito.any()); + + assertThat(getXferRequest.getValue().path(), is(FULL_LOCAL_PATH)); + assertThat(getXferRequest.getValue().remoteURI(), is(HTTP_URL_URI)); + assertThat(getXferRequest.getValue().overwrite(), is(true)); + assertThat(getXferRequest.getValue().verifyChecksum(), is(true)); + + + Multimap xferHeaders = getXferRequest.getValue().transferHeaders(); + assertThat(xferHeaders.size(), is(0)); + + assertThat(xferHeaders.containsKey("SciTag"), is(false)); + } + } diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/PushTransferTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/PushTransferTest.java index e70feeb0..8806e50c 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/PushTransferTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/PushTransferTest.java @@ -37,27 +37,29 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.italiangrid.storm.webdav.tpc.TransferConstants; import com.google.common.collect.Multimap; @ExtendWith(MockitoExtension.class) -public class PushTransferTest extends TransferFilterTestSupport { +class PushTransferTest extends TransferFilterTestSupport { + @Override @BeforeEach public void setup() throws IOException { super.setup(); lenient().when(request.getMethod()).thenReturn(COPY.name()); lenient().when(request.getServletPath()).thenReturn(SERVLET_PATH); lenient().when(request.getPathInfo()).thenReturn(LOCAL_PATH); - lenient().when(request.getHeader(DESTINATION_HEADER)).thenReturn(HTTPS_URL); + lenient().when(request.getHeader(TransferConstants.DESTINATION_HEADER)).thenReturn(HTTPS_URL); lenient().when(request.getHeaderNames()).thenReturn(emptyEnumeration()); lenient().when(resolver.pathExists(FULL_LOCAL_PATH)).thenReturn(true); - lenient().when(request.getHeader(SOURCE_HEADER)).thenReturn(null); - lenient().when(request.getHeader(CLIENT_INFO_HEADER)).thenReturn(null); - lenient().when(request.getHeader(OVERWRITE_HEADER)).thenReturn(null); - lenient().when(request.getHeader(REQUIRE_CHECKSUM_HEADER)).thenReturn(null); - lenient().when(request.getHeader(CREDENTIAL_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.CLIENT_INFO_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.OVERWRITE_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.REQUIRE_CHECKSUM_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.CREDENTIAL_HEADER)).thenReturn(null); } @@ -81,7 +83,7 @@ void pushEmptyTransferHeaders() throws IOException, ServletException { @Test void overwriteHeaderRecognized() throws IOException, ServletException { - when(request.getHeader(OVERWRITE_HEADER)).thenReturn("F"); + when(request.getHeader(TransferConstants.OVERWRITE_HEADER)).thenReturn("F"); filter.doFilter(request, response, chain); verify(client).handle(putXferRequest.capture(), Mockito.any()); assertThat(putXferRequest.getValue().path(), is(FULL_LOCAL_PATH)); @@ -94,7 +96,7 @@ void overwriteHeaderRecognized() throws IOException, ServletException { @Test void checksumRecognized() throws IOException, ServletException { - when(request.getHeader(REQUIRE_CHECKSUM_HEADER)).thenReturn("false"); + when(request.getHeader(TransferConstants.REQUIRE_CHECKSUM_HEADER)).thenReturn("false"); filter.doFilter(request, response, chain); verify(client).handle(putXferRequest.capture(), Mockito.any()); assertThat(putXferRequest.getValue().path(), is(FULL_LOCAL_PATH)); @@ -113,8 +115,8 @@ void checkTransferHeaderPassing() throws IOException, ServletException { when(request.getHeader(TRANSFER_HEADER_WHATEVER_KEY)) .thenReturn(TRANSFER_HEADER_WHATEVER_VALUE); - when(request.getHeaderNames()).thenReturn( - enumeration(asList(TRANSFER_HEADER_AUTHORIZATION_KEY, TRANSFER_HEADER_WHATEVER_KEY))); + when(request.getHeaderNames()).thenReturn(enumeration( + asList(TRANSFER_HEADER_AUTHORIZATION_KEY, TRANSFER_HEADER_WHATEVER_KEY, SCITAG_HEADER))); filter.doFilter(request, response, chain); verify(client).handle(putXferRequest.capture(), Mockito.any()); @@ -132,12 +134,13 @@ void checkTransferHeaderPassing() throws IOException, ServletException { is(TRANSFER_HEADER_AUTHORIZATION_VALUE)); assertThat(xferHeaders.containsKey("Whatever"), is(true)); assertThat(xferHeaders.get("Whatever").iterator().next(), is(TRANSFER_HEADER_WHATEVER_VALUE)); + assertThat(xferHeaders.containsKey("SciTag"), is(false)); } @Test void emptyTransferHeaderAreIgnored() throws IOException, ServletException { - when(request.getHeaderNames()) - .thenReturn(enumeration(asList(TRANSFER_HEADER, TRANSFER_HEADER_WHATEVER_KEY))); + when(request.getHeaderNames()).thenReturn( + enumeration(asList(TRANSFER_HEADER, TRANSFER_HEADER_WHATEVER_KEY))); when(request.getHeader(TRANSFER_HEADER_WHATEVER_KEY)) .thenReturn(TRANSFER_HEADER_WHATEVER_VALUE); @@ -207,4 +210,26 @@ void checkExpectContinueHeaderIsNotSet() throws IOException, ServletException { assertThat(xferHeaders.containsKey(EXPECTED_HEADER), is(false)); } + + @Test + void bothSciTagAndTransferHeaderSciTag() throws IOException, ServletException { + when(request.getHeaderNames()) + .thenReturn(enumeration(asList(SCITAG_HEADER, TRANSFER_HEADER_SCITAG))); + + when(request.getHeader(SCITAG_HEADER)).thenReturn(SCITAG_HEADER_VALUE); + + filter.doFilter(request, response, chain); + verify(client).handle(putXferRequest.capture(), Mockito.any()); + + assertThat(putXferRequest.getValue().path(), is(FULL_LOCAL_PATH)); + assertThat(putXferRequest.getValue().remoteURI(), is(HTTPS_URL_URI)); + assertThat(putXferRequest.getValue().overwrite(), is(true)); + assertThat(putXferRequest.getValue().verifyChecksum(), is(true)); + + + Multimap xferHeaders = putXferRequest.getValue().transferHeaders(); + assertThat(xferHeaders.size(), is(0)); + + assertThat(xferHeaders.containsKey("SciTag"), is(false)); + } } diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/SciTagFilterActivationTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/SciTagFilterActivationTest.java new file mode 100644 index 00000000..51e587c9 --- /dev/null +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/SciTagFilterActivationTest.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.italiangrid.storm.webdav.test.tpc; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.IOException; + +import javax.servlet.ServletException; + +import org.italiangrid.storm.webdav.config.StorageAreaInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.italiangrid.storm.webdav.server.servlet.SciTagFilter; +import org.italiangrid.storm.webdav.scitag.SciTag; +import org.italiangrid.storm.webdav.scitag.SciTagTransfer; +import org.italiangrid.storm.webdav.tpc.TransferConstants; + +@ExtendWith(MockitoExtension.class) +class SciTagFilterActivationTest extends TransferFilterTestSupport { + @TempDir + File tempDir; + + SciTagFilter sciTagFilter; + + @Mock + StorageAreaInfo testSa; + + @Mock + StorageAreaInfo otherSa; + + @Override + @BeforeEach + public void setup() throws IOException { + super.setup(); + sciTagFilter = new SciTagFilter(); + lenient().when(request.getServletPath()).thenReturn(SERVLET_PATH); + lenient().when(request.getPathInfo()).thenReturn(LOCAL_PATH); + lenient().when(response.getWriter()).thenReturn(responseWriter); + lenient().when(resolver.resolveStorageArea(FULL_LOCAL_PATH)).thenReturn(testSa); + lenient().when(resolver.resolveStorageArea("/test/otherfile")).thenReturn(testSa); + lenient().when(resolver.resolveStorageArea("/other/file")).thenReturn(otherSa); + lenient().when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(null); + } + + @Test + void requestWithScitag() throws IOException, ServletException { + when(request.getMethod()).thenReturn("GET"); + when(request.getHeader(SCITAG_HEADER)).thenReturn("66"); + + sciTagFilter.doFilter(request, response, chain); + verify(chain).doFilter(request, response); + verify(request).setAttribute(eq(SciTag.SCITAG_ATTRIBUTE), any(SciTag.class)); + } + + @Test + void requestWithoutScitag() throws IOException, ServletException { + sciTagFilter.doFilter(request, response, chain); + verify(chain).doFilter(request, response); + verify(request, times(0)).setAttribute(eq(SciTag.SCITAG_ATTRIBUTE), any(SciTag.class)); + } + + @Test + void testSciTagWrite() { + SciTag scitag = new SciTag(1, 2, false); + assertThat(scitag.experimentId(), is(1)); + assertThat(scitag.activityId(), is(2)); + assertThat(scitag.remoteAddressIsSource(), is(false)); + File mockFile = new File(tempDir, "flowd"); + SciTagTransfer scitagTransfer = + new SciTagTransfer(scitag, "10.10.10.10", 8443, "10.10.10.11", 12345, mockFile); + scitagTransfer.writeStart(); + scitagTransfer.writeEnd(); + } + +} diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterActivationTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterActivationTest.java index 7fea4844..b7b9a3a0 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterActivationTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterActivationTest.java @@ -31,6 +31,7 @@ import org.italiangrid.storm.webdav.config.StorageAreaInfo; import org.italiangrid.storm.webdav.server.servlet.WebDAVMethod; +import org.italiangrid.storm.webdav.tpc.TransferConstants; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,7 +40,7 @@ import org.springframework.http.HttpMethod; @ExtendWith(MockitoExtension.class) -public class TransferFilterActivationTest extends TransferFilterTestSupport { +class TransferFilterActivationTest extends TransferFilterTestSupport { @Mock StorageAreaInfo testSa; @@ -47,6 +48,7 @@ public class TransferFilterActivationTest extends TransferFilterTestSupport { @Mock StorageAreaInfo otherSa; + @Override @BeforeEach public void setup() throws IOException { super.setup(); @@ -56,11 +58,11 @@ public void setup() throws IOException { lenient().when(resolver.resolveStorageArea(FULL_LOCAL_PATH)).thenReturn(testSa); lenient().when(resolver.resolveStorageArea("/test/otherfile")).thenReturn(testSa); lenient().when(resolver.resolveStorageArea("/other/file")).thenReturn(otherSa); - lenient().when(request.getHeader(SOURCE_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(null); } @Test - public void filterIgnoresOtherHttpOrWebdavMethods() throws IOException, ServletException { + void filterIgnoresOtherHttpOrWebdavMethods() throws IOException, ServletException { // Ignore Http methods for (HttpMethod m : HttpMethod.values()) { when(request.getMethod()).thenReturn(m.toString()); @@ -80,12 +82,12 @@ public void filterIgnoresOtherHttpOrWebdavMethods() throws IOException, ServletE } @Test - public void filterSkippedIfSourceAndDestionationHeaderMissing() + void filterSkippedIfSourceAndDestionationHeaderMissing() throws IOException, ServletException { // No source or destination header - when(request.getHeader(SOURCE_HEADER)).thenReturn(null); - when(request.getHeader(DESTINATION_HEADER)).thenReturn(null); + when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(null); + when(request.getHeader(TransferConstants.DESTINATION_HEADER)).thenReturn(null); when(request.getMethod()).thenReturn(COPY.toString()); filter.doFilter(request, response, chain); @@ -93,9 +95,10 @@ public void filterSkippedIfSourceAndDestionationHeaderMissing() } @Test - public void filterBlocksLocalCopyAcrossStorageAreas() throws IOException, ServletException { + void filterBlocksLocalCopyAcrossStorageAreas() throws IOException, ServletException { when(request.getMethod()).thenReturn(COPY.toString()); - when(request.getHeader(DESTINATION_HEADER)).thenReturn("https://localhost/other/file"); + when(request.getHeader(TransferConstants.DESTINATION_HEADER)) + .thenReturn("https://localhost/other/file"); filter.doFilter(request, response, chain); verify(responseWriter).print(error.capture()); assertThat(error.getValue(), is("Local copy across storage areas is not supported")); @@ -103,19 +106,21 @@ public void filterBlocksLocalCopyAcrossStorageAreas() throws IOException, Servle } @Test - public void filterIgnoresLocalCopyInSameStorageArea() throws IOException, ServletException { + void filterIgnoresLocalCopyInSameStorageArea() throws IOException, ServletException { when(request.getMethod()).thenReturn(COPY.toString()); - when(request.getHeader(DESTINATION_HEADER)).thenReturn("https://localhost/test/otherfile"); + when(request.getHeader(TransferConstants.DESTINATION_HEADER)) + .thenReturn("https://localhost/test/otherfile"); filter.doFilter(request, response, chain); verify(chain).doFilter(request, response); } @Test - public void filterHandlesLocalCopyWithTransferHeader() throws IOException, ServletException { + void filterHandlesLocalCopyWithTransferHeader() throws IOException, ServletException { when(request.getMethod()).thenReturn(COPY.toString()); - when(request.getHeader(DESTINATION_HEADER)).thenReturn("https://localhost/test/otherfile"); + when(request.getHeader(TransferConstants.DESTINATION_HEADER)) + .thenReturn("https://localhost/test/otherfile"); when(requestHeaderNames.hasMoreElements()).thenReturn(true, true, false); - when(requestHeaderNames.nextElement()).thenReturn(DESTINATION_HEADER, + when(requestHeaderNames.nextElement()).thenReturn(TransferConstants.DESTINATION_HEADER, TRANSFER_HEADER_AUTHORIZATION_KEY); filter.doFilter(request, response, chain); @@ -123,17 +128,17 @@ public void filterHandlesLocalCopyWithTransferHeader() throws IOException, Servl } @Test - public void filterHandlesRemoteSourceHeader() throws IOException, ServletException { + void filterHandlesRemoteSourceHeader() throws IOException, ServletException { when(request.getMethod()).thenReturn(COPY.toString()); - when(request.getHeader(SOURCE_HEADER)).thenReturn(HTTP_URL); + when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(HTTP_URL); filter.doFilter(request, response, chain); verifyNoInteractions(chain); } @Test - public void filterHandlesRemoteDestinationHeader() throws IOException, ServletException { + void filterHandlesRemoteDestinationHeader() throws IOException, ServletException { when(request.getMethod()).thenReturn(COPY.toString()); - when(request.getHeader(DESTINATION_HEADER)).thenReturn(HTTP_URL); + when(request.getHeader(TransferConstants.DESTINATION_HEADER)).thenReturn(HTTP_URL); filter.doFilter(request, response, chain); verifyNoInteractions(chain); } diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterTestSupport.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterTestSupport.java index 1702f996..6f81b5c1 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterTestSupport.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferFilterTestSupport.java @@ -33,7 +33,6 @@ import org.italiangrid.storm.webdav.server.PathResolver; import org.italiangrid.storm.webdav.tpc.LocalURLService; import org.italiangrid.storm.webdav.tpc.StaticHostListLocalURLService; -import org.italiangrid.storm.webdav.tpc.TransferConstants; import org.italiangrid.storm.webdav.tpc.TransferFilter; import org.italiangrid.storm.webdav.tpc.transfer.GetTransferRequest; import org.italiangrid.storm.webdav.tpc.transfer.PutTransferRequest; @@ -42,7 +41,7 @@ import org.mockito.Captor; import org.mockito.Mock; -public class TransferFilterTestSupport implements TransferConstants { +public class TransferFilterTestSupport { public static final Instant NOW = Instant.parse("2021-01-01T00:00:00.00Z"); @@ -67,10 +66,14 @@ public class TransferFilterTestSupport implements TransferConstants { public static final String TRANSFER_HEADER_WHATEVER_KEY = "TransferHeaderWhatever"; public static final String TRANSFER_HEADER_WHATEVER_VALUE = "papisilviobelluscona"; + public static final String SCITAG_HEADER = "SciTag"; + public static final String SCITAG_HEADER_VALUE = "65"; + public static final String TRANSFER_HEADER_SCITAG = "TransferHeaderSciTag"; + public static final URI HTTP_URL_URI = URI.create(HTTP_URL); public static final URI HTTPS_URL_URI = URI.create(HTTPS_URL); - public static String[] INVALID_URLs = + public static final String[] INVALID_URLs = {"http:whatever", "httpg://storm.example/test", "gsiftp://whatever/test"}; public static final String EXPECTED_HEADER = org.apache.http.protocol.HTTP.EXPECT_DIRECTIVE; diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferRequestValidationTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferRequestValidationTest.java index e4b8786a..619e7782 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferRequestValidationTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferRequestValidationTest.java @@ -36,10 +36,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +import org.italiangrid.storm.webdav.tpc.TransferConstants; @ExtendWith(MockitoExtension.class) -public class TransferRequestValidationTest extends TransferFilterTestSupport { +class TransferRequestValidationTest extends TransferFilterTestSupport { + @Override @BeforeEach public void setup() throws IOException { super.setup(); @@ -49,18 +51,18 @@ public void setup() throws IOException { lenient().when(request.getHeaderNames()).thenReturn(emptyEnumeration()); lenient().when(resolver.pathExists(FULL_LOCAL_PATH)).thenReturn(false); lenient().when(resolver.pathExists(FULL_LOCAL_PATH_PARENT)).thenReturn(true); - lenient().when(request.getHeader(SOURCE_HEADER)).thenReturn(null); - lenient().when(request.getHeader(OVERWRITE_HEADER)).thenReturn(null); - lenient().when(request.getHeader(DESTINATION_HEADER)).thenReturn(null); - lenient().when(request.getHeader(CLIENT_INFO_HEADER)).thenReturn(null); - lenient().when(request.getHeader(CREDENTIAL_HEADER)).thenReturn(null); - lenient().when(request.getHeader(REQUIRE_CHECKSUM_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.OVERWRITE_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.DESTINATION_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.CLIENT_INFO_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.CREDENTIAL_HEADER)).thenReturn(null); + lenient().when(request.getHeader(TransferConstants.REQUIRE_CHECKSUM_HEADER)).thenReturn(null); } @Test - public void sourceAndDestHeaderPresent() throws IOException, ServletException { - when(request.getHeader(SOURCE_HEADER)).thenReturn(HTTP_URL); - when(request.getHeader(DESTINATION_HEADER)).thenReturn(HTTP_URL); + void sourceAndDestHeaderPresent() throws IOException, ServletException { + when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(HTTP_URL); + when(request.getHeader(TransferConstants.DESTINATION_HEADER)).thenReturn(HTTP_URL); filter.doFilter(request, response, chain); verify(response).sendError(httpStatus.capture(), error.capture()); @@ -69,9 +71,9 @@ public void sourceAndDestHeaderPresent() throws IOException, ServletException { } @Test - public void invalidDestinationURIs() throws IOException, ServletException { + void invalidDestinationURIs() throws IOException, ServletException { for (String u : INVALID_URLs) { - when(request.getHeader(DESTINATION_HEADER)).thenReturn(u); + when(request.getHeader(TransferConstants.DESTINATION_HEADER)).thenReturn(u); filter.doFilter(request, response, chain); verify(response).sendError(httpStatus.capture(), error.capture()); assertThat(httpStatus.getValue(), is(BAD_REQUEST.value())); @@ -82,9 +84,9 @@ public void invalidDestinationURIs() throws IOException, ServletException { @Test - public void invalidSourceURIs() throws IOException, ServletException { + void invalidSourceURIs() throws IOException, ServletException { for (String u : INVALID_URLs) { - when(request.getHeader(SOURCE_HEADER)).thenReturn(u); + when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(u); filter.doFilter(request, response, chain); verify(response).sendError(httpStatus.capture(), error.capture()); assertThat(httpStatus.getValue(), is(BAD_REQUEST.value())); @@ -94,14 +96,14 @@ public void invalidSourceURIs() throws IOException, ServletException { } @Test - public void invalidOverwriteHeader() throws IOException, ServletException { + void invalidOverwriteHeader() throws IOException, ServletException { String[] invalidValues = {"", "cccc", "whatever", "true", "false"}; - when(request.getHeader(SOURCE_HEADER)).thenReturn(HTTP_URL); + when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(HTTP_URL); for (String s : invalidValues) { - when(request.getHeader(OVERWRITE_HEADER)).thenReturn(s); + when(request.getHeader(TransferConstants.OVERWRITE_HEADER)).thenReturn(s); filter.doFilter(request, response, chain); verify(response).sendError(httpStatus.capture(), error.capture()); assertThat(httpStatus.getValue(), is(BAD_REQUEST.value())); @@ -111,12 +113,12 @@ public void invalidOverwriteHeader() throws IOException, ServletException { } @Test - public void invalidRequireChecksumHeader() throws IOException, ServletException { + void invalidRequireChecksumHeader() throws IOException, ServletException { String[] invalidValues = {"t", "F", ""}; - when(request.getHeader(SOURCE_HEADER)).thenReturn(HTTP_URL); + when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(HTTP_URL); for (String s : invalidValues) { - when(request.getHeader(REQUIRE_CHECKSUM_HEADER)).thenReturn(s); + when(request.getHeader(TransferConstants.REQUIRE_CHECKSUM_HEADER)).thenReturn(s); filter.doFilter(request, response, chain); verify(response).sendError(httpStatus.capture(), error.capture()); assertThat(httpStatus.getValue(), is(BAD_REQUEST.value())); @@ -126,8 +128,8 @@ public void invalidRequireChecksumHeader() throws IOException, ServletException } @Test - public void emptyOrNullPathInfo() throws IOException, ServletException { - when(request.getHeader(SOURCE_HEADER)).thenReturn(HTTP_URL); + void emptyOrNullPathInfo() throws IOException, ServletException { + when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(HTTP_URL); String[] invalidPathInfos = {null, "", "does/not/start/with/slash"}; String[] expectedErrorMsgs = {"Null or empty", "Null or empty", "Invalid local path"}; @@ -145,9 +147,9 @@ public void emptyOrNullPathInfo() throws IOException, ServletException { } @Test - public void invalidCredentialHeader() throws IOException, ServletException { - when(request.getHeader(SOURCE_HEADER)).thenReturn(HTTP_URL); - when(request.getHeader(CREDENTIAL_HEADER)).thenReturn("gridsite"); + void invalidCredentialHeader() throws IOException, ServletException { + when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(HTTP_URL); + when(request.getHeader(TransferConstants.CREDENTIAL_HEADER)).thenReturn("gridsite"); filter.doFilter(request, response, chain); verify(response).sendError(httpStatus.capture(), error.capture()); assertThat(httpStatus.getValue(), is(SC_BAD_REQUEST)); @@ -155,9 +157,9 @@ public void invalidCredentialHeader() throws IOException, ServletException { } @Test - public void noneCredentialHeaderAccepted() throws IOException, ServletException { - when(request.getHeader(SOURCE_HEADER)).thenReturn(HTTP_URL); - when(request.getHeader(CREDENTIAL_HEADER)).thenReturn("none"); + void noneCredentialHeaderAccepted() throws IOException, ServletException { + when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(HTTP_URL); + when(request.getHeader(TransferConstants.CREDENTIAL_HEADER)).thenReturn("none"); filter.doFilter(request, response, chain); verify(response).setStatus(httpStatus.capture()); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferReturnStatusTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferReturnStatusTest.java index 22bd4478..b54b1bac 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferReturnStatusTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferReturnStatusTest.java @@ -31,6 +31,7 @@ import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpResponseException; +import org.italiangrid.storm.webdav.tpc.TransferConstants; import org.italiangrid.storm.webdav.tpc.transfer.GetTransferRequest; import org.italiangrid.storm.webdav.tpc.transfer.error.ChecksumVerificationError; import org.italiangrid.storm.webdav.tpc.transfer.error.TransferError; @@ -42,28 +43,29 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -public class TransferReturnStatusTest extends TransferFilterTestSupport { +class TransferReturnStatusTest extends TransferFilterTestSupport { + @Override @BeforeEach public void setup() throws IOException { super.setup(); when(request.getMethod()).thenReturn(COPY.name()); when(request.getServletPath()).thenReturn(SERVLET_PATH); when(request.getPathInfo()).thenReturn(LOCAL_PATH); - when(request.getHeader(SOURCE_HEADER)).thenReturn(HTTP_URL); + when(request.getHeader(TransferConstants.SOURCE_HEADER)).thenReturn(HTTP_URL); when(request.getHeaderNames()).thenReturn(emptyEnumeration()); when(resolver.pathExists(FULL_LOCAL_PATH_PARENT)).thenReturn(true); } @Test - public void filterAnswers202() throws IOException, ServletException { + void filterAnswers202() throws IOException, ServletException { filter.doFilter(request, response, chain); verify(response).setStatus(httpStatus.capture()); assertThat(httpStatus.getValue(), is(SC_ACCEPTED)); } @Test - public void filterAnswers412ForClientProtocolException() throws IOException, ServletException { + void filterAnswers412ForClientProtocolException() throws IOException, ServletException { Mockito.doThrow(new ClientProtocolException("Connection error")) .when(client) .handle(ArgumentMatchers.any(), ArgumentMatchers.any()); @@ -75,7 +77,7 @@ public void filterAnswers412ForClientProtocolException() throws IOException, Ser } @Test - public void filterAnswers412ForHttpExceptionError() throws IOException, ServletException { + void filterAnswers412ForHttpExceptionError() throws IOException, ServletException { Mockito.doThrow(new HttpResponseException(HttpServletResponse.SC_FORBIDDEN, "Access denied")) .when(client) .handle(ArgumentMatchers.any(), ArgumentMatchers.any()); @@ -88,7 +90,7 @@ public void filterAnswers412ForHttpExceptionError() throws IOException, ServletE } @Test - public void filterAnswers412ForChecksumVerificationError() throws IOException, ServletException { + void filterAnswers412ForChecksumVerificationError() throws IOException, ServletException { Mockito.doThrow(new ChecksumVerificationError("Checksum verification error")) .when(client) .handle(ArgumentMatchers.any(), ArgumentMatchers.any()); @@ -100,7 +102,7 @@ public void filterAnswers412ForChecksumVerificationError() throws IOException, S } @Test - public void filterAnswers412ForGenericTransferError() throws IOException, ServletException { + void filterAnswers412ForGenericTransferError() throws IOException, ServletException { Mockito.doThrow(new TransferError("Error")) .when(client) .handle(ArgumentMatchers.any(), ArgumentMatchers.any()); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferStatsTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferStatsTest.java index c0e3a6d0..f602a5e7 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferStatsTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/TransferStatsTest.java @@ -40,7 +40,7 @@ public class TransferStatsTest { TransferStatus.Builder status = TransferStatus.builder(clock); @Test - public void testByteCountPull() { + void testByteCountPull() { GetTransferRequest req = GetTransferRequestBuilder.create().build(); @@ -57,7 +57,7 @@ public void testByteCountPull() { } @Test - public void testOneMsecByteCountPull() { + void testOneMsecByteCountPull() { GetTransferRequest req = GetTransferRequestBuilder.create().build(); req.setTransferStatus(status.inProgress(0)); @@ -72,7 +72,7 @@ public void testOneMsecByteCountPull() { } @Test - public void testHalfMsecByteCountPull() { + void testHalfMsecByteCountPull() { GetTransferRequest req = GetTransferRequestBuilder.create().build(); req.setTransferStatus(status.inProgress(0)); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/ClientTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/ClientTest.java index 4cad306d..efd8b40b 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/ClientTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/ClientTest.java @@ -30,6 +30,7 @@ import org.apache.http.client.ResponseHandler; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.protocol.HttpClientContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -41,12 +42,13 @@ import com.google.common.collect.ArrayListMultimap; @ExtendWith(MockitoExtension.class) -public class ClientTest extends ClientTestSupport { +class ClientTest extends ClientTestSupport { @TempDir public File storage; @SuppressWarnings("unchecked") + @Override @BeforeEach public void setup() throws IOException { @@ -68,13 +70,14 @@ public void setup() throws IOException { } @Test - public void testClientCorrectlyBuildsHttpRequestNoHeaders() throws IOException { + void testClientCorrectlyBuildsHttpRequestNoHeaders() throws IOException { client.handle(req, (r, s) -> { }); verify(httpClient).execute(getRequest.capture(), - ArgumentMatchers.>any()); + ArgumentMatchers.>any(), + ArgumentMatchers.any()); HttpGet httpGetReq = getRequest.getValue(); @@ -84,7 +87,7 @@ public void testClientCorrectlyBuildsHttpRequestNoHeaders() throws IOException { } @Test - public void testClientCorrectlyBuildsHttpRequestWithHeaders() throws IOException { + void testClientCorrectlyBuildsHttpRequestWithHeaders() throws IOException { when(req.transferHeaders()).thenReturn(HEADER_MAP); @@ -92,7 +95,8 @@ public void testClientCorrectlyBuildsHttpRequestWithHeaders() throws IOException }); verify(httpClient).execute(getRequest.capture(), - ArgumentMatchers.>any()); + ArgumentMatchers.>any(), + ArgumentMatchers.any()); HttpGet httpGetReq = getRequest.getValue(); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/DigestTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/DigestTest.java index 5007bf71..5ace7612 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/DigestTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/DigestTest.java @@ -37,7 +37,7 @@ public class DigestTest { @Mock Header header; - public static String[] INVALID_HEADERS = + public static final String[] INVALID_HEADERS = {"", "adler54=1233456", "adler32=8a23d4f889", "sha256:437648", null}; public static final String[] VALID_HEADERS = @@ -51,7 +51,7 @@ protected void instrumentResponse(String headerValue) { } @Test - public void testInvalidHeader() { + void testInvalidHeader() { for (String s : INVALID_HEADERS) { instrumentResponse(s); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/GetResponseHandlerTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/GetResponseHandlerTest.java index 257b060c..498be64d 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/GetResponseHandlerTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/GetResponseHandlerTest.java @@ -36,36 +36,37 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -public class GetResponseHandlerTest extends ClientTestSupport { +class GetResponseHandlerTest extends ClientTestSupport { @Mock StatusLine status; - + @Mock HttpEntity entity; - + @Mock HttpResponse response; - + @Mock StormCountingOutputStream os; - + GetResponseHandler handler; + @Override @BeforeEach public void setup() { - + handler = new GetResponseHandler(null, os, eah); lenient().when(response.getStatusLine()).thenReturn(status); lenient().when(response.getEntity()).thenReturn(entity); } - + @Test - public void handlerWritesToStream() throws IOException { + void handlerWritesToStream() throws IOException { when(status.getStatusCode()).thenReturn(200); - + handler.handleResponse(response); - + verify(entity).getContent(); verify(eah).setChecksumAttribute(ArgumentMatchers.any(), any()); } diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/integration/TpcClientRedirectionTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/integration/TpcClientRedirectionTest.java index 73cebca4..33e51df0 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/integration/TpcClientRedirectionTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/integration/TpcClientRedirectionTest.java @@ -21,13 +21,7 @@ import static org.mockserver.model.NottableString.not; import static org.mockserver.verify.VerificationTimes.exactly; -import java.io.IOException; import java.net.URI; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.cert.CertificateException; import java.util.UUID; import javax.net.ssl.SSLContext; @@ -93,8 +87,7 @@ public static class TestConfig { @Bean("tpcConnectionManager") @Primary public HttpClientConnectionManager tpcClientConnectionManager(ThirdPartyCopyProperties props, - ServiceConfiguration conf) throws KeyStoreException, CertificateException, IOException, - NoSuchAlgorithmException, NoSuchProviderException, KeyManagementException { + ServiceConfiguration conf) { SSLContext ctx = KeyStoreFactory.keyStoreFactory().sslContext(); ConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory(); @@ -144,14 +137,13 @@ private String mockHttpUrl(String path) { } @Test - public void handleCrossProtocolRedirectionCorrectly() { + void handleCrossProtocolRedirectionCorrectly() { Multimap headers = ArrayListMultimap.create(); headers.put("Authorization", "Bearer this-is-a-fake-token"); - GetTransferRequest getRequest = - new GetTransferRequestImpl(UUID.randomUUID().toString(), - "/test/example", URI.create(mockHttpsUrl("/test/example")), headers, false, false); + GetTransferRequest getRequest = new GetTransferRequestImpl(UUID.randomUUID().toString(), + "/test/example", URI.create(mockHttpsUrl("/test/example")), headers, null, false, false); mockServer .when(request().withMethod("GET").withPath("/test/example").withSecure(true), diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/integration/TpcIntegrationTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/integration/TpcIntegrationTest.java index 27b3d7e3..9c8d8e86 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/integration/TpcIntegrationTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/http/integration/TpcIntegrationTest.java @@ -23,11 +23,12 @@ import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.NottableString.not; import static org.mockserver.verify.VerificationTimes.exactly; -import static org.springframework.util.SocketUtils.findAvailableTcpPort; +import static org.springframework.test.util.TestSocketUtils.findAvailableTcpPort; import java.net.URI; import java.util.UUID; +import org.italiangrid.storm.webdav.scitag.SciTag; import org.italiangrid.storm.webdav.tpc.http.HttpTransferClient; import org.italiangrid.storm.webdav.tpc.transfer.GetTransferRequest; import org.italiangrid.storm.webdav.tpc.transfer.PutTransferRequest; @@ -69,7 +70,7 @@ public class TpcIntegrationTest { @BeforeAll public static void startMockServer() { - port = findAvailableTcpPort(15000); + port = findAvailableTcpPort(); mockServer = startClientAndServer(port); } @@ -88,11 +89,12 @@ private String mockUrl(String path) { } @Test - public void testPutRedirectHandled() { + void testPutRedirectHandled() { Multimap emptyHeaders = ArrayListMultimap.create(); PutTransferRequest putRequest = new PutTransferRequestImpl(UUID.randomUUID().toString(), - "/test/example", URI.create(mockUrl("/test/example")), emptyHeaders, false, true); + "/test/example", URI.create(mockUrl("/test/example")), emptyHeaders, new SciTag(1, 2, true), + false, true); mockServer.when(request().withMethod("PUT").withPath("/test/example"), Times.exactly(1)) .respond(HttpResponse.response() @@ -116,12 +118,12 @@ public void testPutRedirectHandled() { } @Test - public void testAuthorizationHeaderIsDroppedOnRedirectForPut() { + void testAuthorizationHeaderIsDroppedOnRedirectForPut() { Multimap headers = ArrayListMultimap.create(); headers.put("Authorization", "Bearer this-is-a-fake-token"); PutTransferRequest putRequest = new PutTransferRequestImpl(UUID.randomUUID().toString(), - "/test/example", URI.create(mockUrl("/test/example")), headers, false, true); + "/test/example", URI.create(mockUrl("/test/example")), headers, null, false, true); mockServer.when(request().withMethod("PUT").withPath("/test/example"), Times.exactly(1)) .respond(HttpResponse.response() @@ -147,13 +149,13 @@ public void testAuthorizationHeaderIsDroppedOnRedirectForPut() { } @Test - public void testAuthorizationHeaderIsDroppedOnRedirectForGet() { + void testAuthorizationHeaderIsDroppedOnRedirectForGet() { Multimap headers = ArrayListMultimap.create(); headers.put("Authorization", "Bearer this-is-a-fake-token"); GetTransferRequest getRequest = new GetTransferRequestImpl(UUID.randomUUID().toString(), - "/test/example", URI.create(mockUrl("/test/example")), headers, false, false); + "/test/example", URI.create(mockUrl("/test/example")), headers, null, false, false); mockServer.when(request().withMethod("GET").withPath("/test/example"), Times.exactly(1)) diff --git a/src/test/java/org/italiangrid/storm/webdav/test/tpc/urlservice/URLServiceTest.java b/src/test/java/org/italiangrid/storm/webdav/test/tpc/urlservice/URLServiceTest.java index 1e370480..5b73f499 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/tpc/urlservice/URLServiceTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/tpc/urlservice/URLServiceTest.java @@ -29,13 +29,13 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -public class URLServiceTest { +class URLServiceTest { public static final String[] SERVICE_ALIASES = {"storm.example", "alias.storm.example", "localhost"}; @Test - public void testEmptyList() { + void testEmptyList() { assertThrows(IllegalArgumentException.class, () -> { new StaticHostListLocalURLService(Collections.emptyList()); @@ -43,7 +43,7 @@ public void testEmptyList() { } @Test - public void testNullList() { + void testNullList() { assertThrows(NullPointerException.class, () -> { new StaticHostListLocalURLService(null); @@ -51,7 +51,7 @@ public void testNullList() { } @Test - public void testResolution() { + void testResolution() { StaticHostListLocalURLService service = new StaticHostListLocalURLService(Arrays.asList(SERVICE_ALIASES)); @@ -66,7 +66,7 @@ public void testResolution() { } @Test - public void testInvalidUrlResolution() { + void testInvalidUrlResolution() { StaticHostListLocalURLService service = new StaticHostListLocalURLService(Arrays.asList(SERVICE_ALIASES)); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/utils/IOUtilsTest.java b/src/test/java/org/italiangrid/storm/webdav/test/utils/IOUtilsTest.java index 6c10477b..ec27df7e 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/utils/IOUtilsTest.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/utils/IOUtilsTest.java @@ -30,7 +30,7 @@ import org.junit.runners.JUnit4; @RunWith(JUnit4.class) -public class IOUtilsTest { +class IOUtilsTest { @TempDir public File testFolder; @@ -48,7 +48,7 @@ public File tempFileOfChar(String name, int b, int size) throws IOException { } @Test - public void testAllFileCopy() throws IOException { + void testAllFileCopy() throws IOException { File source = tempFileOfChar("source", 0, 128); File dest = tempFileOfChar("dest", 1, 128); @@ -66,7 +66,7 @@ public void testAllFileCopy() throws IOException { } @Test - public void testLongerSourceFileCopy() throws IOException { + void testLongerSourceFileCopy() throws IOException { File source = tempFileOfChar("source", 0, 200); File dest = tempFileOfChar("dest", 1, 64); @@ -84,7 +84,7 @@ public void testLongerSourceFileCopy() throws IOException { } @Test - public void testShorterSourceFileCopy() throws IOException { + void testShorterSourceFileCopy() throws IOException { File source = tempFileOfChar("source", 0, 16); File dest = tempFileOfChar("dest", 1, 512); @@ -102,7 +102,7 @@ public void testShorterSourceFileCopy() throws IOException { } @Test - public void testPartialWriteCopy() throws IOException { + void testPartialWriteCopy() throws IOException { File source = tempFileOfChar("source", 0, 128); File dest = tempFileOfChar("dest", 1, 512); @@ -120,7 +120,7 @@ public void testPartialWriteCopy() throws IOException { } @Test - public void testMiddleWrite() throws IOException { + void testMiddleWrite() throws IOException { File source = tempFileOfChar("source", 0, 640); File dest = tempFileOfChar("dest", 1, 256); diff --git a/src/test/java/org/italiangrid/storm/webdav/test/utils/voms/VOMSSecurityContextBuilder.java b/src/test/java/org/italiangrid/storm/webdav/test/utils/voms/VOMSSecurityContextBuilder.java index 892ee721..66fc1e19 100644 --- a/src/test/java/org/italiangrid/storm/webdav/test/utils/voms/VOMSSecurityContextBuilder.java +++ b/src/test/java/org/italiangrid/storm/webdav/test/utils/voms/VOMSSecurityContextBuilder.java @@ -15,7 +15,6 @@ */ package org.italiangrid.storm.webdav.test.utils.voms; -import static java.util.Objects.isNull; import static org.mockito.Mockito.when; import java.time.Clock; @@ -64,7 +63,7 @@ public VOMSSecurityContextBuilder subject(String s) { public VOMSSecurityContextBuilder vos(String... vos) { for (String vo : vos) { - if (!isNull(authorities)) { + if (authorities != null) { authorities.add(new VOMSVOAuthority(vo)); } else { @@ -76,7 +75,7 @@ public VOMSSecurityContextBuilder vos(String... vos) { public VOMSSecurityContextBuilder saReadPermissions(String... sas) { for (String sa : sas) { - if (!isNull(authorities)) { + if (authorities != null) { authorities.add(SAPermission.canRead(sa)); } else { authorities = Lists.newArrayList(SAPermission.canRead(sa)); @@ -87,7 +86,7 @@ public VOMSSecurityContextBuilder saReadPermissions(String... sas) { public VOMSSecurityContextBuilder saWritePermissions(String... sas) { for (String sa : sas) { - if (!isNull(authorities)) { + if (authorities != null) { authorities.add(SAPermission.canWrite(sa)); } else { authorities = Lists.newArrayList(SAPermission.canWrite(sa)); diff --git a/src/test/resources/application-authz-test.yml b/src/test/resources/application-authz-test.yml index 3d9ceafd..9d38c178 100644 --- a/src/test/resources/application-authz-test.yml +++ b/src/test/resources/application-authz-test.yml @@ -20,6 +20,9 @@ storm: redirector: enabled: false + + scitag: + enabled: true authz: policies: @@ -52,4 +55,15 @@ storm: params: iss: https://issuer.example group: /example/admins - \ No newline at end of file + - sa: wlcg + actions: + - all + effect: permit + description: Grant read/write access to a specific JWT client ID + paths: + - /** + principals: + - type: jwt-client + params: + iss: https://issuer.example + id: 1234 \ No newline at end of file diff --git a/src/test/resources/application-lhcb.yml b/src/test/resources/application-lhcb.yml new file mode 100644 index 00000000..093247b5 --- /dev/null +++ b/src/test/resources/application-lhcb.yml @@ -0,0 +1,55 @@ +server: + jetty: + accesslog: + enabled: false +oauth: + enable-oidc: false + +storm: + sa: + config-dir: src/test/resources/lhcb/sa.d + + tls: + trust-anchors-dir: src/test/resources/trust-anchors + certificate-path: src/test/resources/hostcert/hostcert.pem + private-key-path: src/test/resources/hostcert/hostkey.pem + + voms: + trust-store: + dir: src/test/resources/vomsdir + + redirector: + enabled: false + + authz: + policies: + - sa: lhcb_disk + description: Grant all access to lhcb VOMS group members for /failover and its subfolders + actions: + - all + paths: + - /failover/** + effect: permit + principals: + - type: vo + params: + vo: lhcb + - sa: lhcb_disk + description: Grant all access to lhcb prod VOMS group members + actions: + - all + effect: permit + principals: + - type: fqan + params: + fqan: /lhcb/Role=production/Capability=NULL + - sa: lhcb_disk + description: Grant only read and list access to lhcb VOMS group members + actions: + - read + - list + effect: permit + principals: + - type: vo + params: + vo: lhcb diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 00000000..d7758789 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,52 @@ +# +# Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +logging.pattern.dateformat="yyyy-MM-dd'T'HH:mm:ss.SSSXXX", UTC +logging.pattern.level= [%X{storm.requestId}] %X{tpc.clientInfo} %5p +logging.level.root=WARN +logging.level.org.italiangrid.storm=DEBUG + + +#logging.level.org.italiangrid.storm.webdav.tpc=DEBUG + +#logging.level.org.springframework.test.web.servlet.result=DEBUG + +#logging.level.org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory=WARN +#logging.level.org.italiangrid.storm.webdav.authz=DEBUG +#logging.level.org.springframework.web=DEBUG +#logging.level.org.italiangrid.storm.webdav.oauth=DEBUG +#logging.level.org.italiangrid.storm.webdav.oidc=DEBUG +#logging.level.org.springframework.security=DEBUG + + +## Voters +#logging.level.org.italiangrid.storm.webdav.authz.voters=DEBUG +#logging.level.org.italiangrid.storm.webdav.authz.pdp=DEBUG +#logging.level.org.springframework.security.web.util.matcher=DEBUG +#logging.level.org.springframework.security.access=DEBUG + +## HTTP Client +### Connection management +# logging.level.org.apache.http.impl.conn=DEBUG +# logging.level.org.apache.http.impl.client=DEBUG +#logging.level.org.apache.http.client=DEBUG +### Wire +#logging.level.org.apache.http=DEBUG +#logging.level.org.apache.http.wire=DEBUG + +#logging.level.org.eclipse.jetty.io=ERROR +#logging.level.org.eclipse.jetty.server=DEBUG +#logging.level.org.eclipse.jetty.server.HttpOutput=ERROR diff --git a/compose/assets/etc/storm/webdav/sa.d/auth.properties b/src/test/resources/conf/sa.d/fga.properties similarity index 72% rename from compose/assets/etc/storm/webdav/sa.d/auth.properties rename to src/test/resources/conf/sa.d/fga.properties index a740ef11..e9f0a67d 100644 --- a/compose/assets/etc/storm/webdav/sa.d/auth.properties +++ b/src/test/resources/conf/sa.d/fga.properties @@ -14,11 +14,14 @@ # limitations under the License. # -name=auth -rootPath=/storage/auth +name=fga +rootPath=src/test/resources/storage/fga filesystemType=posixfs -accessPoints=/auth -vos=test.vo -authenticatedReadEnabled=true +accessPoints=/fga +authenticatedReadEnabled=false anonymousReadEnabled=false -voMapGrantsWritePermission=false +orgs=https://iam-dev.cloud.cnaf.infn.it/ +orgsGrantReadPermission=false +orgsGrantWritePermission=false +fineGrainedAuthzEnabled=true +wlcgScopeAuthzEnabled=true diff --git a/src/test/resources/jwk/test-keystore.jwks b/src/test/resources/jwk/test-keystore.jwks new file mode 100644 index 00000000..cde1441e --- /dev/null +++ b/src/test/resources/jwk/test-keystore.jwks @@ -0,0 +1,16 @@ +{ + "keys": [ + { + "p": "-qdvzeHU7w_ToV2RlS2QlVggXNL2YfpRWQxvrO8pHZC_dVgYFwKz5nadOMzR1BK0tPuCTWuuI66sFgaA9VENGypdIYoCF2O1FBLFK6GjOO-uc0LZEbIDa6Xn0G7UYOWcLaiYriHTtC_Pzp11L7VGjrUlX4HRgU_B3X1oeGn0mbM", + "kty": "RSA", + "q": "5S2b9tYHi9zBcNGZ8X6GM4TAL4UU9mABH0rKIyzbudkG7Wxxbj6I18skuHzfOOPI4c8sTQSv6IVAr2n1bn3_E5RSyPpbtDSCTYGzhijXl9wZ0ba2NidFrVjnL-KPx_gcHKnUHebKvsIEdjxeuqaaZ1kqEJX326b450Frghd78p8", + "d": "oDb1qfQTaP73jEZHgkOG5C9dY5EJZW57fX_BYUJ-yYTuTHPWZBVDKw9I_Ir1tSYyuTF-Bfb4iPim46gnEBM3AdvMian2iajvrN_rJFUJHo65vtY9xCXCD0d_Jct5JMyOafP5LF3cP38yDcyZRS_JeyKGB6U1KhbL-gG4hrQGS8qO3rdY_JQiLDLVdRRptHsPphS44JHXdP2qeVNJ41-CTfPWKiMIUOC0fj-As-dbTzRXuLXs04NayAdM-yhvRiwKujEfL8YbKW9CDJIgJfm2vzWHXFus5Y11S2Zr65cWxxVvRfnAFbFO1AkIkJc2jHZ4xLxfDU2kTi20sOMq1UFrvQ", + "e": "AQAB", + "kid": "rsa1", + "qi": "bN1wnq_0VkJlECMGPmeRFdZCX2LgAMrgwbJpysRw5J04vO9YsVmAcB_4xqoDjDUg7koioAp3IOhMGjOJWpYzCqWzsaA_84kX4WKGsr2xz6oaFSgt1FvtBY4GqEeZj8RG0LMtEtKSyjHGieA0hd4TUcqSdNZ4osT98Bfd7z3peYc", + "dp": "YfyaxI2IRHyXavm9M-hAIWH2JNOD5gGJU5p8_cnw9NHlRuZNZJF16p5sEAxh6tn1MtsvsTxrMx_RvjqEp2IsEXaaOcZN0v7zhwlfcxMZT-TC-eQkH7rLg4Wz_dOVytt4FpFWPpySuloGjusXKLNhBeDi31dMo5SeYQvpj0k8iek", + "dq": "9_lhyLPNdohmxqwE5kkA7L23NbPJ-svmavWBwo3HMlCiLkQoeCEx8EzebsCux9-wfKSuSqfHrtCALU15QxUR6x2SdeRvVY17cGHm3kNTA_4j8cbBYdccjXSksitzZ-wOfvVDjxcqST2llkm8NjoO18Siv0-F4SXKLG-c5CaE9w", + "n": "4GRvJuFantVV3JdjwQOAkfREnwUFp2znRBTOIJhPamyH4gf4YlI5PQT79415NV4_HrWYzgooH5AK6-7WE-TLLGEAVK5vdk4vv79bG7ukvjvBPxAjEhQn6-Amln88iXtvicEGbh--3CKbQj1jryVU5aWM6jzweaabFSeCILVEd6ZT7ofXaAqan9eLzU5IEtTPy5MfrrOvWw5Q7D2yzMqc5LksmaQSw8XtmhA8gnENnIqjAMmPtRltf93wjtmiamgVENOVPdN-93Nd5w-pnMwEyoO6Q9JqXxV6lD6qBRxI7_5t4_vmVxcbbxcZbSAMoHqA2pbSMJ4Jcw-27Hct9jesLQ" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/lhcb/sa.d/disk.properties b/src/test/resources/lhcb/sa.d/disk.properties new file mode 100644 index 00000000..d7aba683 --- /dev/null +++ b/src/test/resources/lhcb/sa.d/disk.properties @@ -0,0 +1,12 @@ +name=lhcb_disk +rootPath=src/test/resources/lhcb/storage/lhcb_disk +filesystemType=posixfs +accessPoints=/disk/lhcb +vos= +orgs=https://lhcb-auth.web.cern.ch/ +anonymousReadEnabled=false +authenticatedReadEnabled=false +orgsGrantReadPermissions=false +orgsGrantWritePermissions=false +wlcgScopeAuthzEnabled=true +fineGrainedAuthzEnabled=true diff --git a/src/test/resources/lhcb/storage/lhcb_disk/source b/src/test/resources/lhcb/storage/lhcb_disk/source new file mode 100644 index 00000000..e69de29b diff --git a/src/test/resources/storage/fga/file b/src/test/resources/storage/fga/file new file mode 100644 index 00000000..af218bce --- /dev/null +++ b/src/test/resources/storage/fga/file @@ -0,0 +1 @@ +Testing the fine-grained authorization diff --git a/src/test/resources/vomsmap/testers.map b/src/test/resources/vomsmap/testers.map index 9eaa5fed..b5f13a28 100644 --- a/src/test/resources/vomsmap/testers.map +++ b/src/test/resources/vomsmap/testers.map @@ -1,2 +1,4 @@ /C=IT/O=INFN/OU=Personal Certificate/L=CNAF/CN=Andrea Ceccanti,/C=IT/O=INFN/CN=INFN CA,andrea.ceccanti@cnaf.infn.it -/C=IT/O=INFN/OU=Personal Certificate/L=CNAF/CN=Enrico Vianello,/C=IT/O=INFN/CN=INFN CA,enrico.vianello@cnaf.infn.it \ No newline at end of file +C=IT/O=INFN/OU=Personal Certificate/L=CNAF/CN=Enrico Vianello,/C=IT/O=INFN/CN=INFN CA,enrico.vianello@cnaf.infn.it +/C=IT/O=INFN/OU=Personal Certificate/L=CNAF,Bologna/CN=Federica Agostini,/C=IT/O=INFN/CN=INFN CA,federica.agostini@cnaf.infn.it +/C=IT/O=INFN/OU=Personal Certificate/L=CNAF/CN=Roberta Miccoli,/C=IT/O=INFN/CN=INFN CA,roberta.miccoli@cnaf.infn.it \ No newline at end of file diff --git a/travis/build.sh b/travis/build.sh deleted file mode 100644 index f51cf154..00000000 --- a/travis/build.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# -# Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -set -e - -mvn -B clean compile -echo "storm-webdav build completed succesfully" - -mvn -B clean test -echo "storm-webdav tests completed succesfully" diff --git a/travis/install-deps.sh b/travis/install-deps.sh deleted file mode 100644 index 68846397..00000000 --- a/travis/install-deps.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# -# Copyright (c) Istituto Nazionale di Fisica Nucleare, 2014-2023. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -set -ex - -pwd - -sudo apt-get install -y wget -wget https://raw.githubusercontent.com/italiangrid/build-settings/master/maven/cnaf-mirror-settings.xml - -mv cnaf-mirror-settings.xml ~/.m2/settings.xml