diff --git a/.github/workflows/at_server.yaml b/.github/workflows/at_server.yaml index 539d3b454..8b57de360 100644 --- a/.github/workflows/at_server.yaml +++ b/.github/workflows/at_server.yaml @@ -171,7 +171,7 @@ jobs: ls -laR tools/build_virtual_environment/ve/* - name: Build docker image - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: file: tools/build_virtual_environment/ve/Dockerfile context: tools/build_virtual_environment/ve @@ -209,7 +209,7 @@ jobs: # On push event, upload secondary server binary - name: upload secondary server if: ${{ github.event_name == 'push' }} - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: secondary-server path: packages/at_secondary_server/secondary @@ -238,10 +238,10 @@ jobs: grep version pubspec.yaml | head -1 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -249,7 +249,7 @@ jobs: # Builds and pushes the secondary server image to docker hub. - name: Build and push secondary image for x64 id: docker_build_secondary - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: push: true provenance: false @@ -594,13 +594,13 @@ jobs: run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -608,7 +608,7 @@ jobs: # Builds and pushes the at_virtual_env to docker hub. - name: Build and push id: docker_build - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: file: tools/build_virtual_environment/ve/Dockerfile.vip context: . @@ -648,13 +648,13 @@ jobs: run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -662,7 +662,7 @@ jobs: # Builds and pushes the secondary server image to docker hub. - name: Build and push secondary image for amd64 and arm64 id: docker_build_secondary - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: push: true provenance: false @@ -703,13 +703,13 @@ jobs: run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -717,7 +717,7 @@ jobs: # Builds and pushes the secondary server image to docker hub. - name: Build and push secondary image for amd64 and arm64 id: docker_build_observable_secondary - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: push: true provenance: false @@ -756,13 +756,13 @@ jobs: grep version pubspec.yaml | head -1 - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -770,7 +770,7 @@ jobs: # Builds and pushes the secondary server image to docker hub. - name: Build and push secondary image for amd64 and arm64 id: docker_build_secondary - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: push: true provenance: false @@ -801,20 +801,20 @@ jobs: grep version pubspec.yaml | head -1 - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: file: tools/build_virtual_environment/ve/Dockerfile.vip context: . @@ -847,13 +847,13 @@ jobs: run: echo "VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -861,7 +861,7 @@ jobs: # Builds and pushes the secondary server image to docker hub. - name: Build and push secondary image for amd64 and arm64 id: docker_build_secondary - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: push: true provenance: false @@ -882,20 +882,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: file: tools/build_virtual_environment/ve/Dockerfile.vip context: . diff --git a/.github/workflows/at_server_dev_deploy.yaml b/.github/workflows/at_server_dev_deploy.yaml index ebe6b18bb..75664ab02 100644 --- a/.github/workflows/at_server_dev_deploy.yaml +++ b/.github/workflows/at_server_dev_deploy.yaml @@ -21,17 +21,17 @@ jobs: run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} # Build the Docker image for Dev - name: Build and push - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: file: packages/at_root_server/Dockerfile context: packages/at_root_server diff --git a/.github/workflows/at_server_prod_deploy.yaml b/.github/workflows/at_server_prod_deploy.yaml index 4633a5e3b..83ec6ad26 100644 --- a/.github/workflows/at_server_prod_deploy.yaml +++ b/.github/workflows/at_server_prod_deploy.yaml @@ -21,17 +21,17 @@ jobs: run: echo "BRANCH=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} # Build the Docker image for Dev - name: Build and push - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: file: packages/at_root_server/Dockerfile context: packages/at_root_server diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index d06526571..ee8c1c249 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -24,4 +24,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@f6fff72a3217f580d5afd49a46826795305b63c7 # v3.0.8 + uses: actions/dependency-review-action@6c5ccdad469c9f8a2996bfecaec55a631a347034 # v3.1.0 diff --git a/.github/workflows/promote_canary.yaml b/.github/workflows/promote_canary.yaml index 0c1e87c9d..46d74ec1c 100644 --- a/.github/workflows/promote_canary.yaml +++ b/.github/workflows/promote_canary.yaml @@ -22,13 +22,13 @@ jobs: run: echo "VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -36,7 +36,7 @@ jobs: # Builds and pushes the secondary server image to docker hub. - name: Build and push secondary image for amd64 and arm64 id: docker_build_canary_to_prod - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: push: true provenance: false @@ -63,13 +63,13 @@ jobs: run: echo "VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -77,7 +77,7 @@ jobs: # Builds and pushes the secondary server image to docker hub. - name: Build and push virtualenv image for amd64 and arm64 id: docker_build_canary_to_vip - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: push: true file: tools/build_virtual_environment/ve/Dockerfile.canary_to_vip diff --git a/.github/workflows/revert_secondary.yaml b/.github/workflows/revert_secondary.yaml index 0b4e2d088..b08d90f0d 100644 --- a/.github/workflows/revert_secondary.yaml +++ b/.github/workflows/revert_secondary.yaml @@ -29,7 +29,7 @@ jobs: run: echo "VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -72,7 +72,7 @@ jobs: run: echo "VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 1b859e7a7..b986c0e9a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/ve_base.yaml b/.github/workflows/ve_base.yaml index 284ee0184..950be157f 100644 --- a/.github/workflows/ve_base.yaml +++ b/.github/workflows/ve_base.yaml @@ -12,20 +12,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 - name: Login to DockerHub - uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: docker_build - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: file: tools/build_virtual_environment/ve_base/Dockerfile push: true diff --git a/packages/at_root_server/Dockerfile b/packages/at_root_server/Dockerfile index 4ccc08daa..eaf0996e7 100644 --- a/packages/at_root_server/Dockerfile +++ b/packages/at_root_server/Dockerfile @@ -1,4 +1,4 @@ -FROM dart:3.1.0@sha256:96d2e5d03b8356c2a7542716ace7dce745971efe1d03888a1d7ecd2e7c1dde36 AS buildimage +FROM dart:3.1.1@sha256:ec7bb9e577648ea5526c9daf714e9bc7af670ce7c93b594205e68c14a10cea3b AS buildimage ENV HOMEDIR=/atsign ENV BINARYDIR=/usr/local/at ENV USER_ID=1024 diff --git a/packages/at_secondary_server/CHANGELOG.md b/packages/at_secondary_server/CHANGELOG.md index 3eafd44bf..ff53a0fa9 100644 --- a/packages/at_secondary_server/CHANGELOG.md +++ b/packages/at_secondary_server/CHANGELOG.md @@ -1,3 +1,5 @@ +## 3.0.36 +- fix: Implement notify ephemeral changes - Send notification with value without caching the key on receiver's secondary server ## 3.0.35 - chore: Upgraded at_persistence_secondary_server to 3.0.57 for memory optimization in commit log - feat: APKAM keys verb implementation diff --git a/packages/at_secondary_server/config/config.yaml b/packages/at_secondary_server/config/config.yaml index fabcabd2e..4b27f99e4 100644 --- a/packages/at_secondary_server/config/config.yaml +++ b/packages/at_secondary_server/config/config.yaml @@ -149,3 +149,13 @@ sync: #IMPORTANT NOTE : please set testingMode to true only if you know what you're doing. Set to false when not testing testing: testingMode: false + +# APKAM enrollment configurations +enrollment: + # The maximum time in hours for an enrollment to expire, beyond which any action on enrollment is forbidden. + # Default values is 48 hours. + expiryInHours: 48 + # The maximum number of requests allowed within the time window. + maxRequestsPerTimeFrame: 5 + # The duration of the time window in hours. + timeFrameInHours: 1 \ No newline at end of file diff --git a/packages/at_secondary_server/lib/src/connection/inbound/dummy_inbound_connection.dart b/packages/at_secondary_server/lib/src/connection/inbound/dummy_inbound_connection.dart index 8b06dc40a..f7b6f748b 100644 --- a/packages/at_secondary_server/lib/src/connection/inbound/dummy_inbound_connection.dart +++ b/packages/at_secondary_server/lib/src/connection/inbound/dummy_inbound_connection.dart @@ -1,12 +1,19 @@ import 'dart:io'; import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart'; +import 'package:at_secondary/src/server/at_secondary_config.dart'; import 'package:at_server_spec/at_server_spec.dart'; /// A dummy implementation of [InboundConnection] class which returns a dummy inbound connection. class DummyInboundConnection implements InboundConnection { var metadata = InboundConnectionMetadata(); + @override + int maxRequestsPerTimeFrame = AtSecondaryConfig.maxEnrollRequestsAllowed; + + @override + int timeFrameInMillis = AtSecondaryConfig.timeFrameInMills; + @override void acceptRequests(Function(String p1, InboundConnection p2) callback, Function(List<int>, InboundConnection) streamCallback) {} @@ -54,4 +61,9 @@ class DummyInboundConnection implements InboundConnection { @override Socket? receiverSocket; + + @override + bool isRequestAllowed() { + return true; + } } diff --git a/packages/at_secondary_server/lib/src/connection/inbound/inbound_connection_impl.dart b/packages/at_secondary_server/lib/src/connection/inbound/inbound_connection_impl.dart index a19bf6b82..dafbae817 100644 --- a/packages/at_secondary_server/lib/src/connection/inbound/inbound_connection_impl.dart +++ b/packages/at_secondary_server/lib/src/connection/inbound/inbound_connection_impl.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:io'; import 'dart:math'; @@ -5,6 +6,7 @@ import 'package:at_secondary/src/connection/base_connection.dart'; import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart'; import 'package:at_secondary/src/connection/inbound/inbound_connection_pool.dart'; import 'package:at_secondary/src/connection/inbound/inbound_message_listener.dart'; +import 'package:at_secondary/src/server/at_secondary_config.dart'; import 'package:at_secondary/src/server/server_context.dart'; import 'package:at_secondary/src/server/at_secondary_impl.dart'; import 'package:at_secondary/src/utils/logging_util.dart'; @@ -42,6 +44,17 @@ class InboundConnectionImpl extends BaseConnection late double lowWaterMarkRatio; late bool progressivelyReduceAllowableInboundIdleTime; + /// The maximum number of requests allowed within the specified time frame. + @override + late int maxRequestsPerTimeFrame; + + /// The duration of the time frame within which requests are limited. + @override + late int timeFrameInMillis; + + /// A list of timestamps representing the times when requests were made. + late final Queue<int> requestTimestampQueue; + InboundConnectionImpl(Socket? socket, String? sessionId, {this.owningPool}) : super(socket) { metaData = InboundConnectionMetadata() @@ -69,6 +82,10 @@ class InboundConnectionImpl extends BaseConnection secondaryContext.authenticatedInboundIdleTimeMillis; authenticatedMinAllowableIdleTimeMillis = secondaryContext.authenticatedMinAllowableIdleTimeMillis; + + maxRequestsPerTimeFrame = AtSecondaryConfig.maxEnrollRequestsAllowed; + timeFrameInMillis = AtSecondaryConfig.timeFrameInMills; + requestTimestampQueue = Queue(); } /// Returns true if the underlying socket is not null and socket's remote address and port match. @@ -230,4 +247,31 @@ class InboundConnectionImpl extends BaseConnection metaData, 'SENT: ${BaseConnection.truncateForLogging(data)}')); } } + + @override + bool isRequestAllowed() { + int currentTimeInMills = DateTime.now().toUtc().millisecondsSinceEpoch; + _checkAndUpdateQueue(currentTimeInMills); + if (requestTimestampQueue.length < maxRequestsPerTimeFrame) { + requestTimestampQueue.addLast(currentTimeInMills); + return true; + } + return false; + } + + /// Checks and updates the request timestamp queue based on the current time. + /// + /// This method removes timestamps from the queue that are older than the specified + /// time window. + /// + /// [currentTimeInMillis] is the current time in milliseconds since epoch. + void _checkAndUpdateQueue(int currentTimeInMillis) { + if (requestTimestampQueue.isEmpty) return; + int calculatedTime = (currentTimeInMillis - requestTimestampQueue.first); + while (calculatedTime >= timeFrameInMillis) { + requestTimestampQueue.removeFirst(); + if (requestTimestampQueue.isEmpty) break; + calculatedTime = (currentTimeInMillis - requestTimestampQueue.first); + } + } } diff --git a/packages/at_secondary_server/lib/src/exception/global_exception_handler.dart b/packages/at_secondary_server/lib/src/exception/global_exception_handler.dart index ecfe7ec17..c3faead04 100644 --- a/packages/at_secondary_server/lib/src/exception/global_exception_handler.dart +++ b/packages/at_secondary_server/lib/src/exception/global_exception_handler.dart @@ -68,7 +68,8 @@ class GlobalExceptionHandler { exception is KeyNotFoundException || exception is AtConnectException || exception is SocketException || - exception is AtTimeoutException) { + exception is AtTimeoutException || + exception is AtThrottleLimitExceeded) { logger.info(exception.toString()); await _sendResponseForException(exception, atConnection); } else if (exception is InternalServerError) { diff --git a/packages/at_secondary_server/lib/src/notification/resource_manager.dart b/packages/at_secondary_server/lib/src/notification/resource_manager.dart index 245a7ab2d..277852da3 100644 --- a/packages/at_secondary_server/lib/src/notification/resource_manager.dart +++ b/packages/at_secondary_server/lib/src/notification/resource_manager.dart @@ -35,6 +35,7 @@ class ResourceManager { NotifyConnectionsPool.getInstance(); int get outboundConnectionLimit => _notifyConnectionsPool.size; + set outboundConnectionLimit(int ocl) => _notifyConnectionsPool.size = ocl; void start() { @@ -201,6 +202,9 @@ class ResourceManager { // looked at this code. String commandBody; commandBody = '${atNotification.notification}'; + if (atNotification.atValue != null) { + commandBody = '$commandBody:${atNotification.atValue}'; + } var atMetaData = atNotification.atMetadata; if (atMetaData != null) { if (atNotification.atMetadata!.skeEncAlgo != null) { @@ -237,7 +241,7 @@ class ResourceManager { } if (atMetaData.ttr != null) { commandBody = - 'ttr:${atMetaData.ttr}:ccd:${atMetaData.isCascade}:$commandBody:${atNotification.atValue}'; + 'ttr:${atMetaData.ttr}:ccd:${atMetaData.isCascade}:$commandBody'; } if (atMetaData.ttb != null) { commandBody = 'ttb:${atMetaData.ttb}:$commandBody'; diff --git a/packages/at_secondary_server/lib/src/server/at_secondary_config.dart b/packages/at_secondary_server/lib/src/server/at_secondary_config.dart index 65a4f9ec6..e3844660a 100644 --- a/packages/at_secondary_server/lib/src/server/at_secondary_config.dart +++ b/packages/at_secondary_server/lib/src/server/at_secondary_config.dart @@ -44,17 +44,22 @@ class AtSecondaryConfig { //Notification static const bool _autoNotify = true; + // The maximum number of retries for a notification. static const int _maxNotificationRetries = 30; + // The quarantine duration of an atsign. Notifications will be retried max_retries times, every quarantineDuration seconds approximately. static const int _notificationQuarantineDuration = 10; + // The notifications queue will be processed every jobFrequency seconds. However, the notifications queue will always be processed // *immediately* when a new notification is queued. When that happens, the queue processing will not run again until jobFrequency // seconds have passed since the last queue-processing run completed. static const int _notificationJobFrequency = 11; + // The time interval(in seconds) to notify latest commitID to monitor connections // To disable to the feature, set to -1. static const int _statsNotificationJobTimeInterval = 15; + // defines the time after which a notification expires in units of minutes. Notifications expire after 1440 minutes or 24 hours by default. static const int _notificationExpiresAfterMins = 1440; @@ -120,6 +125,20 @@ class AtSecondaryConfig { static String? get secondaryServerVersion => _secondaryServerVersion; + // Enrollment Configurations + static const int _enrollmentExpiryInHours = 48; + static int _maxEnrollRequestsAllowed = 5; + + static final int _timeFrameInHours = 1; + + // For easy of testing, duration in hours is long. Hence introduced "timeFrameInMills" + // to have a shorter time frame. This is defaulted to "_timeFrameInHours", can be modified + // via the config verb + static int _timeFrameInMills = + Duration(hours: _timeFrameInHours).inMilliseconds; + + static int get enrollmentExpiryInHours => _enrollmentExpiryInHours; + // TODO: Medium priority: Most (all?) getters in this class return a default value but the signatures currently // allow for nulls. Should fix this as has been done for logLevel // TODO: Low priority: Lots of very similar boilerplate code here. Not necessarily bad in this particular case, but @@ -707,6 +726,54 @@ class AtSecondaryConfig { } } + static int get maxEnrollRequestsAllowed { + // For easy of testing purpose, we need to reduce the number of requests. + // So, in testing mode, enable to modify the "maxEnrollRequestsAllowed" + // can be set via the config verb + // Defaults to value in config.yaml + if (testingMode) { + return _maxEnrollRequestsAllowed; + } + var result = _getIntEnvVar('maxEnrollRequestsAllowed'); + if (result != null) { + return result; + } + try { + return getConfigFromYaml(['enrollment', 'maxRequestsPerTimeFrame']); + } on ElementNotFoundException { + return _maxEnrollRequestsAllowed; + } + } + + static set maxEnrollRequestsAllowed(int value) { + _maxEnrollRequestsAllowed = value; + } + + static int get timeFrameInMills { + // For easy of testing purpose, we need to reduce the time frame. + // So, in testing mode, enable to modify the "timeFrameInMills" + // can be set via the config verb + // Defaults to value in config.yaml + if (testingMode) { + return _timeFrameInMills; + } + var result = _getIntEnvVar('enrollTimeFrameInHours'); + if (result != null) { + return Duration(hours: result).inMilliseconds; + } + try { + return Duration( + hours: getConfigFromYaml(['enrollment', 'timeFrameInHours'])) + .inMilliseconds; + } on ElementNotFoundException { + return Duration(hours: _timeFrameInHours).inMilliseconds; + } + } + + static set timeFrameInMills(int timeWindowInMills) { + _timeFrameInMills = timeWindowInMills; + } + //implementation for config:set. This method returns a data stream which subscribers listen to for updates static Stream<dynamic>? subscribe(ModifiableConfigs configName) { if (testingMode) { @@ -777,6 +844,10 @@ class AtSecondaryConfig { return false; case ModifiableConfigs.doCacheRefreshNow: return false; + case ModifiableConfigs.maxRequestsPerTimeFrame: + return maxEnrollRequestsAllowed; + case ModifiableConfigs.timeFrameInMills: + return Duration(hours: _timeFrameInHours).inMilliseconds; } } @@ -857,7 +928,9 @@ enum ModifiableConfigs { maxNotificationRetries, checkCertificateReload, shouldReloadCertificates, - doCacheRefreshNow + doCacheRefreshNow, + maxRequestsPerTimeFrame, + timeFrameInMills } class ModifiableConfigurationEntry { diff --git a/packages/at_secondary_server/lib/src/server/at_secondary_impl.dart b/packages/at_secondary_server/lib/src/server/at_secondary_impl.dart index 932d65a54..bc20ae6e3 100644 --- a/packages/at_secondary_server/lib/src/server/at_secondary_impl.dart +++ b/packages/at_secondary_server/lib/src/server/at_secondary_impl.dart @@ -421,6 +421,14 @@ class AtSecondaryServerImpl implements AtSecondaryServer { notificationResourceManager.setMaxRetries(newCount); QueueManager.getInstance().setMaxRetries(newCount); }); + + AtSecondaryConfig.subscribe(ModifiableConfigs.maxRequestsPerTimeFrame)?.listen((maxEnrollRequestsAllowed) { + AtSecondaryConfig.maxEnrollRequestsAllowed = maxEnrollRequestsAllowed; + }); + + AtSecondaryConfig.subscribe(ModifiableConfigs.timeFrameInMills)?.listen((timeWindowInMills) { + AtSecondaryConfig.timeFrameInMills = timeWindowInMills; + }); } } diff --git a/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart index 85f87a2ee..5e0c52b50 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/abstract_verb_handler.dart @@ -11,6 +11,7 @@ import 'package:at_secondary/src/verb/manager/response_handler_manager.dart'; import 'package:at_server_spec/at_server_spec.dart'; import 'package:at_server_spec/at_verb_spec.dart'; import 'package:at_utils/at_logger.dart'; +import 'package:at_secondary/src/utils/secondary_util.dart'; final String paramFullCommandAsReceived = 'FullCommandAsReceived'; @@ -89,6 +90,10 @@ abstract class AbstractVerbHandler implements VerbHandler { AtData enrollData = await keyStore.get(enrollmentKey); EnrollDataStoreValue enrollDataStoreValue = EnrollDataStoreValue.fromJson(jsonDecode(enrollData.data!)); + if (!SecondaryUtil.isActiveKey(enrollData) && + enrollDataStoreValue.approval!.state != EnrollStatus.approved.name) { + enrollDataStoreValue.approval?.state = EnrollStatus.expired.name; + } return enrollDataStoreValue; } on KeyNotFoundException { logger.severe('$enrollmentKey does not exist in the keystore'); diff --git a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart index f18a017fb..31b43f930 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/enroll_verb_handler.dart @@ -4,12 +4,14 @@ import 'dart:convert'; import 'package:at_commons/at_commons.dart'; import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart'; import 'package:at_secondary/src/server/at_secondary_impl.dart'; +import 'package:at_secondary/src/server/at_secondary_config.dart'; import 'package:at_secondary/src/constants/enroll_constants.dart'; import 'package:at_secondary/src/enroll/enroll_datastore_value.dart'; import 'package:at_secondary/src/utils/notification_util.dart'; import 'package:at_secondary/src/verb/handler/otp_verb_handler.dart'; import 'package:at_server_spec/at_server_spec.dart'; import 'package:at_server_spec/at_verb_spec.dart'; +import 'package:meta/meta.dart'; import 'package:uuid/uuid.dart'; import 'abstract_verb_handler.dart'; import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart'; @@ -26,6 +28,10 @@ class EnrollVerbHandler extends AbstractVerbHandler { @override Verb getVerb() => enrollVerb; + @visibleForTesting + int enrollmentExpiryInMills = + Duration(hours: AtSecondaryConfig.enrollmentExpiryInHours).inMilliseconds; + @override Future<void> processVerb( Response response, @@ -40,23 +46,31 @@ class EnrollVerbHandler extends AbstractVerbHandler { throw UnAuthenticatedException( 'Cannot $operation enrollment without authentication'); } + EnrollParams? enrollVerbParams; try { - var enrollVerbParams; - if (verbParams[enrollParams] != null) { + // Ensure that enrollParams are present for all enroll operation + // Exclude operation 'list' which does not have enrollParams + if (verbParams[enrollParams] == null) { + if (operation != 'list') { + logger.severe( + 'Enroll params is empty | EnrollParams: ${verbParams[enrollParams]}'); + throw IllegalArgumentException('Enroll parameters not provided'); + } + } else { enrollVerbParams = EnrollParams.fromJson(jsonDecode(verbParams[enrollParams]!)); } switch (operation) { case 'request': await _handleEnrollmentRequest( - enrollVerbParams, currentAtSign, responseJson, atConnection); + enrollVerbParams!, currentAtSign, responseJson, atConnection); break; case 'approve': case 'deny': case 'revoke': - await _handleEnrollmentPermissions( - enrollVerbParams, currentAtSign, operation, responseJson); + await _handleEnrollmentPermissions(enrollVerbParams!, currentAtSign, + operation, responseJson, response); break; case 'list': @@ -65,14 +79,11 @@ class EnrollVerbHandler extends AbstractVerbHandler { return; } } catch (e, stackTrace) { - response.isError = true; - response.errorMessage = e.toString(); - responseJson['status'] = 'exception'; - responseJson['reason'] = e.toString(); logger.severe('Exception: $e\n$stackTrace'); rethrow; } response.data = jsonEncode(responseJson); + return; } /// Enrollment requests details are persisted in the keystore and are excluded from @@ -94,11 +105,17 @@ class EnrollVerbHandler extends AbstractVerbHandler { /// and its corresponding state. /// /// Throws "AtEnrollmentException", if the OTP provided is invalid. + /// Throws [AtThrottleLimitExceeded], if the number of requests exceed within + /// a time window. Future<void> _handleEnrollmentRequest( EnrollParams enrollParams, currentAtSign, Map<dynamic, dynamic> responseJson, InboundConnection atConnection) async { + if (!atConnection.isRequestAllowed()) { + throw AtThrottleLimitExceeded( + 'Enrollment requests have exceeded the limit within the specified time frame'); + } if (!atConnection.getMetaData().isAuthenticated) { var otp = enrollParams.otp; if (otp == null || @@ -119,7 +136,9 @@ class EnrollVerbHandler extends AbstractVerbHandler { enrollParams.appName!, enrollParams.deviceName!, enrollParams.apkamPublicKey!); - + enrollmentValue.namespaces = enrollNamespaces; + enrollmentValue.requestType = EnrollRequestType.newEnrollment; + AtData enrollData; if (atConnection.getMetaData().authType != null && atConnection.getMetaData().authType == AuthType.cram) { // auto approve request from connection that is CRAM authenticated. @@ -137,15 +156,19 @@ class EnrollVerbHandler extends AbstractVerbHandler { // The keys with AT_PKAM_PUBLIC_KEY does not sync to client. await keyStore.put( AT_PKAM_PUBLIC_KEY, AtData()..data = enrollParams.apkamPublicKey!); + enrollData = AtData()..data = jsonEncode(enrollmentValue.toJson()); } else { enrollmentValue.approval = EnrollApproval(EnrollStatus.pending.name); await _storeNotification(key, enrollParams, currentAtSign); responseJson['status'] = 'pending'; + enrollData = AtData() + ..data = jsonEncode(enrollmentValue.toJson()) + // Set TTL to the pending enrollments. + // The enrollments will expire after configured + // expiry limit, beyond which any action (approve/deny/revoke) on an + // enrollment is forbidden + ..metaData = (AtMetaData()..ttl = enrollmentExpiryInMills); } - - enrollmentValue.namespaces = enrollNamespaces; - enrollmentValue.requestType = EnrollRequestType.newEnrollment; - AtData enrollData = AtData()..data = jsonEncode(enrollmentValue.toJson()); logger.finer('enrollData: $enrollData'); await keyStore.put('$key$currentAtSign', enrollData, skipCommit: true); } @@ -158,41 +181,64 @@ class EnrollVerbHandler extends AbstractVerbHandler { EnrollParams enrollParams, currentAtSign, String? operation, - Map<dynamic, dynamic> responseJson) async { + Map<dynamic, dynamic> responseJson, + Response response) async { final enrollmentIdFromParams = enrollParams.enrollmentId; - var key = + String enrollmentKey = '$enrollmentIdFromParams.$newEnrollmentKeyPattern.$enrollManageNamespace'; - logger.finer('key: $key$currentAtSign'); - var enrollData; + logger.finer( + 'Enrollment key: $enrollmentKey$currentAtSign | Enrollment operation: $operation'); + EnrollDataStoreValue? enrollDataStoreValue; + EnrollStatus? enrollStatus; + // Fetch and returns enrollment data from the keystore. + // Throw AtEnrollmentException, IF + // 1. Enrollment key is not present in keystore + // 2. Enrollment key is not active try { - enrollData = await keyStore.get('$key$currentAtSign'); + enrollDataStoreValue = + await getEnrollDataStoreValue('$enrollmentKey$currentAtSign'); } on KeyNotFoundException { - throw AtEnrollmentException( - 'enrollment id: $enrollmentIdFromParams not found in keystore'); + // When an enrollment key is expired or invalid + enrollStatus = EnrollStatus.expired; + } + enrollStatus ??= + getEnrollStatusFromString(enrollDataStoreValue!.approval!.state); + // Validates if enrollment is not expired + if (EnrollStatus.expired == enrollStatus) { + response.isError = true; + response.errorCode = 'AT0028'; + response.errorMessage = + 'enrollment_id: $enrollmentIdFromParams is expired or invalid'; } - if (enrollData != null) { - final existingAtData = enrollData.data; - var enrollDataStoreValue = - EnrollDataStoreValue.fromJson(jsonDecode(existingAtData)); + if (response.isError) { + return; + } + // Verifies whether the enrollment state matches the intended state + // Throws AtEnrollmentException, if the enrollment state is different from + // the intended state + _verifyEnrollmentStateBeforeAction(operation, enrollStatus); + enrollDataStoreValue!.approval!.state = + _getEnrollStatusEnum(operation).name; + responseJson['status'] = _getEnrollStatusEnum(operation).name; - enrollDataStoreValue.approval!.state = - _getEnrollStatusEnum(operation).name; - responseJson['status'] = _getEnrollStatusEnum(operation).name; - AtData updatedEnrollData = AtData() - ..data = jsonEncode(enrollDataStoreValue.toJson()); - await keyStore.put('$key$currentAtSign', updatedEnrollData, - skipCommit: true); - // when enrollment is approved store the apkamPublicKey of the enrollment - if (operation == 'approve') { - var apkamPublicKeyInKeyStore = - 'public:${enrollDataStoreValue.appName}.${enrollDataStoreValue.deviceName}.pkam.$pkamNamespace.__public_keys$currentAtSign'; - var valueJson = {}; - valueJson[apkamPublicKey] = enrollDataStoreValue.apkamPublicKey; - var atData = AtData()..data = jsonEncode(valueJson); - await keyStore.put(apkamPublicKeyInKeyStore, atData); - await _storeEncryptionKeys( - enrollmentIdFromParams!, enrollParams, currentAtSign); - } + // If an enrollment is approved, we need the enrollment to be active + // to subsequently revoke the enrollment. Hence reset TTL and + // expiredAt on metadata. + /* TODO: Currently TTL is reset on all the enrollments. + However, if the enrollment state is denied or revoked, + unless we wanted to display denied or revoked enrollments in the UI, + we can let the TTL be, so that the enrollment will be deleted subsequently.*/ + await _updateEnrollmentValueAndResetTTL( + '$enrollmentKey$currentAtSign', enrollDataStoreValue); + // when enrollment is approved store the apkamPublicKey of the enrollment + if (operation == 'approve') { + var apkamPublicKeyInKeyStore = + 'public:${enrollDataStoreValue.appName}.${enrollDataStoreValue.deviceName}.pkam.$pkamNamespace.__public_keys$currentAtSign'; + var valueJson = {'apkamPublicKey': enrollDataStoreValue.apkamPublicKey}; + var atData = AtData()..data = jsonEncode(valueJson); + await keyStore.put(apkamPublicKeyInKeyStore, atData); + await _storeEncryptionKeys( + enrollmentIdFromParams!, enrollParams, currentAtSign); } responseJson['enrollmentId'] = enrollmentIdFromParams; } @@ -255,11 +301,13 @@ class EnrollVerbHandler extends AbstractVerbHandler { if (_doesEnrollmentHaveManageNamespace(enrollDataStoreValue)) { await _fetchAllEnrollments(enrollmentKeysList, enrollmentRequestsMap); } else { - enrollmentRequestsMap[enrollmentKey] = { - 'appName': enrollDataStoreValue.appName, - 'deviceName': enrollDataStoreValue.deviceName, - 'namespace': enrollDataStoreValue.namespaces - }; + if (enrollDataStoreValue.approval!.state != EnrollStatus.expired.name) { + enrollmentRequestsMap[enrollmentKey] = { + 'appName': enrollDataStoreValue.appName, + 'deviceName': enrollDataStoreValue.deviceName, + 'namespace': enrollDataStoreValue.namespaces + }; + } } return jsonEncode(enrollmentRequestsMap); } @@ -269,11 +317,13 @@ class EnrollVerbHandler extends AbstractVerbHandler { for (var enrollmentKey in enrollmentKeysList) { EnrollDataStoreValue enrollDataStoreValue = await getEnrollDataStoreValue(enrollmentKey); - enrollmentRequestsMap[enrollmentKey] = { - 'appName': enrollDataStoreValue.appName, - 'deviceName': enrollDataStoreValue.deviceName, - 'namespace': enrollDataStoreValue.namespaces - }; + if (enrollDataStoreValue.approval!.state != EnrollStatus.expired.name) { + enrollmentRequestsMap[enrollmentKey] = { + 'appName': enrollDataStoreValue.appName, + 'deviceName': enrollDataStoreValue.deviceName, + 'namespace': enrollDataStoreValue.namespaces + }; + } } } @@ -311,4 +361,35 @@ class EnrollVerbHandler extends AbstractVerbHandler { 'Error while storing notification key $enrollmentId. Error $e. Trace $trace'); } } + + /// Verifies whether the enrollment state matches the intended state. + /// Throws AtEnrollmentException: If the enrollment state is different + /// from the intended state. + void _verifyEnrollmentStateBeforeAction( + String? operation, EnrollStatus enrollStatus) { + if (operation == 'approve' && EnrollStatus.pending != enrollStatus) { + throw AtEnrollmentException( + 'Cannot approve a ${enrollStatus.name} enrollment. Only pending enrollments can be approved'); + } + if (operation == 'revoke' && EnrollStatus.approved != enrollStatus) { + throw AtEnrollmentException( + 'Cannot revoke a ${enrollStatus.name} enrollment. Only approved enrollments can be revoked'); + } + } + + Future<void> _updateEnrollmentValueAndResetTTL( + String enrollmentKey, EnrollDataStoreValue enrollDataStoreValue) async { + // Fetch the existing data + AtMetaData? enrollMetaData = await keyStore.getMeta(enrollmentKey); + // Update key with new data + // only update ttl, expiresAt in metadata to preserve all the other valid data fields + enrollMetaData?.ttl = 0; + enrollMetaData?.expiresAt = null; + await keyStore.put( + enrollmentKey, + AtData() + ..data = jsonEncode(enrollDataStoreValue.toJson()) + ..metaData = enrollMetaData, + skipCommit: true); + } } diff --git a/packages/at_secondary_server/lib/src/verb/handler/pkam_verb_handler.dart b/packages/at_secondary_server/lib/src/verb/handler/pkam_verb_handler.dart index 601c977c1..96b28aae2 100644 --- a/packages/at_secondary_server/lib/src/verb/handler/pkam_verb_handler.dart +++ b/packages/at_secondary_server/lib/src/verb/handler/pkam_verb_handler.dart @@ -86,16 +86,17 @@ class PkamVerbHandler extends AbstractVerbHandler { String enrollId, String atSign) async { String enrollmentKey = '$enrollId.$newEnrollmentKeyPattern.$enrollManageNamespace$atSign'; - var enrollData = await keyStore.get(enrollmentKey); - final atData = enrollData.data; - final enrollDataStoreValue = - EnrollDataStoreValue.fromJson(jsonDecode(atData)); - EnrollStatus enrollStatus = - EnrollStatus.values.byName(enrollDataStoreValue.approval!.state); - + late final EnrollDataStoreValue enrollDataStoreValue; ApkamVerificationResult apkamResult = ApkamVerificationResult(); - apkamResult.response = _getApprovalStatus( - enrollStatus, enrollId, enrollDataStoreValue.approval!.state); + EnrollStatus? enrollStatus; + try { + enrollDataStoreValue = await getEnrollDataStoreValue(enrollmentKey); + enrollStatus = getEnrollStatusFromString(enrollDataStoreValue.approval!.state); + } on KeyNotFoundException catch (e) { + logger.finer('Caught exception trying to fetch enrollment key: $e'); + enrollStatus = EnrollStatus.expired; + } + apkamResult.response = _getApprovalStatus(enrollStatus, enrollId); if (apkamResult.response.isError) { return apkamResult; } @@ -103,8 +104,7 @@ class PkamVerbHandler extends AbstractVerbHandler { return apkamResult; } - Response _getApprovalStatus( - EnrollStatus enrollStatus, enrollId, approvalState) { + Response _getApprovalStatus(EnrollStatus enrollStatus, enrollId) { Response response = Response(); switch (enrollStatus) { case EnrollStatus.denied: @@ -125,6 +125,11 @@ class PkamVerbHandler extends AbstractVerbHandler { response.errorCode = 'AT0027'; response.errorMessage = 'enrollment_id: $enrollId is revoked'; break; + case EnrollStatus.expired: + response.isError = true; + response.errorCode = 'AT0028'; + response.errorMessage = 'enrollment_id: $enrollId is expired or invalid'; + break; default: response.isError = true; response.errorCode = 'AT0026'; @@ -143,7 +148,7 @@ class PkamVerbHandler extends AbstractVerbHandler { bool isValidSignature = false; var storedSecret = await keyStore.get('private:$sessionId$atSign'); storedSecret = storedSecret?.data; - if(signature == null || signature.isEmpty ){ + if (signature == null || signature.isEmpty) { logger.severe('inputSignature is null/empty'); return false; } diff --git a/packages/at_secondary_server/pubspec.yaml b/packages/at_secondary_server/pubspec.yaml index 5569e42c6..03be29b74 100644 --- a/packages/at_secondary_server/pubspec.yaml +++ b/packages/at_secondary_server/pubspec.yaml @@ -1,6 +1,6 @@ name: at_secondary description: Implementation of secondary server. -version: 3.0.35 +version: 3.0.36 repository: https://github.com/atsign-foundation/at_server homepage: https://www.example.com publish_to: none @@ -18,18 +18,18 @@ dependencies: collection: 1.18.0 basic_utils: 5.6.1 ecdsa: 0.0.4 - at_commons: 3.0.54 + at_commons: 3.0.55 at_utils: 3.0.15 at_chops: 1.0.4 at_lookup: 3.0.40 - at_server_spec: 3.0.14 + at_server_spec: 3.0.15 at_persistence_spec: 2.0.14 at_persistence_secondary_server: 3.0.57 expire_cache: ^2.0.1 intl: ^0.18.1 json_annotation: ^4.8.0 version: 3.0.2 - meta: 1.9.1 + meta: 1.10.0 mutex: 3.0.1 yaml: 3.1.2 logging: 1.2.0 diff --git a/packages/at_secondary_server/test/enroll_verb_test.dart b/packages/at_secondary_server/test/enroll_verb_test.dart index 04f0efb1b..6e48d943d 100644 --- a/packages/at_secondary_server/test/enroll_verb_test.dart +++ b/packages/at_secondary_server/test/enroll_verb_test.dart @@ -220,7 +220,6 @@ void main() { var enrollOperationMap = { 'approve': 'approved', 'deny': 'denied', - 'revoke': 'revoked' }; enrollOperationMap.forEach((operation, expectedStatus) { @@ -342,7 +341,9 @@ void main() { test( 'A test to verify revoke operations thrown exception when given enrollmentId is not in keystore', () async { - String enrollmentRequest = 'enroll:revoke:{"enrollmentId":"123"}'; + String enrollmentId = '123'; + String enrollmentRequest = + 'enroll:revoke:{"enrollmentId":"$enrollmentId"}'; HashMap<String, String?> verbParams = getVerbParam(VerbSyntax.enroll, enrollmentRequest); inboundConnection.getMetaData().isAuthenticated = true; @@ -352,12 +353,13 @@ void main() { Response response = Response(); EnrollVerbHandler enrollVerbHandler = EnrollVerbHandler(secondaryKeyStore); - expect( - () async => await enrollVerbHandler.processVerb( - response, verbParams, inboundConnection), - throwsA(predicate((dynamic e) => - e is AtEnrollmentException && - e.message == 'enrollment id: 123 not found in keystore'))); + await enrollVerbHandler.processVerb( + response, verbParams, inboundConnection); + expect(response.isError, true); + expect(response.errorMessage, isNotNull); + assert(response.errorMessage! + .contains('enrollment_id: $enrollmentId is expired')); + expect(response.errorCode, 'AT0028'); }); tearDown(() async => await verbTestsTearDown()); }); @@ -379,12 +381,12 @@ void main() { inboundConnection.getMetaData().sessionID = 'dummy_session'; (inboundConnection.getMetaData() as InboundConnectionMetadata) .enrollmentId = '123'; - Response response = Response(); + Response responseObject = Response(); EnrollVerbHandler enrollVerbHandler = EnrollVerbHandler(secondaryKeyStore); await enrollVerbHandler.processVerb( - response, verbParams, inboundConnection); - Map<String, dynamic> enrollmentResponse = jsonDecode(response.data!); + responseObject, verbParams, inboundConnection); + Map<String, dynamic> enrollmentResponse = jsonDecode(responseObject.data!); expect(enrollmentResponse['enrollmentId'], isNotNull); expect(enrollmentResponse['status'], 'approved'); // Commit log @@ -463,4 +465,262 @@ void main() { }); tearDown(() async => await verbTestsTearDown()); }); + + group('A group of tests related to enrollment request expiry', () { + String? otp; + setUp(() async { + await verbTestsSetUp(); + // Fetch TOTP + String totpCommand = 'otp:get'; + HashMap<String, String?> totpVerbParams = + getVerbParam(VerbSyntax.otp, totpCommand); + OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); + inboundConnection.getMetaData().isAuthenticated = true; + Response defaultResponse = Response(); + await otpVerbHandler.processVerb( + defaultResponse, totpVerbParams, inboundConnection); + otp = defaultResponse.data; + }); + test('A test to verify expired enrollment cannot be approved', () async { + Response response = Response(); + // Enroll a request on an unauthenticated connection which will expire in 1 millisecond + EnrollVerbHandler enrollVerbHandler = + EnrollVerbHandler(secondaryKeyStore); + enrollVerbHandler.enrollmentExpiryInMills = 1; + String enrollmentRequest = + 'enroll:request:{"appName":"wavi","deviceName":"mydevice","namespaces":{"wavi":"r"},"otp":"$otp","apkamPublicKey":"dummy_apkam_public_key"}'; + HashMap<String, String?> enrollVerbParams = + getVerbParam(VerbSyntax.enroll, enrollmentRequest); + inboundConnection.getMetaData().isAuthenticated = false; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection); + String enrollmentId = jsonDecode(response.data!)['enrollmentId']; + String status = jsonDecode(response.data!)['status']; + expect(status, 'pending'); + await Future.delayed(Duration(milliseconds: 1)); + //Approve enrollment + String approveEnrollmentCommand = + 'enroll:approve:{"enrollmentId":"$enrollmentId"}'; + enrollVerbParams = + getVerbParam(VerbSyntax.enroll, approveEnrollmentCommand); + inboundConnection.getMetaData().isAuthenticated = true; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection); + expect(response.isError, true); + expect(response.errorMessage, isNotNull); + assert(response.errorMessage! + .contains('enrollment_id: $enrollmentId is expired')); + expect(response.errorCode, 'AT0028'); + }); + + test('A test to verify expired enrollment cannot be denied', () async { + Response response = Response(); + // Enroll a request on an unauthenticated connection which will expire in 1 millisecond + EnrollVerbHandler enrollVerbHandler = + EnrollVerbHandler(secondaryKeyStore); + enrollVerbHandler.enrollmentExpiryInMills = 1; + String enrollmentRequest = + 'enroll:request:{"appName":"wavi","deviceName":"mydevice","namespaces":{"wavi":"r"},"otp":"$otp","apkamPublicKey":"dummy_apkam_public_key"}'; + HashMap<String, String?> enrollVerbParams = + getVerbParam(VerbSyntax.enroll, enrollmentRequest); + inboundConnection.getMetaData().isAuthenticated = false; + inboundConnection.getMetaData().sessionID = 'dummy_session_id1'; + await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection); + String enrollmentId = jsonDecode(response.data!)['enrollmentId']; + String status = jsonDecode(response.data!)['status']; + expect(status, 'pending'); + //Deny enrollment + await Future.delayed(Duration(milliseconds: 1)); + String approveEnrollmentCommand = + 'enroll:deny:{"enrollmentId":"$enrollmentId"}'; + enrollVerbParams = + getVerbParam(VerbSyntax.enroll, approveEnrollmentCommand); + inboundConnection.getMetaData().isAuthenticated = true; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection); + expect(response.isError, true); + expect(response.errorMessage, isNotNull); + assert(response.errorMessage! + .contains('enrollment_id: $enrollmentId is expired')); + expect(response.errorCode, 'AT0028'); + }); + + test('A test to verify TTL on approved enrollment is reset', () async { + Response response = Response(); + // Enroll a request on an unauthenticated connection which will expire in 1 minute + EnrollVerbHandler enrollVerbHandler = + EnrollVerbHandler(secondaryKeyStore); + enrollVerbHandler.enrollmentExpiryInMills = 600000; + String enrollmentRequest = + 'enroll:request:{"appName":"wavi","deviceName":"mydevice","namespaces":{"wavi":"r"},"otp":"$otp","apkamPublicKey":"dummy_apkam_public_key"}'; + HashMap<String, String?> enrollVerbParams = + getVerbParam(VerbSyntax.enroll, enrollmentRequest); + inboundConnection.getMetaData().isAuthenticated = false; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection); + String enrollmentId = jsonDecode(response.data!)['enrollmentId']; + String status = jsonDecode(response.data!)['status']; + expect(status, 'pending'); + String enrollmentKey = + '$enrollmentId.$newEnrollmentKeyPattern.$enrollManageNamespace$alice'; + // Verify TTL is added to the enrollment + AtData? enrollmentData = await secondaryKeyStore.get(enrollmentKey); + expect(enrollmentData!.metaData!.expiresAt, isNotNull); + expect(enrollmentData.metaData!.ttl, 600000); + //Approve enrollment + String approveEnrollmentCommand = + 'enroll:approve:{"enrollmentId":"$enrollmentId"}'; + enrollVerbParams = + getVerbParam(VerbSyntax.enroll, approveEnrollmentCommand); + inboundConnection.getMetaData().isAuthenticated = true; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection); + // Verify TTL is reset + enrollmentData = await secondaryKeyStore.get(enrollmentKey); + expect(enrollmentData!.metaData!.expiresAt, null); + expect(enrollmentData.metaData!.ttl, 0); + }); + + test( + 'A test to verify TTL is not set for enrollment requested on an authenticated connection', + () async { + Response response = Response(); + EnrollVerbHandler enrollVerbHandler = + EnrollVerbHandler(secondaryKeyStore); + String enrollmentRequest = + 'enroll:request:{"appName":"wavi","deviceName":"mydevice","namespaces":{"wavi":"r"},"otp":"$otp","apkamPublicKey":"dummy_apkam_public_key"}'; + HashMap<String, String?> enrollVerbParams = + getVerbParam(VerbSyntax.enroll, enrollmentRequest); + inboundConnection.getMetaData().isAuthenticated = true; + inboundConnection.getMetaData().authType = AuthType.cram; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection); + String enrollmentId = jsonDecode(response.data!)['enrollmentId']; + expect(enrollmentId, isNotNull); + expect(jsonDecode(response.data!)['status'], 'approved'); + // Verify TTL is not set + AtData? enrollmentData = await secondaryKeyStore.get( + '$enrollmentId.$newEnrollmentKeyPattern.$enrollManageNamespace$alice'); + expect(enrollmentData!.metaData!.expiresAt, null); + expect(enrollmentData.metaData!.ttl, null); + }); + tearDown(() async => await verbTestsTearDown()); + }); + + group('A group of tests related to approve enrollment', () { + String? otp; + late String enrollmentId; + late EnrollVerbHandler enrollVerbHandler; + HashMap<String, String?> enrollVerbParams; + Response defaultResponse = Response(); + setUp(() async { + await verbTestsSetUp(); + // Fetch OTP + String totpCommand = 'otp:get'; + HashMap<String, String?> totpVerbParams = + getVerbParam(VerbSyntax.otp, totpCommand); + OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore); + inboundConnection.getMetaData().isAuthenticated = true; + await otpVerbHandler.processVerb( + defaultResponse, totpVerbParams, inboundConnection); + otp = defaultResponse.data; + // Enroll a request on an unauthenticated connection which will expire in 1 minute + enrollVerbHandler = EnrollVerbHandler(secondaryKeyStore); + enrollVerbHandler.enrollmentExpiryInMills = 60000; + String enrollmentRequest = + 'enroll:request:{"appName":"wavi","deviceName":"mydevice","namespaces":{"wavi":"r"},"otp":"$otp","apkamPublicKey":"dummy_apkam_public_key"}'; + HashMap<String, String?> enrollVerbParams = + getVerbParam(VerbSyntax.enroll, enrollmentRequest); + inboundConnection.getMetaData().isAuthenticated = false; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + await enrollVerbHandler.processVerb( + defaultResponse, enrollVerbParams, inboundConnection); + enrollmentId = jsonDecode(defaultResponse.data!)['enrollmentId']; + String status = jsonDecode(defaultResponse.data!)['status']; + expect(status, 'pending'); + }); + + test('A test to verify denied enrollment cannot be approved', () async { + Response response = Response(); + //deny enrollment + String denyEnrollmentCommand = + 'enroll:deny:{"enrollmentId":"$enrollmentId"}'; + enrollVerbParams = getVerbParam(VerbSyntax.enroll, denyEnrollmentCommand); + inboundConnection.getMetaData().isAuthenticated = true; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection); + expect(jsonDecode(response.data!)['enrollmentId'], enrollmentId); + expect(jsonDecode(response.data!)['status'], 'denied'); + //approve enrollment + String approveEnrollmentCommand = + 'enroll:approve:{"enrollmentId":"$enrollmentId"}'; + enrollVerbParams = + getVerbParam(VerbSyntax.enroll, approveEnrollmentCommand); + inboundConnection.getMetaData().isAuthenticated = true; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + expect( + () async => await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection), + throwsA(predicate((dynamic e) => + e is AtEnrollmentException && + e.message == + 'Cannot approve a denied enrollment. Only pending enrollments can be approved'))); + }); + + test('A test to verify revoked enrollment cannot be approved', () async { + Response response = Response(); + //approve enrollment + String approveEnrollmentCommand = + 'enroll:approve:{"enrollmentId":"$enrollmentId"}'; + HashMap<String, String?> approveEnrollVerbParams = + getVerbParam(VerbSyntax.enroll, approveEnrollmentCommand); + inboundConnection.getMetaData().isAuthenticated = true; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + await enrollVerbHandler.processVerb( + response, approveEnrollVerbParams, inboundConnection); + expect(jsonDecode(response.data!)['enrollmentId'], enrollmentId); + expect(jsonDecode(response.data!)['status'], 'approved'); + //revoke enrollment + String denyEnrollmentCommand = + 'enroll:revoke:{"enrollmentId":"$enrollmentId"}'; + enrollVerbParams = getVerbParam(VerbSyntax.enroll, denyEnrollmentCommand); + await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection); + expect(jsonDecode(response.data!)['enrollmentId'], enrollmentId); + expect(jsonDecode(response.data!)['status'], 'revoked'); + // Approved a revoked enrollment throws AtEnrollmentException + expect( + () async => await enrollVerbHandler.processVerb( + response, approveEnrollVerbParams, inboundConnection), + throwsA(predicate((dynamic e) => + e is AtEnrollmentException && + e.message == + 'Cannot approve a revoked enrollment. Only pending enrollments can be approved'))); + }); + + test('A test to verify pending enrollment cannot be revoked', () async { + Response response = Response(); + //revoke enrollment + String denyEnrollmentCommand = + 'enroll:revoke:{"enrollmentId":"$enrollmentId"}'; + enrollVerbParams = getVerbParam(VerbSyntax.enroll, denyEnrollmentCommand); + inboundConnection.getMetaData().isAuthenticated = true; + inboundConnection.getMetaData().sessionID = 'dummy_session_id'; + expect( + () async => await enrollVerbHandler.processVerb( + response, enrollVerbParams, inboundConnection), + throwsA(predicate((dynamic e) => + e is AtEnrollmentException && + e.message == + 'Cannot revoke a pending enrollment. Only approved enrollments can be revoked'))); + }); + }); } diff --git a/packages/at_secondary_server/test/inbound_connection_impl_test.dart b/packages/at_secondary_server/test/inbound_connection_impl_test.dart new file mode 100644 index 000000000..89c2c1d5c --- /dev/null +++ b/packages/at_secondary_server/test/inbound_connection_impl_test.dart @@ -0,0 +1,44 @@ +import 'dart:io'; + +import 'package:at_secondary/src/connection/inbound/inbound_connection_impl.dart'; +import 'package:at_server_spec/at_server_spec.dart'; +import 'package:test/test.dart'; + +void main(){ + group('A test to verify the rate limiter on inbound connection', () { + test('A test to verify requests exceeding the limit are rejected', () { + Socket? dummySocket; + AtConnection connection1 = InboundConnectionImpl(dummySocket, 'aaa'); + (connection1 as InboundConnectionImpl).maxRequestsPerTimeFrame = 1; + connection1.timeFrameInMillis = + Duration(milliseconds: 10).inMilliseconds; + expect(connection1.isRequestAllowed(), true); + expect(connection1.isRequestAllowed(), false); + }); + + test('A test to verify requests after the time window are accepted', + () async { + Socket? dummySocket; + AtConnection connection1 = InboundConnectionImpl(dummySocket, 'aaa'); + (connection1 as InboundConnectionImpl).maxRequestsPerTimeFrame = 1; + connection1.timeFrameInMillis = Duration(milliseconds: 2).inMilliseconds; + expect(connection1.isRequestAllowed(), true); + expect(connection1.isRequestAllowed(), false); + await Future.delayed(Duration(milliseconds: 2)); + expect(connection1.isRequestAllowed(), true); + }); + + test('A test to verify request from different connection is allowed', () { + Socket? dummySocket; + AtConnection connection1 = InboundConnectionImpl(dummySocket, 'aaa'); + AtConnection connection2 = InboundConnectionImpl(dummySocket, 'aaa'); + (connection1 as InboundConnectionImpl).maxRequestsPerTimeFrame = 1; + (connection2 as InboundConnectionImpl).maxRequestsPerTimeFrame = 1; + connection1.timeFrameInMillis = + Duration(milliseconds: 10).inMilliseconds; + expect(connection1.isRequestAllowed(), true); + expect(connection1.isRequestAllowed(), false); + expect(connection2.isRequestAllowed(), true); + }); + }); +} \ No newline at end of file diff --git a/packages/at_secondary_server/test/pkam_verb_test.dart b/packages/at_secondary_server/test/pkam_verb_test.dart index 58f3e89f8..9d93d013e 100644 --- a/packages/at_secondary_server/test/pkam_verb_test.dart +++ b/packages/at_secondary_server/test/pkam_verb_test.dart @@ -131,6 +131,18 @@ void main() { 'enrollment_id: enrollId is denied'); }); + test('verify apkam behaviour - case: enrollment expired ', () async { + enrollData.approval = EnrollApproval('denied'); + when(() => mockKeyStore.get(any())) + .thenThrow(KeyNotFoundException('key not found')); + + var apkamResult = + await pkamVerbHandler.handleApkamVerification('enrollId', '@alice'); + expect(apkamResult.response.isError, true); + expect(apkamResult.response.errorCode, 'AT0028'); + expect(apkamResult.response.errorMessage, + 'enrollment_id: enrollId is expired or invalid'); + }); tearDownAll(() async => await tearDownFunc()); }); diff --git a/packages/at_server_spec/CHANGELOG.md b/packages/at_server_spec/CHANGELOG.md index d6a3f02ae..cd7e06452 100644 --- a/packages/at_server_spec/CHANGELOG.md +++ b/packages/at_server_spec/CHANGELOG.md @@ -1,3 +1,6 @@ +## 3.0.15 +- feat: Introduce AtRateLimiter to limit the requests based on the criteria defined +- fix: Modify InboundConnection to implement AtRateLimiter to limit requests ## 3.0.14 - fix: Rename TOTP to OTP ## 3.0.13 diff --git a/packages/at_server_spec/lib/src/at_rate_limiter/at_rate_limiter.dart b/packages/at_server_spec/lib/src/at_rate_limiter/at_rate_limiter.dart new file mode 100644 index 000000000..bd2a3d4f3 --- /dev/null +++ b/packages/at_server_spec/lib/src/at_rate_limiter/at_rate_limiter.dart @@ -0,0 +1,18 @@ +/// A rate limiter class that allows controlling the rate of requests within a specified time frame. +/// +/// This class provides a way to limit the number of requests that can be made +/// within a specified time frame. It keeps track of the timestamps of previous +/// requests and allows requests to be made only if they do not exceed the +/// maximum allowed requests per time frame. +abstract class AtRateLimiter { + /// The maximum number of requests allowed within the specified time frame. + late int maxRequestsPerTimeFrame; + + /// The duration of the time frame within which requests are limited. + late int timeFrameInMillis; + + /// Checks whether a new request is allowed based on the rate limiting rules. + /// + /// Returns `true` if the request is allowed, or `false` if it exceeds the rate limit. + bool isRequestAllowed(); +} diff --git a/packages/at_server_spec/lib/src/connection/inbound_connection.dart b/packages/at_server_spec/lib/src/connection/inbound_connection.dart index c4851ab47..bab07d14b 100644 --- a/packages/at_server_spec/lib/src/connection/inbound_connection.dart +++ b/packages/at_server_spec/lib/src/connection/inbound_connection.dart @@ -1,7 +1,8 @@ import 'dart:io'; +import 'package:at_server_spec/src/at_rate_limiter/at_rate_limiter.dart'; import 'package:at_server_spec/src/connection/at_connection.dart'; -abstract class InboundConnection extends AtConnection { +abstract class InboundConnection extends AtConnection implements AtRateLimiter { ///Returns true if remote socket and remote port of this and connection matches bool equals(InboundConnection connection); diff --git a/packages/at_server_spec/pubspec.yaml b/packages/at_server_spec/pubspec.yaml index 9a327f72d..0812c17ab 100644 --- a/packages/at_server_spec/pubspec.yaml +++ b/packages/at_server_spec/pubspec.yaml @@ -1,6 +1,6 @@ name: at_server_spec description: A Dart library containing abstract classes that defines what implementations of the root and secondary servers are responsible for. -version: 3.0.14 +version: 3.0.15 repository: https://github.com/atsign-foundation/at_server homepage: https://docs.atsign.com documentation: https://docs.atsign.com/atplatform/secondaryserver @@ -10,7 +10,7 @@ environment: dependencies: meta: ^1.8.0 - at_commons: ^3.0.54 + at_commons: ^3.0.55 dev_dependencies: lints: ^1.0.1 diff --git a/tests/at_end2end_test/test/notify_verb_test.dart b/tests/at_end2end_test/test/notify_verb_test.dart index 383d94cc7..b112a28d3 100644 --- a/tests/at_end2end_test/test/notify_verb_test.dart +++ b/tests/at_end2end_test/test/notify_verb_test.dart @@ -49,7 +49,7 @@ void main() { String response = await sh2.read(); print('notify verb response : $response'); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); String notificationId = response.replaceAll('data:', ''); // notify status @@ -69,37 +69,37 @@ void main() { }); test('test to verify notify fetch verb for a valid notification-id', - () async { - /// NOTIFY VERB - var value = 'Copenhagen'; - await sh1.writeCommand( - 'notify:update:messageType:key:notifier:system:ttr:-1:$atSign_2:city.me$atSign_1:$value'); - String response = await sh1.read(); - print('notify verb response : $response'); - assert( + () async { + /// NOTIFY VERB + var value = 'Copenhagen'; + await sh1.writeCommand( + 'notify:update:messageType:key:notifier:system:ttr:-1:$atSign_2:city.me$atSign_1:$value'); + String response = await sh1.read(); + print('notify verb response : $response'); + assert( (!response.contains('Invalid syntax')) && (!response.contains('null'))); - String notificationId = response.replaceAll('data:', ''); - - // Assert notification status - response = await getNotifyStatus(sh1, notificationId, - returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); - print('notify status response : $response'); - expect(response, contains('data:delivered')); - - // Fetch notification - if (atSign1ServerVersion > Version(3, 0, 23)) { - await sh1.writeCommand('notify:fetch:$notificationId'); - response = await sh1.read(); - response = response.replaceFirst('data:', ''); - var atNotificationMap = jsonDecode(response); - expect(atNotificationMap['id'], notificationId); - expect(atNotificationMap['fromAtSign'], atSign_1); - expect(atNotificationMap['toAtSign'], atSign_2); - expect(atNotificationMap['type'], 'NotificationType.sent'); - expect(atNotificationMap['notificationStatus'], - 'NotificationStatus.delivered'); - } - }); + String notificationId = response.replaceAll('data:', ''); + + // Assert notification status + response = await getNotifyStatus(sh1, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + expect(response, contains('data:delivered')); + + // Fetch notification + if (atSign1ServerVersion > Version(3, 0, 23)) { + await sh1.writeCommand('notify:fetch:$notificationId'); + response = await sh1.read(); + response = response.replaceFirst('data:', ''); + var atNotificationMap = jsonDecode(response); + expect(atNotificationMap['id'], notificationId); + expect(atNotificationMap['fromAtSign'], atSign_1); + expect(atNotificationMap['toAtSign'], atSign_2); + expect(atNotificationMap['type'], 'NotificationType.sent'); + expect(atNotificationMap['notificationStatus'], + 'NotificationStatus.delivered'); + } + }); test('notify verb without messageType and operation', () async { /// NOTIFY VERB @@ -108,7 +108,7 @@ void main() { String response = await sh2.read(); print('notify verb response : $response'); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); String notificationId = response.replaceAll('data:', ''); // notify status @@ -118,11 +118,13 @@ void main() { assert(response.contains('data:delivered')); ///notify:list verb - await sh1.writeCommand('notify:list'); - response = await sh1.read(); - print('notify list verb response : $response'); - expect(response, - contains('"key":"$atSign_1:contact-no$atSign_2","value":null')); + if (atSign2ServerVersion > Version(3, 0, 35)) { + await sh1.writeCommand('notify:list'); + response = await sh1.read(); + print('notify list verb response : $response'); + expect(response, + contains('"key":"$atSign_1:contact-no$atSign_2","value":"$value')); + } }); test('notify verb without messageType', () async { @@ -133,7 +135,7 @@ void main() { String response = await sh2.read(); print('notify verb response : $response'); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); String notificationId = response.replaceAll('data:', ''); // notify status @@ -161,7 +163,7 @@ void main() { print('notify verb response : $response'); String notificationId = response.replaceAll('data:', ''); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); // notify status response = await getNotifyStatus(sh2, notificationId, @@ -185,7 +187,7 @@ void main() { print('notify verb response : $response'); String notificationId = response.replaceAll('data:', ''); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); // notify status response = await getNotifyStatus(sh1, notificationId, @@ -194,13 +196,16 @@ void main() { expect(response, contains('data:delivered')); //notify:list verb with regex + await sh2.writeCommand('notify:list:email'); response = await sh2.read(); print('notify list verb response : $response'); - expect( - response, - contains( - '"key":"$atSign_2:email$atSign_1","value":"null","operation":"delete"')); + if (atSign1ServerVersion > Version(3,0,35)) { + expect( + response, + contains( + '"key":"$atSign_2:email$atSign_1","value":null,"operation":"delete"')); + } }); test('notify verb without giving message type value', () async { @@ -234,7 +239,7 @@ void main() { print('notify verb response : $response'); String notificationId = response.replaceAll('data:', ''); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); // notify status response = await getNotifyStatus(sh1, notificationId, @@ -252,7 +257,7 @@ void main() { print('notify verb response : $response'); String notificationId = response.replaceAll('data:', ''); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); // notify status response = await getNotifyStatus(sh1, notificationId, @@ -268,7 +273,7 @@ void main() { print('notify verb response : $response'); String notificationId = response.replaceAll('data:', ''); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); // notify status response = await getNotifyStatus(sh1, notificationId, @@ -319,7 +324,7 @@ void main() { String response = await sh1.read(); print('notify verb response : $response'); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); /// atSign2: notify:list verb with regex String shouldContain = '"key":"$atSign_2:whatsapp$atSign_1"'; @@ -362,9 +367,11 @@ void main() { print('notify verb response : $response'); String notificationId = response.replaceAll('data:', ''); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); - int willExpireAt = DateTime.now().millisecondsSinceEpoch + ttln; + int willExpireAt = DateTime + .now() + .millisecondsSinceEpoch + ttln; // // notify status before ttln expiry time response = await getNotifyStatus(sh2, notificationId, @@ -373,7 +380,9 @@ void main() { expect(response, contains('data:delivered')); // Wait until ttln has been reached - int now = DateTime.now().millisecondsSinceEpoch; + int now = DateTime + .now() + .millisecondsSinceEpoch; if (now < willExpireAt) { await Future.delayed(Duration(milliseconds: willExpireAt - now)); } @@ -386,36 +395,40 @@ void main() { }); test('notify verb with notification expiry for errored- invalid atsign', - () async { - // /// NOTIFY VERB - int ttln = 11000; - await sh2.writeCommand( - 'notify:update:messageType:key:ttln:$ttln:ttr:-1:@xyz:message$atSign_2:Hey!'); - String response = await sh2.read(); - print('notify verb response : $response'); - String notificationId = response.replaceAll('data:', ''); - assert( + () async { + // /// NOTIFY VERB + int ttln = 11000; + await sh2.writeCommand( + 'notify:update:messageType:key:ttln:$ttln:ttr:-1:@xyz:message$atSign_2:Hey!'); + String response = await sh2.read(); + print('notify verb response : $response'); + String notificationId = response.replaceAll('data:', ''); + assert( (!response.contains('Invalid syntax')) && (!response.contains('null'))); - int willExpireAt = DateTime.now().millisecondsSinceEpoch + ttln; - - // notify status before ttln expiry time - response = await getNotifyStatus(sh2, notificationId, - returnWhenStatusIn: ['errored'], timeOutMillis: 10000); - expect(response, contains('data:errored')); - - // Wait until ttln has been reached - int now = DateTime.now().millisecondsSinceEpoch; - if (now < willExpireAt) { - await Future.delayed(Duration(milliseconds: willExpireAt - now)); - } - - /// notify status after ttln expiry time - response = await getNotifyStatus(sh2, notificationId, - returnWhenStatusIn: ['expired'], timeOutMillis: 5000); - print('notify status response : $response'); - expect(response, contains('data:expired')); - }); + int willExpireAt = DateTime + .now() + .millisecondsSinceEpoch + ttln; + + // notify status before ttln expiry time + response = await getNotifyStatus(sh2, notificationId, + returnWhenStatusIn: ['errored'], timeOutMillis: 10000); + expect(response, contains('data:errored')); + + // Wait until ttln has been reached + int now = DateTime + .now() + .millisecondsSinceEpoch; + if (now < willExpireAt) { + await Future.delayed(Duration(milliseconds: willExpireAt - now)); + } + + /// notify status after ttln expiry time + response = await getNotifyStatus(sh2, notificationId, + returnWhenStatusIn: ['expired'], timeOutMillis: 5000); + print('notify status response : $response'); + expect(response, contains('data:expired')); + }); test('notify verb with notification expiry with messageType text', () async { // /// NOTIFY VERB @@ -426,9 +439,11 @@ void main() { print('notify verb response : $response'); String notificationId = response.replaceAll('data:', ''); assert( - (!response.contains('Invalid syntax')) && (!response.contains('null'))); + (!response.contains('Invalid syntax')) && (!response.contains('null'))); - int willExpireAt = DateTime.now().millisecondsSinceEpoch + ttln; + int willExpireAt = DateTime + .now() + .millisecondsSinceEpoch + ttln; // notify status before ttln expiry time response = await getNotifyStatus(sh2, notificationId, @@ -437,7 +452,9 @@ void main() { expect(response, contains('data:delivered')); // Wait until ttln has been reached - int now = DateTime.now().millisecondsSinceEpoch; + int now = DateTime + .now() + .millisecondsSinceEpoch; if (now < willExpireAt) { await Future.delayed(Duration(milliseconds: willExpireAt - now)); } @@ -451,17 +468,17 @@ void main() { /// test('notify verb with notification expiry in an incorrect spelling', - () async { - // /// NOTIFY VERB - await sh2.writeCommand( - 'notify:update:ttlnn:5000:ttr:-1:$atSign_1:message$atSign_2:Hey!'); - String response = await sh2.read(); - print('notify verb response : $response'); - expect(response, contains('Invalid syntax')); - // Invalid syntax results in a closed connection so let's do some housekeeping - sh2.close(); - sh2 = await e2e.getSocketHandler(atSign_2); - }); + () async { + // /// NOTIFY VERB + await sh2.writeCommand( + 'notify:update:ttlnn:5000:ttr:-1:$atSign_1:message$atSign_2:Hey!'); + String response = await sh2.read(); + print('notify verb response : $response'); + expect(response, contains('Invalid syntax')); + // Invalid syntax results in a closed connection so let's do some housekeeping + sh2.close(); + sh2 = await e2e.getSocketHandler(atSign_2); + }); test('Test to verify the update and delete caching of key', () async { var key = 'testcachedkey-$lastValue'; @@ -534,30 +551,34 @@ void main() { test( 'notify a key and verifying the time taken for the status to be delivered', - () async { - var timeBeforeNotification = DateTime.now().millisecondsSinceEpoch; - await sh1.writeCommand( - 'notify:update:messageType:key:$atSign_2:company$atSign_1:atsign'); - String response = await sh1.read(); - print('notify verb response : $response'); - assert( + () async { + var timeBeforeNotification = DateTime + .now() + .millisecondsSinceEpoch; + await sh1.writeCommand( + 'notify:update:messageType:key:$atSign_2:company$atSign_1:atsign'); + String response = await sh1.read(); + print('notify verb response : $response'); + assert( (!response.contains('Invalid syntax')) && (!response.contains('null'))); - String notificationId = response.replaceAll('data:', ''); - assert( + String notificationId = response.replaceAll('data:', ''); + assert( (!response.contains('Invalid syntax')) && (!response.contains('null'))); - // notify status - response = await getNotifyStatus(sh1, notificationId, - returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); - print('notify status response : $response'); - expect(response, contains('data:delivered')); - var timeAfterNotification = DateTime.now().millisecondsSinceEpoch; - var timeDifferenceValue = + // notify status + response = await getNotifyStatus(sh1, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + expect(response, contains('data:delivered')); + var timeAfterNotification = DateTime + .now() + .millisecondsSinceEpoch; + var timeDifferenceValue = DateTime.fromMillisecondsSinceEpoch(timeAfterNotification).difference( DateTime.fromMillisecondsSinceEpoch(timeBeforeNotification)); - print('time difference is $timeDifferenceValue'); - expect(timeDifferenceValue.inMilliseconds <= 10000, true); - }); + print('time difference is $timeDifferenceValue'); + expect(timeDifferenceValue.inMilliseconds <= 10000, true); + }); /// The purpose of this test is verify the date time of second notification is correct /// not picked from the earlier notification. @@ -576,7 +597,8 @@ void main() { var notificationIdFromAtSign2 = (await sh2.read()).replaceAll('data:', ''); var atNotificationMap = jsonDecode(notificationIdFromAtSign2); var firstNotificationDateInEpoch = - DateTime.parse(atNotificationMap['notificationDateTime']) + DateTime + .parse(atNotificationMap['notificationDateTime']) .microsecondsSinceEpoch; // Sending second notification @@ -593,63 +615,64 @@ void main() { notificationIdFromAtSign2 = (await sh2.read()).replaceAll('data:', ''); atNotificationMap = jsonDecode(notificationIdFromAtSign2); var secondNotificationDateInEpoch = - DateTime.parse(atNotificationMap['notificationDateTime']) + DateTime + .parse(atNotificationMap['notificationDateTime']) .microsecondsSinceEpoch; expect(secondNotificationDateInEpoch > firstNotificationDateInEpoch, true); }); test('notify verb for notifying a key update with shared key metadata', - () async { - /// NOTIFY VERB - await sh1.writeCommand('notify:update:messageType:key:notifier:SYSTEM' - ':ttln:86400000:ttr:60000:ccd:false' - ':sharedKeyEnc:abc:pubKeyCS:3c55db695d94b304827367a4f5cab8ae' - ':$atSign_2:phone.wavi$atSign_1:Some ciphertext'); - String response = await sh1.read(); - print('notify verb response : $response'); - assert( + () async { + /// NOTIFY VERB + await sh1.writeCommand('notify:update:messageType:key:notifier:SYSTEM' + ':ttln:86400000:ttr:60000:ccd:false' + ':sharedKeyEnc:abc:pubKeyCS:3c55db695d94b304827367a4f5cab8ae' + ':$atSign_2:phone.wavi$atSign_1:Some ciphertext'); + String response = await sh1.read(); + print('notify verb response : $response'); + assert( (!response.contains('Invalid syntax')) && (!response.contains('null'))); - String notificationId = response.replaceAll('data:', ''); - - // notify status - response = await getNotifyStatus(sh1, notificationId, - returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); - print('notify status response : $response'); - expect(response, contains('data:delivered')); - - await sh2.writeCommand('llookup:all:cached:$atSign_2:phone.wavi$atSign_1'); - response = await sh2.read(); - response = response.replaceAll('data:', ''); - var decodedResponse = jsonDecode(response); - expect(decodedResponse['key'], 'cached:$atSign_2:phone.wavi$atSign_1'); - expect(decodedResponse['data'], 'Some ciphertext'); - expect(decodedResponse['metaData']['sharedKeyEnc'], 'abc'); - expect(decodedResponse['metaData']['pubKeyCS'], - '3c55db695d94b304827367a4f5cab8ae'); - expect(decodedResponse['metaData']['ttr'], 60000); - }); + String notificationId = response.replaceAll('data:', ''); + + // notify status + response = await getNotifyStatus(sh1, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + expect(response, contains('data:delivered')); + + await sh2.writeCommand( + 'llookup:all:cached:$atSign_2:phone.wavi$atSign_1'); + response = await sh2.read(); + response = response.replaceAll('data:', ''); + var decodedResponse = jsonDecode(response); + expect(decodedResponse['key'], 'cached:$atSign_2:phone.wavi$atSign_1'); + expect(decodedResponse['data'], 'Some ciphertext'); + expect(decodedResponse['metaData']['sharedKeyEnc'], 'abc'); + expect(decodedResponse['metaData']['pubKeyCS'], + '3c55db695d94b304827367a4f5cab8ae'); + expect(decodedResponse['metaData']['ttr'], 60000); + }); test('notify verb for notifying a key update with new encryption metadata', - () async { - /// NOTIFY VERB - var sharedKeyEnc = 'abc'; - var pubKeyCS = '3c55db695d94b304827367a4f5cab8ae'; - var encKeyName = 'someEncKeyName'; - var encAlgo = 'AES/CTR/PKCS7Padding'; - var iv = 'anInitializationVector'; - var skeEncKeyName = 'someSkeEncKeyName'; - var skeEncAlgo = 'RSA-2048'; - var ttln = 60 * 1000; // 60 seconds - - if (atSign1ServerVersion < Version(3, 0, 29)) { - // Server version 3.0.28 or earlier will not process new metadata - // No point in trying to send anything - return; - } - - await sh1.writeCommand( - 'notify:update' + () async { + /// NOTIFY VERB + var sharedKeyEnc = 'abc'; + var pubKeyCS = '3c55db695d94b304827367a4f5cab8ae'; + var encKeyName = 'someEncKeyName'; + var encAlgo = 'AES/CTR/PKCS7Padding'; + var iv = 'anInitializationVector'; + var skeEncKeyName = 'someSkeEncKeyName'; + var skeEncAlgo = 'RSA-2048'; + var ttln = 60 * 1000; // 60 seconds + + if (atSign1ServerVersion < Version(3, 0, 29)) { + // Server version 3.0.28 or earlier will not process new metadata + // No point in trying to send anything + return; + } + + await sh1.writeCommand('notify:update' ':messageType:key' ':notifier:SYSTEM' ':ttln:$ttln' @@ -664,47 +687,284 @@ void main() { ':skeEncAlgo:$skeEncAlgo' ':$atSign_2:phone.wavi$atSign_1' ':Some ciphertext'); - String response = await sh1.read(); - print('notify verb response : $response'); - assert( + String response = await sh1.read(); + print('notify verb response : $response'); + assert( (!response.contains('Invalid syntax')) && (!response.contains('null'))); - String notificationId = response.replaceAll('data:', ''); - - // notify status - response = await getNotifyStatus(sh1, notificationId, - returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); - print('notify status response : $response'); - expect(response, contains('data:delivered')); + String notificationId = response.replaceAll('data:', ''); + + // notify status + response = await getNotifyStatus(sh1, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + expect(response, contains('data:delivered')); + + await sh2.writeCommand( + 'llookup:all:cached:$atSign_2:phone.wavi$atSign_1'); + response = await sh2.read(); + response = response.replaceAll('data:', ''); + var decodedResponse = jsonDecode(response); + expect(decodedResponse['key'], 'cached:$atSign_2:phone.wavi$atSign_1'); + expect(decodedResponse['data'], 'Some ciphertext'); + expect(decodedResponse['metaData']['sharedKeyEnc'], sharedKeyEnc); + expect(decodedResponse['metaData']['pubKeyCS'], pubKeyCS); + expect(decodedResponse['metaData']['ttr'], 10); + + if (atSign2ServerVersion > Version(3, 0, 28)) { + expect(decodedResponse['metaData']['encKeyName'], encKeyName); + expect(decodedResponse['metaData']['encAlgo'], encAlgo); + expect(decodedResponse['metaData']['ivNonce'], iv); + expect(decodedResponse['metaData']['skeEncKeyName'], skeEncKeyName); + expect(decodedResponse['metaData']['skeEncAlgo'], skeEncAlgo); + } else { + expect(decodedResponse['metaData']['encKeyName'], null); + expect(decodedResponse['metaData']['encAlgo'], null); + expect(decodedResponse['metaData']['ivNonce'], null); + expect(decodedResponse['metaData']['skeEncKeyName'], null); + expect(decodedResponse['metaData']['skeEncAlgo'], null); + } + }); + + group('A group of tests related to notify ephemeral', () { + test( + 'notify verb without ttr for messageType-key and operation type - update and with value', + () async { + // The notify ephemeral changes are not into Canary and production. + // So, no point in running against and Canary and Prod servers.` + if (atSign2ServerVersion < Version(3, 0, 36)) { + return; + } + + /// NOTIFY VERB + var value = 'testingvalue'; + await sh2.writeCommand( + 'notify:update:messageType:key:$atSign_1:testkey$atSign_2:$value'); + String response = await sh2.read(); + print('notify verb response : $response'); + assert((!response.contains('Invalid syntax')) && + (!response.contains('null'))); + String notificationId = response.replaceAll('data:', ''); + + // notify status + response = await getNotifyStatus(sh2, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + assert(response.contains('data:delivered')); + + ///notify:list verb + await sh1.writeCommand('notify:list'); + response = await sh1.read(); + print('notify list verb response : $response'); + expect( + response, + contains( + '"key":"$atSign_1:testkey$atSign_2","value":"$value","operation":"update"')); + }); + + test('notify verb without ttr and without value for operation type update', + () async { + // The notify ephemeral changes are not into Canary and production. + // So, no point in running against and Canary and Prod servers.` + if (atSign2ServerVersion < Version(3, 0, 36)) { + return; + } + + /// NOTIFY VERB + await sh2.writeCommand('notify:update:$atSign_1:nottrkey$atSign_2'); + String response = await sh2.read(); + print('notify verb response : $response'); + assert((!response.contains('Invalid syntax')) && + (!response.contains('null'))); + String notificationId = response.replaceAll('data:', ''); + + // notify status + response = await getNotifyStatus(sh2, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + assert(response.contains('data:delivered')); + + ///notify:list verb + await sh1.writeCommand('notify:list'); + response = await sh1.read(); + print('notify list verb response : $response'); + expect( + response, + contains( + '"key":"$atSign_1:nottrkey$atSign_2","value":null,"operation":"update"')); + }); + + test( + 'notify verb without ttr for messageType-text and operation type - update', + () async { + // The notify ephemeral changes are not into Canary and production. + // So, no point in running against and Canary and Prod servers.` + if (atSign2ServerVersion < Version(3, 0, 36)) { + return; + } + + /// NOTIFY VERB + await sh2.writeCommand( + 'notify:update:messageType:text:$atSign_1:hello_world$atSign_2'); + String response = await sh2.read(); + print('notify verb response : $response'); + assert((!response.contains('Invalid syntax')) && + (!response.contains('null'))); + String notificationId = response.replaceAll('data:', ''); + + // notify status + response = await getNotifyStatus(sh2, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + assert(response.contains('data:delivered')); + + ///notify:list verb + await sh1.writeCommand('notify:list'); + response = await sh1.read(); + print('notify list verb response : $response'); + expect( + response, + contains( + '"key":"$atSign_1:hello_world","value":null,"operation":"update"')); + }); + + test( + 'notify verb without ttr for messageType-text and operation type - delete', + () async { + // The notify ephemeral changes are not into Canary and production. + // So, no point in running against and Canary and Prod servers.` + if (atSign2ServerVersion < Version(3, 0, 36)) { + return; + } + + /// NOTIFY VERB + await sh2.writeCommand( + 'notify:delete:messageType:text:$atSign_1:hello_world$atSign_2'); + String response = await sh2.read(); + print('notify verb response : $response'); + assert((!response.contains('Invalid syntax')) && + (!response.contains('null'))); + String notificationId = response.replaceAll('data:', ''); + + // notify status + response = await getNotifyStatus(sh2, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + assert(response.contains('data:delivered')); + + ///notify:list verb + await sh1.writeCommand('notify:list'); + response = await sh1.read(); + print('notify list verb response : $response'); + expect( + response, + contains( + '"key":"$atSign_1:hello_world","value":null,"operation":"delete"')); + }); + + test( + 'notify verb without ttr and without value for operation type update (self notification)', + () async { + // The notify ephemeral changes are not into Canary and production. + // So, no point in running against and Canary and Prod servers.` + if (atSign2ServerVersion < Version(3, 0, 36)) { + return; + } + + /// NOTIFY VERB + await sh2.writeCommand('notify:update:$atSign_2:nottrkey$atSign_2'); + String response = await sh2.read(); + print('notify verb response : $response'); + assert((!response.contains('Invalid syntax')) && + (!response.contains('null'))); + String notificationId = response.replaceAll('data:', ''); + + // notify status + response = await getNotifyStatus(sh2, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + assert(response.contains('data:delivered')); + + ///notify:list verb + await sh2.writeCommand('notify:list'); + response = await sh2.read(); + print('notify list verb response : $response'); + expect( + response, + contains( + '"key":"$atSign_2:nottrkey$atSign_2","value":null,"operation":"update"')); + }); + + test( + 'notify verb without ttr and with value for operation type update (self notification)', + () async { + // The notify ephemeral changes are not into Canary and production. + // So, no point in running against and Canary and Prod servers.` + if (atSign2ServerVersion < Version(3, 0, 36)) { + return; + } + + /// NOTIFY VERB + var value = 'no-ttr'; + await sh2 + .writeCommand('notify:update:$atSign_2:nottrkey$atSign_2:$value'); + String response = await sh2.read(); + print('notify verb response : $response'); + assert((!response.contains('Invalid syntax')) && + (!response.contains('null'))); + String notificationId = response.replaceAll('data:', ''); + + // notify status + response = await getNotifyStatus(sh2, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + assert(response.contains('data:delivered')); + + ///notify:list verb + await sh2.writeCommand('notify:list'); + response = await sh2.read(); + print('notify list verb response : $response'); + expect( + response, + contains( + '"key":"$atSign_2:nottrkey$atSign_2","value":"$value","operation":"update"')); + }); + + test('notify verb without ttr for operation type delete', () async { + // The notify ephemeral changes are not into Canary and production. + // So, no point in running against and Canary and Prod servers.` + if (atSign2ServerVersion < Version(3, 0, 36)) { + return; + } - await sh2.writeCommand('llookup:all:cached:$atSign_2:phone.wavi$atSign_1'); - response = await sh2.read(); - response = response.replaceAll('data:', ''); - var decodedResponse = jsonDecode(response); - expect(decodedResponse['key'], 'cached:$atSign_2:phone.wavi$atSign_1'); - expect(decodedResponse['data'], 'Some ciphertext'); - expect(decodedResponse['metaData']['sharedKeyEnc'], sharedKeyEnc); - expect(decodedResponse['metaData']['pubKeyCS'], pubKeyCS); - expect(decodedResponse['metaData']['ttr'], 10); - - if (atSign2ServerVersion > Version(3, 0, 28)) { - expect(decodedResponse['metaData']['encKeyName'], encKeyName); - expect(decodedResponse['metaData']['encAlgo'], encAlgo); - expect(decodedResponse['metaData']['ivNonce'], iv); - expect(decodedResponse['metaData']['skeEncKeyName'], skeEncKeyName); - expect(decodedResponse['metaData']['skeEncAlgo'], skeEncAlgo); - } else { - expect(decodedResponse['metaData']['encKeyName'], null); - expect(decodedResponse['metaData']['encAlgo'], null); - expect(decodedResponse['metaData']['ivNonce'], null); - expect(decodedResponse['metaData']['skeEncKeyName'], null); - expect(decodedResponse['metaData']['skeEncAlgo'], null); - } + /// NOTIFY VERB + await sh2.writeCommand('notify:delete:$atSign_1:twitter-id$atSign_2'); + String response = await sh2.read(); + print('notify verb response : $response'); + assert((!response.contains('Invalid syntax')) && + (!response.contains('null'))); + String notificationId = response.replaceAll('data:', ''); + + // notify status + response = await getNotifyStatus(sh2, notificationId, + returnWhenStatusIn: ['delivered'], timeOutMillis: 15000); + print('notify status response : $response'); + assert(response.contains('data:delivered')); + + ///notify:list verb + await sh1.writeCommand('notify:list'); + response = await sh1.read(); + print('notify list verb response : $response'); + expect( + response, + contains( + '"key":"$atSign_1:twitter-id$atSign_2","value":null,"operation":"delete"')); + }); }); } // get notify status -Future<String> getNotifyStatus( - e2e.SimpleOutboundSocketHandler sh, String notificationId, +Future<String> getNotifyStatus(e2e.SimpleOutboundSocketHandler sh, + String notificationId, {List<String>? returnWhenStatusIn, int timeOutMillis = 5000}) async { returnWhenStatusIn ??= ['expired']; print( @@ -715,8 +975,12 @@ Future<String> getNotifyStatus( String response = 'NO_RESPONSE'; bool readTimedOut = false; - int endTime = DateTime.now().millisecondsSinceEpoch + timeOutMillis; - while (DateTime.now().millisecondsSinceEpoch < endTime) { + int endTime = DateTime + .now() + .millisecondsSinceEpoch + timeOutMillis; + while (DateTime + .now() + .millisecondsSinceEpoch < endTime) { await Future.delayed(Duration(milliseconds: loopDelay)); if (!readTimedOut) { @@ -726,7 +990,7 @@ Future<String> getNotifyStatus( log: true, timeoutMillis: loopDelay, throwTimeoutException: false); readTimedOut = - (response == e2e.SimpleOutboundSocketHandler.readTimedOutMessage); + (response == e2e.SimpleOutboundSocketHandler.readTimedOutMessage); if (response.startsWith('data:')) { String status = response.replaceFirst('data:', '').replaceAll('\n', ''); @@ -752,8 +1016,12 @@ Future<String> retryCommandUntilMatchOrTimeout( String response = 'NO_RESPONSE'; bool readTimedOut = false; - int endTime = DateTime.now().millisecondsSinceEpoch + timeoutMillis; - while (DateTime.now().millisecondsSinceEpoch < endTime) { + int endTime = DateTime + .now() + .millisecondsSinceEpoch + timeoutMillis; + while (DateTime + .now() + .millisecondsSinceEpoch < endTime) { await Future.delayed(Duration(milliseconds: loopDelay)); if (!readTimedOut) { @@ -764,7 +1032,7 @@ Future<String> retryCommandUntilMatchOrTimeout( log: false, timeoutMillis: loopDelay, throwTimeoutException: false); readTimedOut = - (response == e2e.SimpleOutboundSocketHandler.readTimedOutMessage); + (response == e2e.SimpleOutboundSocketHandler.readTimedOutMessage); if (readTimedOut) { continue; } diff --git a/tests/at_functional_test/pubspec.yaml b/tests/at_functional_test/pubspec.yaml index f688db31a..e0a4919e0 100644 --- a/tests/at_functional_test/pubspec.yaml +++ b/tests/at_functional_test/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: ref: trunk at_chops: ^1.0.1 at_lookup: ^3.0.32 - at_commons: ^3.0.53 + at_commons: ^3.0.55 uuid: ^3.0.7 elliptic: ^0.3.8 diff --git a/tests/at_functional_test/test/enroll_verb_test.dart b/tests/at_functional_test/test/enroll_verb_test.dart index b7eac899e..afa4e56db 100644 --- a/tests/at_functional_test/test/enroll_verb_test.dart +++ b/tests/at_functional_test/test/enroll_verb_test.dart @@ -119,10 +119,8 @@ void main() { await socket_writer(socketConnection1!, approveEnrollCommand); var approveEnrollResponse = await read(); approveEnrollResponse = approveEnrollResponse.replaceFirst('error:', ''); - expect( - approveEnrollResponse.contains( - 'enrollment id: $dummyEnrollmentId not found in keystore'), - true); + expect(approveEnrollResponse, + 'AT0028:enrollment_id: $dummyEnrollmentId is expired or invalid\n'); }); test( @@ -141,10 +139,8 @@ void main() { await socket_writer(socketConnection1!, denyEnrollCommand); var denyEnrollResponse = await read(); denyEnrollResponse = denyEnrollResponse.replaceFirst('error:', ''); - expect( - denyEnrollResponse.contains( - 'enrollment id: $dummyEnrollmentId not found in keystore'), - true); + expect(denyEnrollResponse, + 'AT0028:enrollment_id: $dummyEnrollmentId is expired or invalid\n'); }); test('enroll request on unauthenticated connection without otp', () async { @@ -651,4 +647,230 @@ void main() { }); }); }); + + group('A group of negative tests on enroll verb', () { + late String enrollmentId; + late String enrollmentResponse; + setUp(() async { + // Get TOTP from server + String otp = await _getOTPFromServer(firstAtsign); + await socketConnection1?.close(); + // Close the connection and create a new connection and send an enrollment request on an + // unauthenticated connection. + await _connect(); + String enrollRequest = + 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}'; + await socket_writer(socketConnection1!, enrollRequest); + enrollmentResponse = await read(); + enrollmentResponse = enrollmentResponse.replaceAll('data:', ''); + enrollmentId = jsonDecode(enrollmentResponse)['enrollmentId']; + socketConnection1?.close(); + }); + test( + 'A test to verify error is returned when pending enrollment is revoked', + () async { + // Revoke enrollment on an authenticate connection + await _connect(); + await prepare(socketConnection1!, firstAtsign); + await socket_writer( + socketConnection1!, 'enroll:revoke:{"enrollmentId":"$enrollmentId"}'); + enrollmentResponse = (await read()).replaceAll('error:', ''); + expect(jsonDecode(enrollmentResponse)['errorDescription'], + 'Internal server exception : Cannot revoke a pending enrollment. Only approved enrollments can be revoked'); + }); + + test( + 'A test to verify error is returned when denied enrollment is approved', + () async { + // Deny enrollment on an authenticate connection + await _connect(); + await prepare(socketConnection1!, firstAtsign); + await socket_writer( + socketConnection1!, 'enroll:deny:{"enrollmentId":"$enrollmentId"}'); + enrollmentResponse = (await read()).replaceAll('data:', ''); + expect(jsonDecode(enrollmentResponse)['status'], 'denied'); + expect(jsonDecode(enrollmentResponse)['enrollmentId'], enrollmentId); + // Approve enrollment + await socket_writer(socketConnection1!, + 'enroll:approve:{"enrollmentId":"$enrollmentId"}'); + enrollmentResponse = (await read()).replaceAll('error:', ''); + expect( + jsonDecode(enrollmentResponse)['errorDescription'], + 'Internal server exception : Cannot approve a denied enrollment. ' + 'Only pending enrollments can be approved'); + }); + + test('A test to verify error is returned when denied enrollment is revoked', + () async { + // Deny enrollment on an authenticate connection + await _connect(); + await prepare(socketConnection1!, firstAtsign); + await socket_writer( + socketConnection1!, 'enroll:deny:{"enrollmentId":"$enrollmentId"}'); + enrollmentResponse = (await read()).replaceAll('data:', ''); + expect(jsonDecode(enrollmentResponse)['status'], 'denied'); + expect(jsonDecode(enrollmentResponse)['enrollmentId'], enrollmentId); + // Revoke enrollment + await socket_writer( + socketConnection1!, 'enroll:revoke:{"enrollmentId":"$enrollmentId"}'); + enrollmentResponse = (await read()).replaceAll('error:', ''); + expect( + jsonDecode(enrollmentResponse)['errorDescription'], + 'Internal server exception : Cannot revoke a denied enrollment. ' + 'Only approved enrollments can be revoked'); + }); + + test('A test to verify revoked enrollment cannot be approved', () async { + // Approve enrollment + await _connect(); + await prepare(socketConnection1!, firstAtsign); + await socket_writer(socketConnection1!, + 'enroll:approve:{"enrollmentId":"$enrollmentId"}'); + enrollmentResponse = (await read()).replaceAll('data:', ''); + expect(jsonDecode(enrollmentResponse)['status'], 'approved'); + expect(jsonDecode(enrollmentResponse)['enrollmentId'], enrollmentId); + // Revoke enrollment + await socket_writer( + socketConnection1!, 'enroll:revoke:{"enrollmentId":"$enrollmentId"}'); + enrollmentResponse = (await read()).replaceAll('data:', ''); + expect(jsonDecode(enrollmentResponse)['status'], 'revoked'); + expect(jsonDecode(enrollmentResponse)['enrollmentId'], enrollmentId); + // Approve a revoked enrollment + await socket_writer(socketConnection1!, + 'enroll:approve:{"enrollmentId":"$enrollmentId"}'); + enrollmentResponse = (await read()).replaceAll('error:', ''); + expect( + jsonDecode(enrollmentResponse)['errorDescription'], + 'Internal server exception : Cannot approve a revoked enrollment. ' + 'Only pending enrollments can be approved'); + }); + }); + + group('A group of test related to Rate limiting enrollment requests', () { + String otp = ''; + setUp(() async { + await socket_writer(socketConnection1!, 'from:$firstAtsign'); + var fromResponse = await read(); + fromResponse = fromResponse.replaceAll('data:', ''); + var cramResponse = getDigest(firstAtsign, fromResponse); + await socket_writer(socketConnection1!, 'cram:$cramResponse'); + var cramResult = await read(); + expect(cramResult, 'data:success\n'); + await socket_writer( + socketConnection1!, 'config:set:maxRequestsPerTimeFrame=1\n'); + var configResponse = await read(); + expect(configResponse.trim(), 'data:ok'); + await socket_writer( + socketConnection1!, 'config:set:timeFrameInMills=100\n'); + configResponse = await read(); + expect(configResponse.trim(), 'data:ok'); + await socket_writer(socketConnection1!, 'otp:get'); + otp = await read(); + otp = otp.replaceAll('data:', '').trim(); + }); + + test( + 'A test to verify exception is thrown when request exceed the configured limit', + () async { + SecureSocket unAuthenticatedConnection = + await secure_socket_connection(firstAtsignServer, firstAtsignPort); + socket_listener(unAuthenticatedConnection); + var enrollRequest = + 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; + await socket_writer(unAuthenticatedConnection, enrollRequest); + var enrollmentResponse = + jsonDecode((await read()).replaceAll('data:', '')); + expect(enrollmentResponse['status'], 'pending'); + expect(enrollmentResponse['enrollmentId'], isNotNull); + enrollRequest = + 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; + await socket_writer(unAuthenticatedConnection, enrollRequest); + enrollmentResponse = await read() + ..replaceAll('error:', ''); + expect( + enrollmentResponse.contains( + 'Enrollment requests have exceeded the limit within the specified time frame'), + true); + }); + + test('A test to verify request is successful after the time window', + () async { + SecureSocket unAuthenticatedConnection = + await secure_socket_connection(firstAtsignServer, firstAtsignPort); + socket_listener(unAuthenticatedConnection); + var enrollRequest = + 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; + await socket_writer(unAuthenticatedConnection, enrollRequest); + var enrollmentResponse = + jsonDecode((await read()).replaceAll('data:', '')); + expect(enrollmentResponse['status'], 'pending'); + expect(enrollmentResponse['enrollmentId'], isNotNull); + enrollRequest = + 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; + await socket_writer(unAuthenticatedConnection, enrollRequest); + enrollmentResponse = await read() + ..replaceAll('error:', ''); + expect( + enrollmentResponse.contains( + 'Enrollment requests have exceeded the limit within the specified time frame'), + true); + await Future.delayed(Duration(milliseconds: 110)); + await socket_writer(unAuthenticatedConnection, enrollRequest); + enrollmentResponse = jsonDecode((await read()).replaceAll('data:', '')); + expect(enrollmentResponse['status'], 'pending'); + expect(enrollmentResponse['enrollmentId'], isNotNull); + }); + + test('A test to verify rate limit is per connection', () async { + SecureSocket unAuthenticatedConnection = + await secure_socket_connection(firstAtsignServer, firstAtsignPort); + socket_listener(unAuthenticatedConnection); + var enrollRequest = + 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; + await socket_writer(unAuthenticatedConnection, enrollRequest); + var enrollmentResponse = + jsonDecode((await read()).replaceAll('data:', '')); + expect(enrollmentResponse['status'], 'pending'); + expect(enrollmentResponse['enrollmentId'], isNotNull); + enrollRequest = + 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; + await socket_writer(unAuthenticatedConnection, enrollRequest); + enrollmentResponse = await read() + ..replaceAll('error:', ''); + expect( + enrollmentResponse.contains( + 'Enrollment requests have exceeded the limit within the specified time frame'), + true); + SecureSocket secondUnAuthenticatedConnection2 = + await secure_socket_connection(firstAtsignServer, firstAtsignPort); + socket_listener(secondUnAuthenticatedConnection2); + enrollRequest = + 'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n'; + await socket_writer(secondUnAuthenticatedConnection2, enrollRequest); + enrollmentResponse = jsonDecode((await read()).replaceAll('data:', '')); + expect(enrollmentResponse['status'], 'pending'); + expect(enrollmentResponse['enrollmentId'], isNotNull); + }); + + tearDown(() async { + socket_writer(socketConnection1!, 'config:reset:maxRequestsAllowed'); + await read(); + socket_writer(socketConnection1!, 'config:reset:timeWindowInMills'); + await read(); + }); + }); +} + +Future<String> _getOTPFromServer(String atSign) async { + await socket_writer(socketConnection1!, 'from:$atSign'); + var fromResponse = await read(); + fromResponse = fromResponse.replaceAll('data:', ''); + var pkamDigest = generatePKAMDigest(atSign, fromResponse); + await socket_writer(socketConnection1!, 'pkam:$pkamDigest'); + // Calling read to remove the PKAM request from the queue + await read(); + await socket_writer(socketConnection1!, 'otp:get'); + String otp = await read(); + otp = otp.replaceAll('data:', '').trim(); + return otp; } diff --git a/tools/build_secondary/Dockerfile b/tools/build_secondary/Dockerfile index b62ccd554..d09b25296 100644 --- a/tools/build_secondary/Dockerfile +++ b/tools/build_secondary/Dockerfile @@ -1,4 +1,4 @@ -FROM dart:3.1.0@sha256:96d2e5d03b8356c2a7542716ace7dce745971efe1d03888a1d7ecd2e7c1dde36 AS buildimage +FROM dart:3.1.1@sha256:ec7bb9e577648ea5526c9daf714e9bc7af670ce7c93b594205e68c14a10cea3b AS buildimage ENV HOMEDIR=/atsign ENV USER_ID=1024 ENV GROUP_ID=1024 diff --git a/tools/build_secondary/Dockerfile.observe b/tools/build_secondary/Dockerfile.observe index 75c53affc..36ae693cd 100644 --- a/tools/build_secondary/Dockerfile.observe +++ b/tools/build_secondary/Dockerfile.observe @@ -1,4 +1,4 @@ -FROM dart:3.1.0@sha256:96d2e5d03b8356c2a7542716ace7dce745971efe1d03888a1d7ecd2e7c1dde36 AS buildimage +FROM dart:3.1.1@sha256:ec7bb9e577648ea5526c9daf714e9bc7af670ce7c93b594205e68c14a10cea3b AS buildimage ENV HOMEDIR=/atsign ENV USER_ID=1024 ENV GROUP_ID=1024 diff --git a/tools/build_virtual_environment/ve/Dockerfile.vip b/tools/build_virtual_environment/ve/Dockerfile.vip index 91b025b19..e402ca057 100644 --- a/tools/build_virtual_environment/ve/Dockerfile.vip +++ b/tools/build_virtual_environment/ve/Dockerfile.vip @@ -1,4 +1,4 @@ -FROM dart:3.1.0@sha256:96d2e5d03b8356c2a7542716ace7dce745971efe1d03888a1d7ecd2e7c1dde36 AS buildimage +FROM dart:3.1.1@sha256:ec7bb9e577648ea5526c9daf714e9bc7af670ce7c93b594205e68c14a10cea3b AS buildimage ENV USER_ID=1024 ENV GROUP_ID=1024 WORKDIR /app diff --git a/tools/build_virtual_environment/ve_base/Dockerfile b/tools/build_virtual_environment/ve_base/Dockerfile index 9131e5f4c..e8553fea4 100644 --- a/tools/build_virtual_environment/ve_base/Dockerfile +++ b/tools/build_virtual_environment/ve_base/Dockerfile @@ -1,4 +1,4 @@ -FROM dart:3.1.0@sha256:96d2e5d03b8356c2a7542716ace7dce745971efe1d03888a1d7ecd2e7c1dde36 AS buildimage +FROM dart:3.1.1@sha256:ec7bb9e577648ea5526c9daf714e9bc7af670ce7c93b594205e68c14a10cea3b AS buildimage ENV USER_ID=1024 ENV GROUP_ID=1024 WORKDIR /app @@ -17,7 +17,7 @@ RUN \ dart pub update ; \ dart compile exe bin/install_PKAM_Keys.dart -o install_PKAM_Keys -FROM debian:stable-20230814-slim@sha256:6fe30b9cb71d604a872557be086c74f95451fecd939d72afe3cffca3d9e60607 +FROM debian:stable-20230904-slim@sha256:0941f9e9cc96c4106845a381fb6fca98393f5f659f3eba6a64e9f79219165cfc # was debian:stable-20221114-slim USER root