diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f0f2b4234..0ffe58fbd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,7 @@ updates: - "/packages/c/sshnpd/tools/" - "/packages/dart/sshnoports/tools/" - "/tests/end2end_tests/image/" + - "/tools/multibuild/" schedule: interval: "daily" groups: diff --git a/.github/workflows/multibuild.yaml b/.github/workflows/multibuild.yaml index c272ba13a..312052b10 100644 --- a/.github/workflows/multibuild.yaml +++ b/.github/workflows/multibuild.yaml @@ -3,7 +3,7 @@ name: Multibuild on: push: tags: - - 'v*.*.*' + - "v*.*.*" workflow_dispatch: inputs: main_build_only: @@ -27,7 +27,9 @@ jobs: git config --global user.name 'Atsign Robot' git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' git checkout -b multibuild-${{github.run_number}} - - name: Ensure pubspec.yaml matches git ref (if current git ref is a version tag) + - name: + Ensure pubspec.yaml matches git ref (if current git ref is a version + tag) shell: bash if: startsWith(github.ref, 'refs/tags/v') working-directory: ./packages/dart/sshnoports @@ -53,28 +55,31 @@ jobs: include: - os: ubuntu-latest output-name: sshnp-linux-x64 - ext: '' - bundle: 'shell' + ext: "" + bundle: "shell" - os: macos-13 output-name: sshnp-macos-x64 - ext: '' - bundle: 'shell' + ext: "" + bundle: "shell" - os: macos-14 output-name: sshnp-macos-arm64 - ext: '' - bundle: 'shell' + ext: "" + bundle: "shell" - os: windows-latest output-name: sshnp-windows-x64 - ext: '.exe' - bundle: 'windows' + ext: ".exe" + bundle: "windows" steps: - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: multibuild-${{github.run_number}} - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 # v1.6.5 + - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 + with: + node-version: '20.17.0' # create directories need for build - run: | - mkdir sshnp + mkdir -p sshnp/web/admin mkdir tarball - if: ${{ matrix.os != 'windows-latest' }} run: mkdir sshnp/debug @@ -85,8 +90,21 @@ jobs: dart compile exe bin/activate_cli.dart -v -o sshnp/at_activate${{ matrix.ext }} dart compile exe bin/sshnp.dart -v -o sshnp/sshnp${{ matrix.ext }} dart compile exe bin/npt.dart -v -o sshnp/npt${{ matrix.ext }} + dart compile exe bin/npp_file.dart -v -o sshnp/npa_file${{ matrix.ext }} + dart compile exe bin/npp_file.dart -v -o sshnp/npp_file${{ matrix.ext }} dart compile exe bin/sshnpd.dart -v -o sshnp/sshnpd${{ matrix.ext }} dart compile exe bin/srv.dart -v -o sshnp/srv${{ matrix.ext }} + dart compile exe bin/npp_atserver.dart -v -o sshnp/npp_atserver${{ matrix.ext }} + - name: build admin API + working-directory: ./apps/admin/admin_api + run: | + dart pub get --enforce-lockfile + dart compile exe bin/np_admin.dart -v -o ../../../packages/dart/sshnoports/sshnp/np_admin${{ matrix.ext }} + - name: build admin webapp + working-directory: ./apps/admin/webapp + run: | + npm install + npm run build - if: ${{ matrix.os != 'windows-latest' }} run: | dart compile exe bin/srvd.dart -v -o sshnp/srvd${{ matrix.ext }} @@ -95,13 +113,15 @@ jobs: - run: | cp -r bundles/core/* sshnp/ cp -r bundles/${{ matrix.bundle }}/* sshnp/ + cp -r ../../../apps/admin/webapp/dist/* sshnp/web/admin/ cp LICENSE sshnp # codesign for apple - if: ${{ matrix.os == 'macos-13' || matrix.os == 'macos-14' }} name: Import certificates env: MACOS_CODESIGN_CERT: ${{ secrets.MACOS_CODESIGN_CERT }} - MACOS_CODESIGN_CERT_PASSWORD: ${{ secrets.MACOS_CODESIGN_CERT_PASSWORD }} + MACOS_CODESIGN_CERT_PASSWORD: + ${{ secrets.MACOS_CODESIGN_CERT_PASSWORD }} MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }} MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} run: | @@ -124,14 +144,17 @@ jobs: --prefix "com.atsign." \ --timestamp \ -v \ - sshnp/{sshnp,sshnpd,srv,srvd,at_activate,debug/srvd,npt} + sshnp/{sshnp,sshnpd,srv,srvd,at_activate,debug/srvd,npt,npa_file,npp_file,npp_atserver,np_admin} # zip the build - if: ${{ matrix.os == 'macos-13' || matrix.os == 'macos-14' }} - run: ditto -c -k --keepParent sshnp tarball/${{ matrix.output-name }}.zip + run: + ditto -c -k --keepParent sshnp tarball/${{ matrix.output-name }}.zip - if: ${{ matrix.os == 'ubuntu-latest' }} run: tar -cvzf tarball/${{ matrix.output-name }}.tgz sshnp - if: ${{ matrix.os == 'windows-latest' }} - run: Compress-Archive -Path sshnp -Destination tarball/${{ matrix.output-name }}.zip + run: + Compress-Archive -Path sshnp -Destination tarball/${{ + matrix.output-name }}.zip # notarize the build - if: ${{ matrix.os == 'macos-13' || matrix.os == 'macos-14' }} env: @@ -147,16 +170,15 @@ jobs: # upload the build - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: - name: ${{ matrix.output-name }}-${{github.ref_name}}-${{github.run_number}}-${{github.run_attempt}} + name: + ${{ matrix.output-name + }}-${{github.ref_name}}-${{github.run_number}}-${{github.run_attempt}} path: ./packages/dart/sshnoports/tarball if-no-files-found: error other_build: needs: verify_tags runs-on: ubuntu-latest - defaults: - run: - working-directory: ./packages/dart strategy: matrix: platform: [linux/arm/v7, linux/arm64, linux/riscv64] @@ -178,15 +200,17 @@ jobs: uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1 - if: ${{ ! inputs.main_build_only }} run: | - docker buildx build -t atsigncompany/sshnptarball -f sshnoports/tools/Dockerfile.package \ + docker buildx build -t atsigncompany/sshnptarball -f ./tools/multibuild/Dockerfile.package \ --platform ${{ matrix.platform }} -o type=tar,dest=bins.tar . mkdir tarballs tar -xvf bins.tar -C tarballs - if: ${{ ! inputs.main_build_only }} uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: - name: ${{ matrix.output-name }}-${{github.ref_name}}-${{github.run_number}}-${{github.run_attempt}} - path: ./packages/dart/tarballs/${{ matrix.output-name }}.tgz + name: + ${{ matrix.output-name + }}-${{github.ref_name}}-${{github.run_number}}-${{github.run_attempt}} + path: ./tarballs/${{ matrix.output-name }}.tgz if-no-files-found: error universal_sh: @@ -223,13 +247,12 @@ jobs: working-directory: ./packages/dart/sshnoports/bundles runs-on: ubuntu-latest steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 - with: - name: universal.ps1-${{github.ref_name}}-${{github.run_number}}-${{github.run_attempt}} - path: ./packages/dart/sshnoports/bundles/universal.ps1 - if-no-files-found: error - + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: universal.ps1-${{github.ref_name}}-${{github.run_number}}-${{github.run_attempt}} + path: ./packages/dart/sshnoports/bundles/universal.ps1 + if-no-files-found: error github-release: name: >- @@ -239,55 +262,54 @@ jobs: outputs: hashes: ${{ steps.hash.outputs.hashes }} permissions: - contents: write # Mandatory for making GitHub Releases - id-token: write # Mandatory for sigstore + contents: write # Mandatory for making GitHub Releases + id-token: write # Mandatory for sigstore attestations: write steps: - - name: Checkout pubspec.lock - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - with: - sparse-checkout: packages/dart/sshnoports/pubspec.lock - sparse-checkout-cone-mode: false - - name: Install Syft - uses: anchore/sbom-action/download-syft@61119d458adab75f756bc0b9e4bde25725f86a7a # v0.17.2 - - name: Download all the tarballs - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - with: - path: tarballs/ - - name: Generate SBOMs - run: | - syft scan file:./packages/dart/sshnoports/pubspec.lock \ - -o 'spdx-json=tarballs/dart_sshnoports_sbom.spdx.json' \ - -o 'cyclonedx-json=tarballs/dart_sshnoports_sbom.cyclonedx.json' - - name: Move packages for signing - run: | - cd tarballs - mv */*.sh . - mv */*.ps1 . - mv */*.tgz . - mv */*.zip . - rm -Rf -- */ - - name: Generate SHA256 checksums - working-directory: tarballs - run: sha256sum * > checksums.txt - - name: Upload artifacts to GitHub Release - env: - GITHUB_TOKEN: ${{ github.token }} - # Upload to GitHub Release using the `gh` CLI. - # `tarballs/` contains the built packages, and the - # Syft produced SBOMs - run: >- - gh release upload - '${{ github.ref_name }}' tarballs/** - --repo '${{ github.repository }}' - - id: hash - name: Pass artifact hashes for SLSA provenance - working-directory: tarballs - run: | - echo "hashes=$(cat checksums.txt | base64 -w0)" >> "$GITHUB_OUTPUT" - - uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2 - with: - subject-path: 'tarballs/**' + - name: Checkout pubspec.lock + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + sparse-checkout: packages/dart/sshnoports/pubspec.lock + sparse-checkout-cone-mode: false + - name: Install Syft + uses: anchore/sbom-action/download-syft@61119d458adab75f756bc0b9e4bde25725f86a7a # v0.17.2 + - name: Download all the tarballs + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + path: tarballs/ + - name: Generate SBOMs + run: | + syft scan file:./packages/dart/sshnoports/pubspec.lock \ + -o 'spdx-json=tarballs/dart_sshnoports_sbom.spdx.json' \ + -o 'cyclonedx-json=tarballs/dart_sshnoports_sbom.cyclonedx.json' + - name: Move packages for signing + run: | + cd tarballs + mv */*.sh . + mv */*.ps1 . + mv */*.tgz . + mv */*.zip . + rm -Rf -- */ + - name: Generate SHA256 checksums + working-directory: tarballs + run: sha256sum * > checksums.txt + - name: Upload artifacts to GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + # Upload to GitHub Release using the `gh` CLI. + # `tarballs/` contains the built packages, and the + # Syft produced SBOMs + run: >- + gh release upload '${{ github.ref_name }}' tarballs/** --repo '${{ + github.repository }}' + - id: hash + name: Pass artifact hashes for SLSA provenance + working-directory: tarballs + run: | + echo "hashes=$(cat checksums.txt | base64 -w0)" >> "$GITHUB_OUTPUT" + - uses: actions/attest-build-provenance@6149ea5740be74af77f260b9db67e633f6b0a9a1 # v1.4.2 + with: + subject-path: "tarballs/**" provenance: needs: [github-release] @@ -308,11 +330,11 @@ jobs: permissions: contents: write # Needed to delete workflow branch steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - with: - ref: multibuild-${{github.run_number}} - - name: Delete workflow branch - run: git push origin --delete multibuild-${{github.run_number}} + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + ref: multibuild-${{github.run_number}} + - name: Delete workflow branch + run: git push origin --delete multibuild-${{github.run_number}} notify_on_completion: needs: [github-release, cleanup] @@ -321,7 +343,9 @@ jobs: - name: Google Chat Notification uses: Co-qn/google-chat-notification@3691ccf4763537d6e544bc6cdcccc1965799d056 # v1 with: - name: SSH no ports binaries were built by GitHub Action ${{ github.run_number }} + name: + SSH no ports binaries were built by GitHub Action ${{ + github.run_number }} url: ${{ secrets.GOOGLE_CHAT_WEBHOOK }} status: ${{ job.status }} @@ -333,6 +357,8 @@ jobs: - name: Google Chat Notification uses: Co-qn/google-chat-notification@3691ccf4763537d6e544bc6cdcccc1965799d056 # v1 with: - name: SSH no ports binaries build by GitHub Action ${{ github.run_number }} + name: + SSH no ports binaries build by GitHub Action ${{ github.run_number + }} url: ${{ secrets.GOOGLE_CHAT_WEBHOOK }} status: failure diff --git a/apps/admin/admin_api/.gitignore b/apps/admin/admin_api/.gitignore new file mode 100644 index 000000000..3a8579040 --- /dev/null +++ b/apps/admin/admin_api/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/apps/admin/admin_api/CHANGELOG.md b/apps/admin/admin_api/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/apps/admin/admin_api/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/apps/admin/admin_api/README.md b/apps/admin/admin_api/README.md new file mode 100644 index 000000000..3816eca3a --- /dev/null +++ b/apps/admin/admin_api/README.md @@ -0,0 +1,2 @@ +A sample command-line application with an entrypoint in `bin/`, library code +in `lib/`, and example unit test in `test/`. diff --git a/apps/admin/admin_api/analysis_options.yaml b/apps/admin/admin_api/analysis_options.yaml new file mode 100644 index 000000000..ac2b7eb70 --- /dev/null +++ b/apps/admin/admin_api/analysis_options.yaml @@ -0,0 +1,16 @@ +# Defines a default set of lint rules enforced for +# projects at Google. For details and rationale, +# see https://pub.dev/packages/lints. +include: package:lints/recommended.yaml + +# For lint rules and documentation, see http://dart-lang.github.io/linter/lints. +# Uncomment to specify additional rules. +linter: + rules: + annotate_overrides: true + prefer_final_fields: true + camel_case_types : true + unnecessary_string_interpolations : true + await_only_futures : true + unawaited_futures: true + depend_on_referenced_packages : false diff --git a/apps/admin/admin_api/bin/np_admin.dart b/apps/admin/admin_api/bin/np_admin.dart new file mode 100644 index 000000000..26e1e3390 --- /dev/null +++ b/apps/admin/admin_api/bin/np_admin.dart @@ -0,0 +1,104 @@ +import 'dart:io'; + +import 'package:admin_api/src/expose_apis.dart' as expose; +import 'package:alfred/alfred.dart'; +import 'package:alfred/src/type_handlers/websocket_type_handler.dart'; +import 'package:at_cli_commons/at_cli_commons.dart'; +import 'package:noports_core/admin.dart'; + +void main(List args) async { + CLIBase cli = await CLIBase.fromCommandLineArgs(args); + final api = PolicyService.withAtClient(atClient: cli.atClient); + await api.init(); + + // await _createGroups(api); // useful for testing + + final app = Alfred(); + app.all('*', cors(origin: 'http://localhost:5173')); + if (Platform.executable.contains('admin_api')) { + // Production usage - we're using the compiled binary + final executableLocation = + (Platform.resolvedExecutable.split(Platform.pathSeparator) + ..removeLast()) + .join(Platform.pathSeparator); + final dir = Directory( + [executableLocation, 'web', 'admin'].join(Platform.pathSeparator)); + print ('Will serve webapp from $dir'); + app.get('/*', (req, res) => dir); + } else { + // TODO Maybe do something smarter here, but this is for dev purposes only + final dir = Directory('../../../apps/admin/webapp/dist'); + print ('Will serve webapp from ${dir.absolute}'); + app.get('/*', (req, res) => dir); + } + await expose.policy(app, '/api/policy', api); + + // Track connected clients + var users = []; + + // WebSocket chat relay implementation + app.get('/api/policy/events', (req, res) { + return WebSocketSession( + onOpen: (ws) { + users.add(ws); + }, + onClose: (ws) { + users.remove(ws); + }, + onMessage: (ws, dynamic data) async { + stderr.writeln('Received $data on the events websocket'); + }, + ); + }); + + api.eventStream.listen((s) { + for (final u in users) { + u.send(s); + } + }); + + await app.listen(); +} + +// ignore: unused_element +Future _createGroups(PolicyService api) async { + UserGroup sysAdmins = UserGroup( + name: 'SysAdmins', + description: 'System Administrators - full access', + userAtSigns: ['@alice'], + daemonAtSigns: ['@delta'], + devices: [ + Device(name: 'bastion1', permitOpens: ['*:*']) + ], + deviceGroups: [ + DeviceGroup( + name: 'atsign_staging_cloud', permitOpens: ['localhost:*', '*:22']) + ], + ); + + await api.createUserGroup(sysAdmins); + + UserGroup policyOwners = UserGroup( + name: 'PolicyOwners', + description: 'Policy Owners - can connect to policy API', + userAtSigns: ['@bob'], + daemonAtSigns: ['@delta'], + devices: [ + Device(name: 'bastion1', permitOpens: ['localhost:15001']) + ], + deviceGroups: [], + ); + await api.createUserGroup(policyOwners); + + UserGroup rdpUsers = UserGroup( + name: 'RdpUsers', + description: 'RDP Users - can connect to RDP ports on this network', + userAtSigns: ['@alice', '@bob', '@chuck'], + daemonAtSigns: ['@delta'], + devices: [ + Device(name: 'bastion1', permitOpens: ['*:3389']) + ], + deviceGroups: [], + ); + await api.createUserGroup(rdpUsers); +} diff --git a/apps/admin/admin_api/lib/src/expose_apis.dart b/apps/admin/admin_api/lib/src/expose_apis.dart new file mode 100644 index 000000000..6cf3b83a4 --- /dev/null +++ b/apps/admin/admin_api/lib/src/expose_apis.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:alfred/alfred.dart'; +import 'package:at_client/at_client.dart'; +import 'package:noports_core/admin.dart'; + +policy(Alfred app, String pathPrefix, PolicyService api) { + // policy log events + app.get('$pathPrefix/logs', (req, res) async { + stderr.writeln('Fetching policy log events'); + final now = DateTime.now(); + final r = jsonEncode(await api.getLogEvents( + from: now.subtract(Duration(hours: 24)).millisecondsSinceEpoch, + to: now.millisecondsSinceEpoch, + )); + stderr.writeln('Fetched policy log events'); + return r; + }); + + // all groups TODO add query parameters for search, pagination etc + app.get('$pathPrefix/group', (req, res) async { + stderr.writeln('Fetching all groups'); + final r = jsonEncode(await api.getUserGroups()); + stderr.writeln('Fetched all groups'); + return r; + }); + + // get by group ID + app.get('$pathPrefix/group/:id', (req, res) async { + final id = req.params['id'].toString(); + stderr.writeln('Fetching group $id'); + final g = await api.getUserGroup(id); + if (g == null) { + res.statusCode = 404; + await res.send('No group with id $id'); + return jsonEncode({}); + } + final r = jsonEncode(g); + stderr.writeln('Fetched group $id (${g.name})'); + return r; + }); + + // create new group + app.post('$pathPrefix/group', (req, res) async { + stderr.writeln('Creating new group'); + UserGroup ug; + try { + ug = UserGroup.fromJson((await req.body)! as Map); + } catch (_) { + throw IllegalArgumentException('Unable to construct User from this json'); + } + try { + await api.createUserGroup(ug); + stderr.writeln('Updated group ${ug.name}'); + return jsonEncode(ug); + } catch (e) { + res.statusCode = 400; + await res.send(e.toString()); + } + }); + + // update group + app.put('$pathPrefix/group/:id', (req, res) async { + final id = req.params['id'].toString(); + stderr.writeln('Updating group with ID $id'); + + UserGroup ug; + try { + ug = UserGroup.fromJson((await req.body)! as Map); + } catch (_) { + throw IllegalArgumentException('Unable to construct User from this json'); + } + if (ug.id != id) { + throw IllegalArgumentException('GroupID mis-match'); + } + try { + await api.updateUserGroup(ug); + stderr.writeln('Updated group ${ug.name}'); + return jsonEncode(ug); + } catch (e) { + res.statusCode = 400; + await res.send(e.toString()); + } + }); + + // delete group + app.delete('$pathPrefix/group/:id', (req, res) async { + final id = req.params['id'].toString(); + stderr.writeln('Updating group with ID $id'); + try { + await api.deleteUserGroup(id); + } catch (e) { + res.statusCode = 400; + await res.send(e.toString()); + } + }); + + app.printRoutes(); +} diff --git a/apps/admin/admin_api/pubspec.lock b/apps/admin/admin_api/pubspec.lock new file mode 100644 index 000000000..17c7899f0 --- /dev/null +++ b/apps/admin/admin_api/pubspec.lock @@ -0,0 +1,970 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" + url: "https://pub.dev" + source: hosted + version: "73.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + alfred: + dependency: "direct main" + description: + name: alfred + sha256: "61c74bfccd41447ddb7eb76e0e1204e1b1da0c099d441d00fa8d2b86de5415e3" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" + url: "https://pub.dev" + source: hosted + version: "6.8.0" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" + args: + dependency: "direct main" + description: + path: "." + ref: "gkc/show-aliases-in-usage" + resolved-ref: ece6d42302acc5bae7a4ba793440ccb0945d48f5 + url: "https://github.com/gkc/args" + source: git + version: "2.4.2" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "58082b3f0dca697204dbab0ef9ff208bfaea7767ea771076af9a343488428dda" + url: "https://pub.dev" + source: hosted + version: "1.5.3" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + at_auth: + dependency: transitive + description: + name: at_auth + sha256: "28f72f0fc26ec7f5f58d28fd29f964c9b2b35ecdc8dd4805ed7174851da2cbcc" + url: "https://pub.dev" + source: hosted + version: "2.0.5" + at_base2e15: + dependency: transitive + description: + name: at_base2e15 + sha256: "06ee6ffba9b3439f1c41f9bf0c01f579ce0a8b25f42da8c374ba3a14d721937f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + at_chops: + dependency: transitive + description: + name: at_chops + sha256: "825171a3132b3756119bd16b6fd1fa6257f74a64babaf13cae2d82d53b8c6be1" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + at_cli_commons: + dependency: "direct main" + description: + name: at_cli_commons + sha256: "23db4c959e5cefdc8dbcfb563172eeee1c3c42a16974cf2f6df5fa2d8b91747a" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + at_client: + dependency: "direct main" + description: + name: at_client + sha256: "76a24bbf17867b64a5e827bb33d7c5f3ca2edfd5766d0920211e068d30ae748f" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + at_commons: + dependency: transitive + description: + name: at_commons + sha256: "2d0490a0c5bcd43c6a37911d85b71c133767aec47abc65bd8ecb20c8caaddeab" + url: "https://pub.dev" + source: hosted + version: "4.0.11" + at_demo_data: + dependency: transitive + description: + name: at_demo_data + sha256: "0f59a24b83f0cd6d0e0557021511602ff167ece0ac69f12b8612c03263dff9ea" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + at_lookup: + dependency: transitive + description: + name: at_lookup + sha256: e989099d5f2cd6415097c8e4353340bd2048c9ee1bc82665f2b4f7c4615ad055 + url: "https://pub.dev" + source: hosted + version: "3.0.47" + at_onboarding_cli: + dependency: transitive + description: + name: at_onboarding_cli + sha256: fca7f5d96e83adf50057bccc6ebde3ec6562adfff99031894c781badf6daf623 + url: "https://pub.dev" + source: hosted + version: "1.6.2" + at_persistence_secondary_server: + dependency: transitive + description: + name: at_persistence_secondary_server + sha256: "1ec73b56e61b8aee94104ad4610c17cf07e366239337bedd43fa80c7765a391d" + url: "https://pub.dev" + source: hosted + version: "3.0.63" + at_persistence_spec: + dependency: transitive + description: + name: at_persistence_spec + sha256: ea8e550368ccee9150247ae7abdc256dccf78467bb48c11d1d0d66b843b21ba7 + url: "https://pub.dev" + source: hosted + version: "2.0.14" + at_server_status: + dependency: transitive + description: + name: at_server_status + sha256: "316c3e6717592677207d4f0a836b013271ca0f729e8b575c9195d19cfc57e71b" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + at_utf7: + dependency: transitive + description: + name: at_utf7 + sha256: c88e964e307bfe0e53e0048cff1ebf5ab60e23ceb4273f1ca664e724a9a5c5c9 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + at_utils: + dependency: transitive + description: + name: at_utils + sha256: ec28600e4eec321ee5e22be051109fa7b2e94590dc51d9f957730c2540beb681 + url: "https://pub.dev" + source: hosted + version: "3.0.16" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" + chalkdart: + dependency: transitive + description: + name: chalkdart + sha256: "0b7ec5c6a6bafd1445500632c00c573722bd7736e491675d4ac3fe560bbd9cfe" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + url: "https://pub.dev" + source: hosted + version: "1.19.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" + cron: + dependency: transitive + description: + name: cron + sha256: d98aa8cdad0cccdb6b098e6a1fb89339c180d8a229145fa4cd8c6fc538f0e35f + url: "https://pub.dev" + source: hosted + version: "0.5.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + url: "https://pub.dev" + source: hosted + version: "3.0.5" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + crypton: + dependency: transitive + description: + name: crypton + sha256: "17b6631fbf89e389d421b46629132287ed37d601b2ad1357445826ab85022271" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + dart_periphery: + dependency: transitive + description: + name: dart_periphery + sha256: "03fef538e07124346ca89e214c34e505e6ff2d2766d7927cac099bae53601113" + url: "https://pub.dev" + source: hosted + version: "0.9.6" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + dartssh2: + dependency: transitive + description: + name: dartssh2 + sha256: "9aa21bb23e4ce3b8133637162f8439af4796ee08c207c8c5e777b03c33ba7f10" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + ecdsa: + dependency: transitive + description: + name: ecdsa + sha256: b71687a843151255fced9fead63b09816cc59e9ae7b954e6a852bdc344ae1aca + url: "https://pub.dev" + source: hosted + version: "0.1.0" + elliptic: + dependency: transitive + description: + name: elliptic + sha256: "98e2fa89a714c649174553c823db2612dc9581814477fe1264a499d448237b6b" + url: "https://pub.dev" + source: hosted + version: "0.3.10" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + image: + dependency: transitive + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + internet_connection_checker: + dependency: transitive + description: + name: internet_connection_checker + sha256: "1c683e63e89c9ac66a40748b1b20889fd9804980da732bf2b58d6d5456c8e876" + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + meta: + dependency: "direct main" + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + mime_type: + dependency: transitive + description: + name: mime_type + sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb + url: "https://pub.dev" + source: hosted + version: "1.0.1" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + ninja_asn1: + dependency: transitive + description: + name: ninja_asn1 + sha256: b0f04877243fda51c475ec2bcaadb55a92759baee9f02888124c60775760ccf7 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + noports_core: + dependency: "direct main" + description: + path: "../../../packages/dart/noports_core" + relative: true + source: path + version: "6.1.0" + openssh_ed25519: + dependency: transitive + description: + name: openssh_ed25519 + sha256: eb65bfb9158c05c294d653f639bb9b4b18aca6dfd010986e4f325e439a93655f + url: "https://pub.dev" + source: hosted + version: "1.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + pinenacl: + dependency: transitive + description: + name: pinenacl + sha256: "57e907beaacbc3c024a098910b6240758e899674de07d6949a67b52fd984cbdf" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + url: "https://pub.dev" + source: hosted + version: "6.0.1" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + queue: + dependency: transitive + description: + name: queue + sha256: "9a41ecadc15db79010108c06eae229a45c56b18db699760f34e8c9ac9b831ff9" + url: "https://pub.dev" + source: hosted + version: "3.1.0+2" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + socket_connector: + dependency: transitive + description: + name: socket_connector + sha256: "3c641546699aa58e9ab8be9841627a30af3c1ffcf4461ca5d00d7c56392ab63a" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" + source: hosted + version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + url: "https://pub.dev" + source: hosted + version: "1.25.8" + test_api: + dependency: transitive + description: + name: test_api + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + url: "https://pub.dev" + source: hosted + version: "0.7.3" + test_core: + dependency: transitive + description: + name: test_core + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + url: "https://pub.dev" + source: hosted + version: "0.6.5" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + version: + dependency: transitive + description: + name: version + sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + zxing2: + dependency: transitive + description: + name: zxing2 + sha256: "6cf995abd3c86f01ba882968dedffa7bc130185e382f2300239d2e857fc7912c" + url: "https://pub.dev" + source: hosted + version: "0.2.3" +sdks: + dart: ">=3.5.0-259.0.dev <4.0.0" diff --git a/apps/admin/admin_api/pubspec.yaml b/apps/admin/admin_api/pubspec.yaml new file mode 100644 index 000000000..aee6bb103 --- /dev/null +++ b/apps/admin/admin_api/pubspec.yaml @@ -0,0 +1,32 @@ +name: admin_api +description: A sample command-line application. +version: 1.0.0 +publish_to: none +# repository: https://github.com/my_org/my_repo + +environment: + sdk: ^3.4.3 + +# Add regular dependencies here. +dependencies: + alfred: ^1.1.1 + at_client: ^3.1.0 + json_annotation: ^4.9.0 + at_cli_commons: ^1.1.0 + args: 2.5.0 + meta: ^1.15.0 + noports_core: + path: ../../../packages/dart/noports_core + +dependency_overrides: + args: + git: + ref: gkc/show-aliases-in-usage + url: https://github.com/gkc/args + +dev_dependencies: + lints: ^4.0.0 + test: ^1.25.8 + mocktail: ^1.0.4 + build_runner: ^2.4.12 + json_serializable: ^6.8.0 diff --git a/apps/admin/admin_api/test/policy_api_test.dart b/apps/admin/admin_api/test/policy_api_test.dart new file mode 100644 index 000000000..ea0160e7e --- /dev/null +++ b/apps/admin/admin_api/test/policy_api_test.dart @@ -0,0 +1,108 @@ +import 'package:noports_core/admin.dart'; +import 'package:at_client/at_client.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class MockAtClient extends Mock implements AtClient {} + +void main() { + group('core create retrieve update delete', () { + final api = PolicyService.inMemory(); + + setUp(() async { + expect(api.groups, isEmpty); + }); + + test('add group', () async { + String n = 'sysadmins'; + String d = 'Description'; + + UserGroup? ug = UserGroup.empty(name: n, description: d); + await api.createUserGroup(ug); + + expect(api.groups.containsKey(ug.id), true); + expect(api.groups[ug.id]!.name, n); + expect(api.groups[ug.id]!.description, d); + + ug = await api.getUserGroup(ug.id!); + expect(ug, isNotNull); + expect(ug!.name, n); + expect(ug.description, d); + expect(ug.daemonAtSigns, isEmpty); + expect(ug.devices, isEmpty); + expect(ug.deviceGroups, isEmpty); + expect(ug.userAtSigns, isEmpty); + }); + + test('update group', () async { + String n1 = 'sysadmins'; + String d1 = 'Description'; + String n2 = 'some other group'; + String d2 = 'some other group description'; + + var g1 = await api.createUserGroup( + UserGroup.empty(name: n1, description: d1), + ); + expect(api.groups.length, 1); + + var g2 = await api.createUserGroup( + UserGroup.empty(name: n2, description: d2), + ); + expect(api.groups.length, 2); + + g1 = (await api.getUserGroup(g1.id!))!; + expect(g1.name, n1); + expect(g1.description, d1); + + g2 = (await api.getUserGroup(g2.id!))!; + expect(g2.name, n2); + expect(g2.description, d2); + + await api.updateUserGroup( + UserGroup.empty( + id: g1.id, + name: n1, + description: 'Updated description', + ), + ); + expect(api.groups.length, 2); + + g1 = (await api.getUserGroup(g1.id!))!; + expect(g1.name, n1); + expect(g1.description, 'Updated description'); + + g2 = (await api.getUserGroup(g2.id!))!; + expect(g2.name, n2); + expect(g2.description, d2); + }); + + test('delete group', () async { + String n1 = 'sysadmins'; + String d1 = 'Description'; + String n2 = 'some other group'; + String d2 = 'some other group description'; + + var g1 = await api.createUserGroup( + UserGroup.empty(name: n1, description: d1), + ); + expect(api.groups.length, 1); + + var g2 = await api.createUserGroup( + UserGroup.empty(name: n2, description: d2), + ); + expect(api.groups.length, 2); + + expect(api.groups.keys.contains(g1.id!), true); + await api.deleteUserGroup(g1.id!); + expect(api.groups.length, 1); + expect(api.groups.keys.contains(g1.id!), false); + expect(api.groups.keys.contains(g2.id!), true); + }); + + tearDown(() async { + for (String gid in List.from(api.groups.keys)) { + await api.deleteUserGroup(gid); + } + }); + }); +} diff --git a/apps/admin/webapp/.gitignore b/apps/admin/webapp/.gitignore new file mode 100644 index 000000000..61cb0c2e4 --- /dev/null +++ b/apps/admin/webapp/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +.vite + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/apps/admin/webapp/.vscode/extensions.json b/apps/admin/webapp/.vscode/extensions.json new file mode 100644 index 000000000..bdef82015 --- /dev/null +++ b/apps/admin/webapp/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["svelte.svelte-vscode"] +} diff --git a/apps/admin/webapp/README.md b/apps/admin/webapp/README.md new file mode 100644 index 000000000..382941e05 --- /dev/null +++ b/apps/admin/webapp/README.md @@ -0,0 +1,47 @@ +# Svelte + Vite + +This template should help get you started developing with Svelte in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + +This template contains as little as possible to get started with Vite + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. + +**Why enable `checkJs` in the JS template?** + +It is likely that most cases of changing variable types in runtime are likely to be accidental, rather than deliberate. This provides advanced typechecking out of the box. Should you like to take advantage of the dynamically-typed nature of JavaScript, it is trivial to change the configuration. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/sveltejs/svelte-hmr/tree/master/packages/svelte-hmr#preservation-of-local-state). + +If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. + +```js +// store.js +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/apps/admin/webapp/index.html b/apps/admin/webapp/index.html new file mode 100644 index 000000000..e356a950e --- /dev/null +++ b/apps/admin/webapp/index.html @@ -0,0 +1,17 @@ + + + + + + + + + NoPorts Policy Manager + + +
+ + + diff --git a/apps/admin/webapp/jsconfig.json b/apps/admin/webapp/jsconfig.json new file mode 100644 index 000000000..5696a2de7 --- /dev/null +++ b/apps/admin/webapp/jsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "moduleResolution": "bundler", + "target": "ESNext", + "module": "ESNext", + /** + * svelte-preprocess cannot figure out whether you have + * a value or a type, so tell TypeScript to enforce using + * `import type` instead of `import` for Types. + */ + "verbatimModuleSyntax": true, + "isolatedModules": true, + "resolveJsonModule": true, + /** + * To have warnings / errors of the Svelte compiler at the + * correct position, enable source maps by default. + */ + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable this if you'd like to use dynamic types. + */ + "checkJs": true + }, + /** + * Use global.d.ts instead of compilerOptions.types + * to avoid limiting type declarations. + */ + "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/apps/admin/webapp/package-lock.json b/apps/admin/webapp/package-lock.json new file mode 100644 index 000000000..2453908f7 --- /dev/null +++ b/apps/admin/webapp/package-lock.json @@ -0,0 +1,1113 @@ +{ + "name": "webapp_proto", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "webapp_proto", + "version": "0.0.0", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.1.1", + "svelte": "^4.2.18", + "vite": "^5.4.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz", + "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz", + "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz", + "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz", + "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz", + "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz", + "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz", + "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz", + "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz", + "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz", + "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz", + "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz", + "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz", + "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz", + "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz", + "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz", + "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz", + "integrity": "sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.10", + "svelte-hmr": "^0.16.0", + "vitefu": "^0.2.5" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", + "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dev": true, + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz", + "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.21.0", + "@rollup/rollup-android-arm64": "4.21.0", + "@rollup/rollup-darwin-arm64": "4.21.0", + "@rollup/rollup-darwin-x64": "4.21.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.0", + "@rollup/rollup-linux-arm-musleabihf": "4.21.0", + "@rollup/rollup-linux-arm64-gnu": "4.21.0", + "@rollup/rollup-linux-arm64-musl": "4.21.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0", + "@rollup/rollup-linux-riscv64-gnu": "4.21.0", + "@rollup/rollup-linux-s390x-gnu": "4.21.0", + "@rollup/rollup-linux-x64-gnu": "4.21.0", + "@rollup/rollup-linux-x64-musl": "4.21.0", + "@rollup/rollup-win32-arm64-msvc": "4.21.0", + "@rollup/rollup-win32-ia32-msvc": "4.21.0", + "@rollup/rollup-win32-x64-msvc": "4.21.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "4.2.18", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.18.tgz", + "integrity": "sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/estree": "^1.0.1", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-hmr": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", + "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.1.tgz", + "integrity": "sha512-1oE6yuNXssjrZdblI9AfBbHCC41nnyoVoEZxQnID6yvQZAFBzxxkqoFLtHUMkYunL8hwOLEjgTuxpkRxvba3kA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.41", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + } + } +} diff --git a/apps/admin/webapp/package.json b/apps/admin/webapp/package.json new file mode 100644 index 000000000..1375d4ef1 --- /dev/null +++ b/apps/admin/webapp/package.json @@ -0,0 +1,16 @@ +{ + "name": "webapp_proto", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.1.1", + "svelte": "^4.2.18", + "vite": "^5.4.1" + } +} diff --git a/apps/admin/webapp/public/vite.svg b/apps/admin/webapp/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/apps/admin/webapp/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/webapp/src/App.svelte b/apps/admin/webapp/src/App.svelte new file mode 100644 index 000000000..4057c1bd3 --- /dev/null +++ b/apps/admin/webapp/src/App.svelte @@ -0,0 +1,22 @@ + + +
+

AtSign NoPorts Policy Manager

+ +
+ + + +
+
+ © 2024   + AtSign NoPorts +
+
+
diff --git a/apps/admin/webapp/src/app.css b/apps/admin/webapp/src/app.css new file mode 100644 index 000000000..794c84b4a --- /dev/null +++ b/apps/admin/webapp/src/app.css @@ -0,0 +1,15 @@ +.table td { + background-color: aliceblue; +} +.table th { + background-color: lightcyan; +} + +.selected { + font-weight: bold; + background-color: cadetblue !important; +} + +.container { + max-width: 90%; +} \ No newline at end of file diff --git a/apps/admin/webapp/src/assets/atsign.svg b/apps/admin/webapp/src/assets/atsign.svg new file mode 100644 index 000000000..121c733c6 --- /dev/null +++ b/apps/admin/webapp/src/assets/atsign.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/webapp/src/assets/noports.avif b/apps/admin/webapp/src/assets/noports.avif new file mode 100644 index 000000000..de30002dc Binary files /dev/null and b/apps/admin/webapp/src/assets/noports.avif differ diff --git a/apps/admin/webapp/src/assets/noports.jpg b/apps/admin/webapp/src/assets/noports.jpg new file mode 100644 index 000000000..78aa3c662 Binary files /dev/null and b/apps/admin/webapp/src/assets/noports.jpg differ diff --git a/apps/admin/webapp/src/assets/svelte.svg b/apps/admin/webapp/src/assets/svelte.svg new file mode 100644 index 000000000..c5e08481f --- /dev/null +++ b/apps/admin/webapp/src/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/admin/webapp/src/lib/Child.svelte b/apps/admin/webapp/src/lib/Child.svelte new file mode 100644 index 000000000..3bb3a1ee7 --- /dev/null +++ b/apps/admin/webapp/src/lib/Child.svelte @@ -0,0 +1,13 @@ + + +
+ Depth == {depth}: Value == {value} + +
+ +{#if depth < 10} + {value = v; valueChanged(value);}}> +{/if} \ No newline at end of file diff --git a/apps/admin/webapp/src/lib/InPlaceEdit.svelte b/apps/admin/webapp/src/lib/InPlaceEdit.svelte new file mode 100644 index 000000000..da3085128 --- /dev/null +++ b/apps/admin/webapp/src/lib/InPlaceEdit.svelte @@ -0,0 +1,60 @@ + + +{#if editing} +
+ +
+{:else} +
+ {value} +
+{/if} + + diff --git a/apps/admin/webapp/src/lib/PolicyUserGroups.svelte b/apps/admin/webapp/src/lib/PolicyUserGroups.svelte new file mode 100644 index 000000000..6c431551d --- /dev/null +++ b/apps/admin/webapp/src/lib/PolicyUserGroups.svelte @@ -0,0 +1,665 @@ + + +
+ {#if status !== ''} +

{status}

+ {:else} +

 

+ + {/if} +
+ +
+
+
+ {#key selectedGroupIndex} +
+

Roles

+ + + + + + + + + + {#each groups as group, i} + selectGroup(i, e)}> + + + + + {/each} + + + + + + +
NameDescription
+ + + updateGroup(group)} + /> + + updateGroup(group)} + /> +
+ +
+
+ {/key} +
+
+
+ {#key selectedGroupIndex} +
+ {#if selectedGroupIndex >= 0 && selectedGroupIndex < groups.length} +
+

Role: {groups[selectedGroupIndex].name}

+ + + + + + + + +
+ updateGroup(groups[selectedGroupIndex])} + /> +
+
+
+

Device AtSigns

+ + + + + + + + + + {#each groups[selectedGroupIndex].daemonAtSigns as daemonAtSign} + + + + + {/each} + + + + + +
atSign
+ + + updateGroup(groups[selectedGroupIndex])} + /> +
+ +
+
+
+

Devices

+ + + + + + + + + + + {#each groups[selectedGroupIndex].devices as device} + + + + + + {/each} + + + + + + +
NameAccess
+ + + updateGroup(groups[selectedGroupIndex])} + /> + + + + + + {#each device.permitOpens as po, i} + + + + + {/each} + + + + +
+ + + updateGroup(groups[selectedGroupIndex])} + /> +
+ +
+
+ +
+
+
+

Device Groups

+ + + + + + + + + + + {#each groups[selectedGroupIndex].deviceGroups as dg} + + + + + + {/each} + + + + + + +
NameAccess
+ + + updateGroup(groups[selectedGroupIndex])} + /> + + + + + + {#each dg.permitOpens as po, i} + + + + + {/each} + + + + +
+ + + updateGroup(groups[selectedGroupIndex])} + /> +
+ +
+
+ +
+
+
+

Users

+ + + + + + + + + + {#each groups[selectedGroupIndex].userAtSigns as userAtSign} + + + + + {/each} + + + + + +
atSign
+ + + updateGroup(groups[selectedGroupIndex])} + /> +
+ +
+
+
+
+ {/if} +
+ {/key} +
+
+ +
+ + +
+
+

Logs

+ + {#key events} + + + + + + + + + + + + + {#each events as eventData} + + + + + + + + + {/each} + +
TimestampTypeDaemonAtSignDeviceDeviceGroupDetails
+ {new Date(eventData.timestamp).toLocaleString('en-GB', {timeZoneName: 'short'})} + + {eventData.type} + + {eventData.daemon} + + {eventData.deviceName} + + {eventData.deviceGroupName} + + {detailsForEvent(eventData)} +
+ {/key} +
+
diff --git a/apps/admin/webapp/src/main.js b/apps/admin/webapp/src/main.js new file mode 100644 index 000000000..8a909a15a --- /dev/null +++ b/apps/admin/webapp/src/main.js @@ -0,0 +1,8 @@ +import './app.css' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app'), +}) + +export default app diff --git a/apps/admin/webapp/src/routes/+layout.js b/apps/admin/webapp/src/routes/+layout.js new file mode 100644 index 000000000..e69de29bb diff --git a/apps/admin/webapp/src/vite-env.d.ts b/apps/admin/webapp/src/vite-env.d.ts new file mode 100644 index 000000000..4078e7476 --- /dev/null +++ b/apps/admin/webapp/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/apps/admin/webapp/svelte.config.js b/apps/admin/webapp/svelte.config.js new file mode 100644 index 000000000..b0683fd24 --- /dev/null +++ b/apps/admin/webapp/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +} diff --git a/apps/admin/webapp/vite.config.js b/apps/admin/webapp/vite.config.js new file mode 100644 index 000000000..d70196943 --- /dev/null +++ b/apps/admin/webapp/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [svelte()], +}) diff --git a/packages/dart/noports_core/analysis_options.yaml b/packages/dart/noports_core/analysis_options.yaml index 66f6f13f3..4a91764dd 100644 --- a/packages/dart/noports_core/analysis_options.yaml +++ b/packages/dart/noports_core/analysis_options.yaml @@ -7,8 +7,12 @@ include: package:lints/recommended.yaml # Uncomment to specify additional rules. linter: rules: + annotate_overrides: true + prefer_final_fields: true + implicit_call_tearoffs: true camel_case_types : true unnecessary_string_interpolations : true await_only_futures : true unawaited_futures: true depend_on_referenced_packages : false + avoid_function_literals_in_foreach_calls: true diff --git a/packages/dart/noports_core/lib/admin.dart b/packages/dart/noports_core/lib/admin.dart new file mode 100644 index 000000000..639a1296c --- /dev/null +++ b/packages/dart/noports_core/lib/admin.dart @@ -0,0 +1,5 @@ +library noports_core_admin; + +export 'src/admin/models.dart'; +export 'src/admin/interface.dart'; +export 'src/admin/impl.dart'; diff --git a/packages/dart/noports_core/lib/npa.dart b/packages/dart/noports_core/lib/npa.dart index 224873b91..efa9a3e85 100644 --- a/packages/dart/noports_core/lib/npa.dart +++ b/packages/dart/noports_core/lib/npa.dart @@ -1,6 +1,7 @@ library noports_core_npa; export 'src/npa/npa.dart'; +export 'src/npa/npa_impl.dart'; export 'src/npa/npa_params.dart'; export 'src/npa/npa_rpcs.dart'; export 'src/common/types.dart'; diff --git a/packages/dart/noports_core/lib/src/admin/impl.dart b/packages/dart/noports_core/lib/src/admin/impl.dart new file mode 100644 index 000000000..bb89bc18c --- /dev/null +++ b/packages/dart/noports_core/lib/src/admin/impl.dart @@ -0,0 +1,261 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:at_client/at_client.dart'; +import 'package:at_utils/at_logger.dart'; +import 'package:noports_core/admin.dart'; +import 'package:noports_core/sshnp_foundation.dart'; + +class PolicyServiceWithAtClient extends PolicyServiceInMem with AtClientBindings { + @override + final logger = AtSignLogger('PolicyServiceWithAtClient'); + @override + final AtClient atClient; + + PolicyServiceWithAtClient({ + required this.atClient, + }); + + @override + Future init() async { + await super.init(); + + atClient.notificationService.subscribe( + regex: r'.*\.groups\.policy\.sshnp', + shouldDecrypt: true, + ).listen((AtNotification n) { + String groupId = n.key.split(':')[1].split('.').first; + logger.info('Received ${n.operation} notification for group ${n.key} - ID is $groupId'); + if (n.operation == 'delete') { + groups.remove(groupId); + } else { + UserGroup g = UserGroup.fromJson(jsonDecode(n.value!)); + groups[groupId] = g; + } + }); + + subscribe( + regex: r'.*\.logs\.policy\.sshnp', + shouldDecrypt: true, + ).listen((AtNotification n) { + logger.shout('Received policy log notification from ${jsonDecode(n.value!)['daemon']}'); + // TODO Make a PolicyLogEvent and use PolicyLogEvent.fromJson() + onPolicyLogEvent(n.value!); + }); + + subscribe( + regex: r'.*\.devices\.policy\.sshnp', + shouldDecrypt: true, + ).listen((AtNotification n) { + logger.shout('Received device heartbeat from ${n.from}'); + // TODO Make a PolicyLogEvent and use PolicyLogEvent.fromJson() + final v = jsonDecode(n.value!); + final e = {}; + e['timestamp'] = n.epochMillis; + e['daemon'] = n.from; + e['payload'] = v; + onDaemonEvent(jsonEncode(e)); + }); + + logger.shout('Loading groups via AtClient'); + // Fetch all the groups + List groupKeys = await atClient.getAtKeys( + regex: '.*.groups.policy.sshnp', + sharedBy: atClient.getCurrentAtSign()); + for (final AtKey groupKey in groupKeys) { + logger.shout('Loading group from atKey: $groupKey'); + final v = await atClient.get( + groupKey, + getRequestOptions: GetRequestOptions()..useRemoteAtServer = true, + ); + UserGroup g = UserGroup.fromJson(jsonDecode(v.value)); + logger.shout('Loaded $groupKey - group name is (${g.name})'); + groups[g.id!] = g; + } + logger.shout('Load complete'); + } + + String _groupKey(String id) { + return '$id.groups.policy.sshnp${atClient.getCurrentAtSign()!}'; + } + + AtKey _groupAtKey(String id) { + return AtKey.fromString(_groupKey(id)); + } + @override + Future createUserGroup(UserGroup group) async { + if (group.id != null) { + throw StateError('New groups must not already have an ID'); + } + group.id = '${_maxGroupId() + 1}'; + + await atClient.put( + _groupAtKey(group.id!), + jsonEncode(group), + putRequestOptions: PutRequestOptions()..useRemoteAtServer = true, + ); + await atClient.notificationService.notify( + NotificationParams.forUpdate( + AtKey.fromString('${atClient.getCurrentAtSign()}:${_groupAtKey(group.id!)}'), + value: jsonEncode(group), + ), + ); + groups[group.id!] = group; + return group; + } + + @override + Future updateUserGroup(UserGroup group) async { + if (group.id == null) { + throw StateError('Existing groups must already have an ID'); + } + await atClient.put( + _groupAtKey(group.id!), + jsonEncode(group), + putRequestOptions: PutRequestOptions()..useRemoteAtServer = true, + ); + await atClient.notificationService.notify( + NotificationParams.forUpdate( + AtKey.fromString('${atClient.getCurrentAtSign()}:${_groupAtKey(group.id!)}'), + value: jsonEncode(group), + ), + ); + groups[group.id!] = group; + } + + @override + Future deleteUserGroup(String id) async { + await atClient.delete(_groupAtKey(id), + deleteRequestOptions: DeleteRequestOptions()..useRemoteAtServer = true, + ); + await atClient.notificationService.notify( + NotificationParams.forDelete( + AtKey.fromString('${atClient.getCurrentAtSign()}:${_groupAtKey(id)}'), + ), + ); + return groups.remove(id) != null; + } +} + +class PolicyServiceInMem implements PolicyService { + @override + Future init() async { + + } + + @override + final Map groups = {}; + + @override + final List logEvents = []; + + int _maxGroupId() { + int i = 0; + for (final g in groups.values) { + int gid = int.parse(g.id!); + if (gid > i) { + i = gid; + } + } + return i; + } + + Future onDaemonEvent(json) async { + final de = jsonDecode(json); + final e = { + 'timestamp': de['timestamp'], + 'type': 'DaemonHeartbeat', + 'daemon': de['daemon'], + 'deviceName': de['payload']['devicename'], + 'deviceGroupName': de['payload']['deviceGroupName'], + }; + esc.add(jsonEncode(e)); + } + + Future onPolicyLogEvent(json) async { + final pe = jsonDecode(json); + logEvents.add(pe); + final e = { + 'timestamp': pe['timestamp'], + 'type': 'PolicyCheck', + 'daemon': pe['daemon'], + 'deviceName': pe['payload']['request']['payload']['daemonDeviceName'], + 'deviceGroupName': pe['payload']['request']['payload']['daemonDeviceGroupName'], + 'user': pe['payload']['request']['payload']['clientAtsign'], + 'authorized': pe['payload']['response']['payload']['authorized'], + 'message': pe['payload']['response']['payload']['message'], + 'permitOpen': pe['payload']['response']['payload']['permitOpen'], + }; + esc.add(jsonEncode(e)); + } + + StreamController esc = StreamController.broadcast(); + + @override + Stream get eventStream { + return esc.stream; + } + + @override + Future> getLogEvents( + {required int from, required int to}) async { + return List.from(logEvents.where((event) { + int ts = event['timestamp']; + return (ts >= from && ts <= to); + })); + } + + @override + Future getUserGroup(String id) async { + return groups[id]; + } + + @override + Future> getUserGroups() async => List.from(groups.values); + + @override + Future createUserGroup(UserGroup group) async { + if (group.id != null) { + throw StateError('New groups must not already have an ID'); + } + group.id = '${_maxGroupId() + 1}'; + + groups[group.id!] = group; + return group; + } + + @override + Future updateUserGroup(UserGroup group) async { + if (group.id == null) { + throw StateError('Existing groups must already have an ID'); + } + groups[group.id!] = group; + } + + @override + Future deleteUserGroup(String id) async { + return groups.remove(id) != null; + } + + @override + Future> getGroupsForUser(String atSign) async { + List l = []; + + for (String groupId in groups.keys) { + UserGroup g = groups[groupId]!; + if (g.userAtSigns.contains(atSign)) { + l.add(g); + } + } + return l; + } + + @override + Set get daemonAtSigns { + final Set s = {}; + for (final g in groups.values) { + s.addAll(g.daemonAtSigns); + } + return s; + } +} diff --git a/packages/dart/noports_core/lib/src/admin/interface.dart b/packages/dart/noports_core/lib/src/admin/interface.dart new file mode 100644 index 000000000..de0c5e42e --- /dev/null +++ b/packages/dart/noports_core/lib/src/admin/interface.dart @@ -0,0 +1,56 @@ +import 'package:at_client/at_client.dart'; +import 'package:meta/meta.dart'; +import 'package:noports_core/admin.dart'; + +abstract interface class PolicyService { + /// initialize the policy service once it's been created + Future init(); + + /// The in-memory groups map. Not for external use. + @visibleForTesting + Map get groups; + + /// The in-memory list of log events. Not for external use. + @visibleForTesting + List get logEvents; + + Stream get eventStream; + + // TODO Use a PolicyLogEvent + /// Fetch some log events + Future> getLogEvents({required int from, required int to}); + + /// Get (some of) the permission groups known to this policy service. + /// Method rather than getter, as we will add query parameters later + Future> getUserGroups(); + + /// Get a group object by its ID + Future getUserGroup(String id); + + /// Create a group. Must not already have an `id` + Future createUserGroup(UserGroup group); + + /// Update a group. Must already have an `id` + Future updateUserGroup(UserGroup group); + + /// Delete a group. + /// Return true if deleted, false if not. + Future deleteUserGroup(String id); + + /// Get the list of groups of which this user is a member. + Future> getGroupsForUser(String atSign); + + Set get daemonAtSigns; + + factory PolicyService.withAtClient({ + required AtClient atClient, + }) { + return PolicyServiceWithAtClient( + atClient: atClient, + ); + } + factory PolicyService.inMemory() { + return PolicyServiceInMem(); + } +} + diff --git a/packages/dart/noports_core/lib/src/admin/models.dart b/packages/dart/noports_core/lib/src/admin/models.dart new file mode 100644 index 000000000..c603f3b8e --- /dev/null +++ b/packages/dart/noports_core/lib/src/admin/models.dart @@ -0,0 +1,91 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'models.g.dart'; + +@JsonSerializable() +class Device { + final String name; + + final List permitOpens; + + Device({ + required this.name, + required this.permitOpens, + }); + + Map toJson() => _$DeviceToJson(this); + + static Device fromJson(Map json) => _$DeviceFromJson(json); +} + +@JsonSerializable() +class DeviceGroup { + final String name; + + List permitOpens; + + DeviceGroup({ + required this.name, + required this.permitOpens, + }); + + Map toJson() => _$DeviceGroupToJson(this); + + static DeviceGroup fromJson(Map json) => + _$DeviceGroupFromJson(json); +} + +@JsonSerializable() +class UserGroup { + // { + // "id":"xyz123", + // "name":"sysadmins", + // "userAtSigns":["@alice", ...], + // "daemonAtSigns":["@bob", ...], + // "devices":{ + // "name":"some_device_name", + // "permitOpens":["localhost:3000", ...] + // }, + // "deviceGroups":{ + // "name":"some_device_group_name", + // "permitOpens":["localhost:3000", ...] + // } + // } + String? id; + final String name; + final String description; + + final List daemonAtSigns; + + final List devices; + + final List deviceGroups; + + final List userAtSigns; + + factory UserGroup.empty({String? id, required String name, required String description}) { + return UserGroup( + id: id, + name: name, + description: description, + userAtSigns: [], + daemonAtSigns: [], + devices: [], + deviceGroups: []); + } + + UserGroup({ + this.id, + required this.name, + required this.description, + required this.userAtSigns, + required this.daemonAtSigns, + required this.devices, + required this.deviceGroups, + }); + + Map toJson() => _$UserGroupToJson(this); + + static UserGroup fromJson(Map json) => + _$UserGroupFromJson(json); +} diff --git a/packages/dart/noports_core/lib/src/admin/models.g.dart b/packages/dart/noports_core/lib/src/admin/models.g.dart new file mode 100644 index 000000000..fc6b18683 --- /dev/null +++ b/packages/dart/noports_core/lib/src/admin/models.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Device _$DeviceFromJson(Map json) => Device( + name: json['name'] as String, + permitOpens: (json['permitOpens'] as List) + .map((e) => e as String) + .toList(), + ); + +Map _$DeviceToJson(Device instance) => { + 'name': instance.name, + 'permitOpens': instance.permitOpens, + }; + +DeviceGroup _$DeviceGroupFromJson(Map json) => DeviceGroup( + name: json['name'] as String, + permitOpens: (json['permitOpens'] as List) + .map((e) => e as String) + .toList(), + ); + +Map _$DeviceGroupToJson(DeviceGroup instance) => + { + 'name': instance.name, + 'permitOpens': instance.permitOpens, + }; + +UserGroup _$UserGroupFromJson(Map json) => UserGroup( + id: json['id'] as String?, + name: json['name'] as String, + description: json['description'] as String, + userAtSigns: (json['userAtSigns'] as List) + .map((e) => e as String) + .toList(), + daemonAtSigns: (json['daemonAtSigns'] as List) + .map((e) => e as String) + .toList(), + devices: (json['devices'] as List) + .map((e) => Device.fromJson(e as Map)) + .toList(), + deviceGroups: (json['deviceGroups'] as List) + .map((e) => DeviceGroup.fromJson(e as Map)) + .toList(), + ); + +Map _$UserGroupToJson(UserGroup instance) => { + 'id': instance.id, + 'name': instance.name, + 'description': instance.description, + 'daemonAtSigns': instance.daemonAtSigns, + 'devices': instance.devices, + 'deviceGroups': instance.deviceGroups, + 'userAtSigns': instance.userAtSigns, + }; diff --git a/packages/dart/noports_core/lib/src/common/default_args.dart b/packages/dart/noports_core/lib/src/common/default_args.dart index fe4e5bb37..3086da642 100644 --- a/packages/dart/noports_core/lib/src/common/default_args.dart +++ b/packages/dart/noports_core/lib/src/common/default_args.dart @@ -48,4 +48,5 @@ class DefaultSshnpdArgs { static const int localSshdPort = 22; static const String deviceGroupName = '__none__'; static const String sshPublicKeyPermissions = ""; + static const Duration policyHeartbeatFrequency = Duration(minutes: 5); } diff --git a/packages/dart/noports_core/lib/src/common/io_types.dart b/packages/dart/noports_core/lib/src/common/io_types.dart index f5d01e756..912190b4d 100644 --- a/packages/dart/noports_core/lib/src/common/io_types.dart +++ b/packages/dart/noports_core/lib/src/common/io_types.dart @@ -1,5 +1,7 @@ /// This file contains all of the dart:io calls in noports_core /// All io used should be wrapped for the sake of testing and compatibility +library io_types; + import 'dart:io' show Process, ProcessResult, ProcessStartMode; import 'package:meta/meta.dart'; diff --git a/packages/dart/noports_core/lib/src/npa/npa.dart b/packages/dart/noports_core/lib/src/npa/npa.dart index 9ebd0a74b..0a68162bb 100644 --- a/packages/dart/noports_core/lib/src/npa/npa.dart +++ b/packages/dart/noports_core/lib/src/npa/npa.dart @@ -29,21 +29,27 @@ abstract class NPA implements AtRpcCallbacks { String get authorizerAtsign; - abstract Set daemonAtsigns; + String get loggingAtsign; - abstract NPARequestHandler handler; + Set get daemonAtsigns; - static Future fromCommandLineArgs(List args, - {required NPARequestHandler handler, - AtClient? atClient, - FutureOr Function(NPAParams)? atClientGenerator, - void Function(Object, StackTrace)? usageCallback}) async { + NPARequestHandler get handler; + + static Future fromCommandLineArgs( + List args, { + required NPARequestHandler handler, + AtClient? atClient, + FutureOr Function(NPAParams)? atClientGenerator, + void Function(Object, StackTrace)? usageCallback, + Set? daemonAtsigns, + }) async { return NPAImpl.fromCommandLineArgs( args, handler: handler, atClient: atClient, atClientGenerator: atClientGenerator, usageCallback: usageCallback, + daemonAtsigns: daemonAtsigns, ); } diff --git a/packages/dart/noports_core/lib/src/npa/npa_impl.dart b/packages/dart/noports_core/lib/src/npa/npa_impl.dart index ef84f0df0..0f23b2ce0 100644 --- a/packages/dart/noports_core/lib/src/npa/npa_impl.dart +++ b/packages/dart/noports_core/lib/src/npa/npa_impl.dart @@ -5,12 +5,11 @@ import 'dart:io'; import 'package:at_client/at_client.dart' hide StringBuffer; import 'package:at_utils/at_logger.dart'; import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; import 'package:noports_core/npa.dart'; +import 'package:noports_core/src/common/mixins/at_client_bindings.dart'; import 'package:noports_core/utils.dart'; -@protected -class NPAImpl implements NPA { +class NPAImpl with AtClientBindings implements NPA { @override final AtSignLogger logger = AtSignLogger(' sshnpa '); @@ -24,10 +23,13 @@ class NPAImpl implements NPA { String get authorizerAtsign => atClient.getCurrentAtSign()!; @override - Set daemonAtsigns; + String get loggingAtsign => atClient.getCurrentAtSign()!; @override - NPARequestHandler handler; + final Set daemonAtsigns; + + @override + final NPARequestHandler handler; static const JsonEncoder jsonPrettyPrinter = JsonEncoder.withIndent(' '); @@ -42,11 +44,14 @@ class NPAImpl implements NPA { logger.logger.level = Level.SHOUT; } - static Future fromCommandLineArgs(List args, - {required NPARequestHandler handler, - AtClient? atClient, - FutureOr Function(NPAParams)? atClientGenerator, - void Function(Object, StackTrace)? usageCallback}) async { + static Future fromCommandLineArgs( + List args, { + required NPARequestHandler handler, + AtClient? atClient, + FutureOr Function(NPAParams)? atClientGenerator, + void Function(Object, StackTrace)? usageCallback, + Set? daemonAtsigns, + }) async { try { var p = await NPAParams.fromArgs(args); @@ -69,7 +74,7 @@ class NPAImpl implements NPA { var sshnpa = NPAImpl( atClient: atClient, homeDirectory: p.homeDirectory, - daemonAtsigns: p.daemonAtsigns, + daemonAtsigns: daemonAtsigns ?? p.daemonAtsigns, handler: handler, ); @@ -103,24 +108,55 @@ class NPAImpl implements NPA { Future handleRequest(AtRpcReq request, String fromAtSign) async { logger.info('Received request from $fromAtSign: ' '${jsonPrettyPrinter.convert(request.toJson())}'); + // We will send a 'log' notification to the loggingAtsign + var logKey = AtKey() + ..key = '${DateTime.now().millisecondsSinceEpoch}.logs.policy' + ..sharedBy = authorizerAtsign + ..sharedWith = loggingAtsign + ..namespace = DefaultArgs.namespace + ..metadata = (Metadata() + ..isPublic = false + ..isEncrypted = true + ..namespaceAware = true); NPAAuthCheckRequest authCheckRequest = NPAAuthCheckRequest.fromJson(request.payload); + AtRpcResp rpcResponse; try { var authCheckResponse = await handler.doAuthCheck(authCheckRequest); - return AtRpcResp( + rpcResponse = AtRpcResp( reqId: request.reqId, respType: AtRpcRespType.success, payload: authCheckResponse.toJson()); } catch (e, st) { logger.shout('Exception: $e : StackTrace : \n$st'); - return AtRpcResp( + rpcResponse = AtRpcResp( reqId: request.reqId, respType: AtRpcRespType.success, - payload: - NPAAuthCheckResponse(authorized: false, message: 'Exception: $e') - .toJson()); + payload: NPAAuthCheckResponse( + authorized: false, + message: 'Exception: $e', + permitOpen: [], + ).toJson()); } + await notify( + logKey, + // TODO Make a PolicyLogEvent and use PolicyLogEvent.toJson() + jsonEncode( + { + 'daemon': fromAtSign, + 'timestamp': DateTime.now().millisecondsSinceEpoch, + 'payload': { + 'request': request, + 'response': rpcResponse, + } + }, + ), + checkForFinalDeliveryStatus: false, + waitForFinalDeliveryStatus: false, + ttln: Duration(hours: 1), + ); + return rpcResponse; } /// We're not sending any RPCs so we don't implement `handleResponse` diff --git a/packages/dart/noports_core/lib/src/npa/npa_params.dart b/packages/dart/noports_core/lib/src/npa/npa_params.dart index f1d79b92e..0f102622a 100644 --- a/packages/dart/noports_core/lib/src/npa/npa_params.dart +++ b/packages/dart/noports_core/lib/src/npa/npa_params.dart @@ -51,7 +51,8 @@ class NPAParams { parser.addOption( 'daemon-atsigns', - mandatory: true, + mandatory: false, + defaultsTo: '', help: 'Comma-separated list of daemon atSigns which use this authorizer', ); diff --git a/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart b/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart index 55709669a..f973fea17 100644 --- a/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart +++ b/packages/dart/noports_core/lib/src/npa/npa_rpcs.dart @@ -38,18 +38,26 @@ class NPAAuthCheckRequest { class NPAAuthCheckResponse { final bool authorized; final String? message; + final List permitOpen; - NPAAuthCheckResponse({required this.authorized, required this.message}); + NPAAuthCheckResponse({ + required this.authorized, + required this.message, + required this.permitOpen, + }); static NPAAuthCheckResponse fromJson(Map json) { return NPAAuthCheckResponse( - authorized: json['authorized'], - message: json['message'], - ); + authorized: json['authorized'], + message: json['message'], + permitOpen: List.from(json['permitOpen'])); } - Map toJson() => - {'authorized': authorized, 'message': message}; + Map toJson() => { + 'authorized': authorized, + 'message': message, + 'permitOpen': permitOpen, + }; @override String toString() => jsonPrettyPrinter.convert(toJson()); diff --git a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart index 4d47b22c8..b98a08ea9 100644 --- a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart +++ b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_impl.dart @@ -126,6 +126,7 @@ class SshnpdImpl implements Sshnpd { pingResponse = { 'devicename': device, + 'deviceGroupName': deviceGroup, 'version': version, 'corePackageVersion': packageVersion, 'supportedFeatures': { @@ -239,6 +240,13 @@ class SshnpdImpl implements Sshnpd { ); } + // If using a policy service, tell it we're here + await _sendHeartbeatToPolicy(); + Timer.periodic( + DefaultSshnpdArgs.policyHeartbeatFrequency, + (_) async => await _sendHeartbeatToPolicy(), + ); + logger.info('Done'); } @@ -268,14 +276,12 @@ class SshnpdImpl implements Sshnpd { /// Notification handler for sshnpd void _notificationHandler(AtNotification notification) async { - bool authed; - String message; - (authed, message) = await isFromAuthorizedAtsign(notification); - if (!authed) { + NPAAuthCheckResponse auth = await authCheck(notification); + if (!auth.authorized) { // TODO IF $someConditions apply then send a 'nice' error // TODO message notification back to the requester logger.shout('Notification ignored from ${notification.from}' - ' which is not authorized: $message' + ' which is not authorized: ${auth.message}' ' Notification value was ${notification.value}'); return; } @@ -321,40 +327,54 @@ class SshnpdImpl implements Sshnpd { case 'npt_request': logger.info('$notificationKey received from ${notification.from}' ' ( ${notification.value} )'); - _handleNptRequestNotification(notification); + _handleNptRequestNotification(notification, auth); break; } } - Future<(bool, String)> isFromAuthorizedAtsign( - AtNotification notification) async { + Future authCheck(AtNotification notification) async { const authTimeoutSeconds = 10; - late bool authed; - late String message; String client = notification.from; if (managerAtsigns.contains(client)) { - return (true, '$client is in --managers list'); + return NPAAuthCheckResponse( + authorized: true, + message: '$client is in --managers list', + permitOpen: ['*:*'], + ); + } + if (policyManagerAtsign == client) { + return NPAAuthCheckResponse( + authorized: true, + message: '$client is the policy manager', + permitOpen: ['*:*'], + ); } if (authChecker != null) { + late NPAAuthCheckResponse resp; try { logger.info('Asking $policyManagerAtsign' ' whether $client may connect to this daemon'); - NPAAuthCheckResponse resp = await authChecker! + resp = await authChecker! .mayConnect(clientAtsign: client) .timeout(const Duration(seconds: authTimeoutSeconds)); - authed = resp.authorized; - message = resp.message ?? ''; } on TimeoutException { - authed = false; - message = 'Timed out waiting for authorizer response'; + resp = NPAAuthCheckResponse( + authorized: false, + message: 'Timed out waiting for authorizer response', + permitOpen: [], + ); } - return (authed, message); + return resp; } - return (false, '$client is not in --managers list'); + return NPAAuthCheckResponse( + authorized: false, + message: '$client is not in --managers list', + permitOpen: [], + ); } void _handlePingNotification(AtNotification notification) { @@ -420,7 +440,10 @@ class SshnpdImpl implements Sshnpd { } } - void _handleNptRequestNotification(AtNotification notification) async { + void _handleNptRequestNotification( + AtNotification notification, + NPAAuthCheckResponse auth, + ) async { String requestingAtsign = notification.from; // Extract the NPT request payload. @@ -461,10 +484,8 @@ class SshnpdImpl implements Sshnpd { } String requested = '${req.requestedHost}:${req.requestedPort}'; - if (!(permitOpen.contains(requested) || - permitOpen.contains('*:${req.requestedPort}') || - permitOpen.contains('${req.requestedHost}:*') || - permitOpen.contains('*:*'))) { + // Check if this *daemon* allows connections to the requested host / port + if (!_permittedToOpen(permitOpen, req)) { // Notify noports client that this session is NOT connected await _notify( atKey: _createResponseAtKey( @@ -475,6 +496,20 @@ class SshnpdImpl implements Sshnpd { return; } + + // Check if this *client* is allowed connections to the requested host / port + if (!_permittedToOpen(auth.permitOpen, req)) { + // Notify noports client that this session is NOT connected + await _notify( + atKey: _createResponseAtKey( + requestingAtsign: requestingAtsign, sessionId: req.sessionId), + value: 'Client is not permitted connections to $requested', + sessionId: req.sessionId, + ); + + return; + } + // Start our side of the tunnel await startNpt( requestingAtsign: requestingAtsign, @@ -482,6 +517,15 @@ class SshnpdImpl implements Sshnpd { ); } + bool _permittedToOpen(List po, NptSessionRequest req) { + String requested = '${req.requestedHost}:${req.requestedPort}'; + // Check if this daemon allows connections to the requested host / port + return (po.contains(requested) || + po.contains('*:${req.requestedPort}') || + po.contains('${req.requestedHost}:*') || + po.contains('*:*')); + } + Future startNpt({ required String requestingAtsign, required NptSessionRequest req, @@ -1176,21 +1220,12 @@ class SshnpdImpl implements Sshnpd { if (makeDeviceInfoVisible) { try { logger.info('Sharing username $username with $managerAtsign'); - await atClient.notificationService.notify( - NotificationParams.forUpdate( - atKey, - value: username, - // notification can expire rapidly, the info is being cached - notificationExpiry: Duration(minutes: 1), - ), + await _notify( + atKey: atKey, + value: username, + ttln: Duration(minutes: 1), waitForFinalDeliveryStatus: false, checkForFinalDeliveryStatus: false, - onSuccess: (notification) { - logger.info('SUCCESS:$notification $username'); - }, - onError: (notification) { - logger.info('ERROR:$notification $username'); - }, ); } catch (e) { stderr.writeln(e.toString()); @@ -1259,6 +1294,29 @@ class SshnpdImpl implements Sshnpd { } } } + + /// If using a policy service, tell it we're here + Future _sendHeartbeatToPolicy() async { + if (policyManagerAtsign != null) { + var atKey = AtKey() + ..key = '$device.devices.policy' + ..sharedBy = deviceAtsign + ..sharedWith = policyManagerAtsign + ..namespace = DefaultArgs.namespace + ..metadata = (Metadata() + ..isPublic = false + ..isEncrypted = true + ..namespaceAware = true); + + logger.info('Sending heartbeat to policy service $policyManagerAtsign'); + /// send it + await _notify( + atKey: atKey, + value: jsonEncode(pingResponse), + ttln: DefaultSshnpdArgs.policyHeartbeatFrequency, + ); + } + } } abstract interface class AuthChecker { @@ -1301,8 +1359,8 @@ class _NPAAuthChecker implements AuthChecker, AtRpcCallbacks { authCheckCache[clientAtsign] = request.reqId; // To keep memory tidy, we'll clear this request and its cached response - // after 30 seconds - Future.delayed(Duration(seconds: 30), () { + // after 15 seconds + Future.delayed(Duration(seconds: 15), () { completerMap.remove(request.reqId); authCheckCache.remove(clientAtsign); }); @@ -1358,8 +1416,10 @@ class _NPAAuthChecker implements AuthChecker, AtRpcCallbacks { 'Got non-success auth check response from ${sshnpd.policyManagerAtsign}' ' : $response'); completer.complete(NPAAuthCheckResponse( - authorized: false, - message: response.message ?? 'Got non-success response $response')); + authorized: false, + message: response.message ?? 'Got non-success response $response', + permitOpen: [], + )); break; } } diff --git a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart index e04d57571..a7625ce10 100644 --- a/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart +++ b/packages/dart/noports_core/lib/src/sshnpd/sshnpd_params.dart @@ -239,6 +239,7 @@ class SshnpdParams { parser.addOption( 'device-group', + aliases: const ['dg'], mandatory: false, defaultsTo: DefaultSshnpdArgs.deviceGroupName, help: 'The name of this device\'s group. When delegated authorization' diff --git a/packages/dart/noports_core/pubspec.yaml b/packages/dart/noports_core/pubspec.yaml index acb6fbdd9..532354736 100644 --- a/packages/dart/noports_core/pubspec.yaml +++ b/packages/dart/noports_core/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: args: ^2.4.2 at_chops: ^2.0.0 - at_client: ^3.0.75 + at_client: ^3.1.0 at_commons: ^4.0.3 at_utils: ^3.0.16 cryptography: ^2.7.0 @@ -24,6 +24,7 @@ dependencies: socket_connector: ^2.2.0 uuid: ^3.0.7 mutex: ^3.1.0 + json_annotation: ^4.9.0 dependency_overrides: dartssh2: @@ -36,8 +37,9 @@ dependency_overrides: url: https://github.com/gkc/args dev_dependencies: - build_runner: ^2.4.6 + build_runner: ^2.4.12 + json_serializable: ^6.8.0 build_version: ^2.1.1 - lints: ^2.1.1 + lints: ^4.0.0 mocktail: ^1.0.1 test: ^1.24.4 diff --git a/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_channel_test.dart b/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_channel_test.dart index 2cf16c4d3..654508c67 100644 --- a/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_channel_test.dart +++ b/packages/dart/noports_core/test/sshnp/util/srvd_channel/srvd_channel_test.dart @@ -62,9 +62,9 @@ void main() { atClient: mockAtClient, params: mockParams, sessionId: sessionId, - srvGenerator: srvGeneratorStub, - notify: notifyStub, - subscribe: subscribeStub, + srvGenerator: srvGeneratorStub.call, + notify: notifyStub.call, + subscribe: subscribeStub.call, ); registerFallbackValue(AtKey()); diff --git a/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart b/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart index e2f86c9c7..309c76ebc 100644 --- a/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart +++ b/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_channel_test.dart @@ -59,9 +59,9 @@ void main() { params: mockParams, sessionId: sessionId, namespace: namespace, - notify: notifyStub, - subscribe: subscribeStub, - handleSshnpdPayload: payloadStub, + notify: notifyStub.call, + subscribe: subscribeStub.call, + handleSshnpdPayload: payloadStub.call, ); registerFallbackValue(AtKey()); diff --git a/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart b/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart index f92c5321c..ea9e6af87 100644 --- a/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart +++ b/packages/dart/noports_core/test/sshnp/util/sshnpd_channel/sshnpd_default_channel_test.dart @@ -50,7 +50,7 @@ void main() { params: mockParams, sessionId: sessionId, namespace: namespace, - subscribe: subscribeStub, + subscribe: subscribeStub.call, ); registerFallbackValue(AtKey()); diff --git a/packages/dart/sshnoports/analysis_options.yaml b/packages/dart/sshnoports/analysis_options.yaml index 66f6f13f3..4a91764dd 100644 --- a/packages/dart/sshnoports/analysis_options.yaml +++ b/packages/dart/sshnoports/analysis_options.yaml @@ -7,8 +7,12 @@ include: package:lints/recommended.yaml # Uncomment to specify additional rules. linter: rules: + annotate_overrides: true + prefer_final_fields: true + implicit_call_tearoffs: true camel_case_types : true unnecessary_string_interpolations : true await_only_futures : true unawaited_futures: true depend_on_referenced_packages : false + avoid_function_literals_in_foreach_calls: true diff --git a/packages/dart/sshnoports/bin/.gitignore b/packages/dart/sshnoports/bin/.gitignore index 6bb8f1b72..16e3ff860 100644 --- a/packages/dart/sshnoports/bin/.gitignore +++ b/packages/dart/sshnoports/bin/.gitignore @@ -2,4 +2,5 @@ * # Allow dart files !*.dart +!*.yaml !.gitignore diff --git a/packages/dart/sshnoports/bin/demo/npa_always_deny.dart b/packages/dart/sshnoports/bin/demo/npa_always_deny.dart index 4a58899db..f33547f2a 100644 --- a/packages/dart/sshnoports/bin/demo/npa_always_deny.dart +++ b/packages/dart/sshnoports/bin/demo/npa_always_deny.dart @@ -11,6 +11,9 @@ class AlwaysDeny implements NPARequestHandler { Future doAuthCheck( NPAAuthCheckRequest authCheckRequest) async { return NPAAuthCheckResponse( - authorized: false, message: 'Computer says "Noooo..."'); + authorized: false, + message: 'Computer says "Noooo..."', + permitOpen: [], + ); } } diff --git a/packages/dart/sshnoports/bin/demo/npa_cli.dart b/packages/dart/sshnoports/bin/demo/npa_cli.dart index 4c7f2a05b..5d22c1728 100644 --- a/packages/dart/sshnoports/bin/demo/npa_cli.dart +++ b/packages/dart/sshnoports/bin/demo/npa_cli.dart @@ -18,9 +18,18 @@ class CLI implements NPARequestHandler { decision = stdin.readLineSync()!; } final bool authorized = decision.toLowerCase().startsWith('a'); - return NPAAuthCheckResponse( - authorized: authorized, - message: authorized ? 'Approved via CLI' : 'Denied via CLI', - ); + if (authorized) { + return NPAAuthCheckResponse( + authorized: true, + message: 'Approved via CLI', + permitOpen: ['*:*'], + ); + } else { + return NPAAuthCheckResponse( + authorized: false, + message: 'Denied via CLI', + permitOpen: [], + ); + } } } diff --git a/packages/dart/sshnoports/bin/npp_atserver.dart b/packages/dart/sshnoports/bin/npp_atserver.dart new file mode 100644 index 000000000..b0c5920c3 --- /dev/null +++ b/packages/dart/sshnoports/bin/npp_atserver.dart @@ -0,0 +1,156 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:at_client/at_client.dart'; +import 'package:at_cli_commons/at_cli_commons.dart'; +import 'package:at_utils/at_logger.dart'; +import 'package:logging/logging.dart'; +import 'package:noports_core/admin.dart'; +import 'package:noports_core/npa.dart'; +import 'package:noports_core/sshnp_foundation.dart'; +import 'package:sshnoports/src/create_at_client_cli.dart'; + +late AtSignLogger logger; +void main(List args) async { + var p = await NPAParams.fromArgs(args); + + // Check atKeyFile selected exists + if (!await File(p.atKeysFilePath).exists()) { + throw ('\n Unable to find .atKeys file : ${p.atKeysFilePath}'); + } + + AtSignLogger.root_level = 'SHOUT'; + if (p.verbose) { + AtSignLogger.root_level = 'INFO'; + } + AtSignLogger.defaultLoggingHandler = AtSignLogger.stdErrLoggingHandler; + + + logger = AtSignLogger(' npp '); + AtClient atClient = await createAtClientCli( + atsign: p.authorizerAtsign, + atKeysFilePath: p.atKeysFilePath, + rootDomain: p.rootDomain, + atServiceFactory: ServiceFactoryWithNoOpSyncService(), + namespace: DefaultArgs.namespace, + storagePath: standardAtClientStoragePath( + homeDirectory: p.homeDirectory, + atSign: p.authorizerAtsign, + progName: '.${DefaultArgs.namespace}', + uniqueID: 'single'), + ); + + Handler handler = Handler(atClient); + await handler.init(); + + logger.shout('Daemon atSigns: ${handler.daemonAtSigns}'); + var sshnpa = NPAImpl( + atClient: atClient, + homeDirectory: p.homeDirectory, + daemonAtsigns: handler.daemonAtSigns, + handler: handler, + ); + + if (p.verbose) { + sshnpa.logger.logger.level = Level.INFO; + } + + Set notifiedDaemonAtSigns = {}; + + atClient.notificationService.subscribe( + regex: r'.*\.devices\.policy\.sshnp', + shouldDecrypt: true, + ).listen((AtNotification n) { + notifiedDaemonAtSigns.add(n.from); + sshnpa.daemonAtsigns.clear(); + sshnpa.daemonAtsigns.addAll(handler.api.daemonAtSigns); + sshnpa.daemonAtsigns.addAll(notifiedDaemonAtSigns); + logger.info('daemonAtSigns is now ${sshnpa.daemonAtsigns}'); + }); + + atClient.notificationService.subscribe( + regex: r'.*\.groups\.policy\.sshnp', + shouldDecrypt: true, + ).listen((AtNotification n) { + sshnpa.daemonAtsigns.clear(); + sshnpa.daemonAtsigns.addAll(handler.api.daemonAtSigns); + sshnpa.daemonAtsigns.addAll(notifiedDaemonAtSigns); + logger.info('daemonAtSigns is now ${sshnpa.daemonAtsigns}'); + }); + + await sshnpa.run(); +} + +class Handler implements NPARequestHandler { + final AtClient atClient; + late final PolicyServiceWithAtClient api; + + Handler(this.atClient) { + api = PolicyServiceWithAtClient(atClient: atClient); + } + + Future init() async { + await api.init(); + } + + Set get daemonAtSigns => api.daemonAtSigns; + + @override + Future doAuthCheck( + NPAAuthCheckRequest authCheckRequest) async { + + logger.info('Checking policy for request: $authCheckRequest'); + // member of any groups? + final groups = await api.getGroupsForUser(authCheckRequest.clientAtsign); + if (groups.isEmpty) { + return NPAAuthCheckResponse( + authorized: false, + message: 'No permissions for ${authCheckRequest.clientAtsign}', + permitOpen: [], + ); + } + + // OK - user is in some groups. What's it permitted to talk to? + Set permitOpens = {}; + + // for each group + // does it contain the authCheckRequest.daemonAtsign? + for (final group in groups) { + if (group.daemonAtSigns.contains(authCheckRequest.daemonAtsign)) { + + // does it contain a matching deviceName? if so, add the permitOpens + for (final d in group.devices) { + if (d.name == authCheckRequest.daemonDeviceName) { + permitOpens.addAll(d.permitOpens); + } + } + // or a matching deviceGroupName? if so, add the permitOpens + for (final dg in group.deviceGroups) { + if (dg.name == authCheckRequest.daemonDeviceGroupName) { + permitOpens.addAll(dg.permitOpens); + } + } + + } + } + + if (permitOpens.isNotEmpty) { + return NPAAuthCheckResponse( + authorized: true, + message: '${authCheckRequest.clientAtsign} has permission' + ' for device ${authCheckRequest.daemonDeviceName}' + ' and/or device group ${authCheckRequest.daemonDeviceGroupName}' + ' at daemon ${authCheckRequest.daemonAtsign}', + permitOpen: List.from(permitOpens), + ); + } else { + return NPAAuthCheckResponse( + authorized: false, + message: 'No permissions for ${authCheckRequest.clientAtsign}' + ' at ${authCheckRequest.daemonAtsign}' + ' for either the device ${authCheckRequest.daemonDeviceName}' + ' or the deviceGroup ${authCheckRequest.daemonDeviceGroupName}', + permitOpen: [], + ); + } + } +} diff --git a/packages/dart/sshnoports/bin/npp_file.dart b/packages/dart/sshnoports/bin/npp_file.dart new file mode 100644 index 000000000..44293bf0f --- /dev/null +++ b/packages/dart/sshnoports/bin/npp_file.dart @@ -0,0 +1,231 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:args/args.dart'; +import 'package:noports_core/npa.dart'; +import 'package:sshnoports/npa_bootstrapper.dart' as bootstrapper; +import 'package:yaml/yaml.dart'; + +void main(List args) async { + ArgParser parser = NPAParams.parser; + parser.addOption( + 'yaml', + mandatory: true, + help: 'Path to policy yaml', + ); + ArgResults r = parser.parse(args); + + YamlMap? yaml = loadYaml(File(r['yaml']).readAsStringSync()); + + FileBasedPolicy policy = FileBasedPolicy(yaml!); + await bootstrapper.run( + policy, + args, + daemonAtsigns: policy.daemonAtsigns, + ); +} + +/// - Client atSigns request access to a $deviceName at some $daemonAtSign for $someReason +/// - Daemons run with a deviceName and a deviceGroup +/// - Daemons send the daemonAtsign, clientAtSign, deviceName and deviceGroup to policy service +/// - The policy service needs to check if the clientAtsign is +/// - 1. permitted to talk to this daemonAtsign +/// - --> is there a value at permissions[@client]['daemons'][@daemon] +/// - and +/// - 2a. permitted to talk to this daemon's deviceName +/// - --> is there a value at permissions[@client]['daemons'][@daemon]['deviceNames'][$deviceName] +/// - or +/// - 2b. permitted to talk to this daemon's deviceGroupName +/// - --> is there a value at permissions[@client]['daemons'][@daemon]['deviceGroupNames'][$deviceGroupName] +/// +/// - The value at 2a or 2b will be a list of permitOpens (hostMask:portMask) +/// +/// Uses a policy.yaml file like the following: +/// userGroups: +/// "api_users": +/// userAtSigns: +/// - "@alice" +/// - "@bob" +/// - "@chuck" +/// - "@derek" +/// permissions: +/// daemonAtSigns: +/// - "@zaphod" +/// - "@dentarthurdent" +/// deviceNames: +/// "h2g2": +/// - "localhost:3000" +/// "rdp_users": +/// userAtSigns: +/// - "@charlie" +/// - "@filip" +/// - "@dipak" +/// permissions: +/// daemonAtSigns: +/// - "@zaphod" +/// - "@dentarthurdent" +/// deviceNames: +/// deviceGroupNames: +/// "network_name_123": +/// - "*:3389" +/// "ssh_users": +/// userAtSigns: +/// - "@bob" +/// permissions: +/// daemonAtSigns: +/// - "@zaphod" +/// - "@dentarthurdent" +/// deviceGroupNames: +/// deviceNames: +/// "h2g2": +/// - "*:22" +class FileBasedPolicy implements NPARequestHandler { + YamlMap yaml; + + final Set _daemonAtSigns = {}; + final Map _userAtSigns = {}; + + FileBasedPolicy(this.yaml) { + // Get the full list of daemonAtSigns which this policy service will listen to + for (String userGroupName in yaml['userGroups'].keys ?? []) { + for (String daemonAtSign in yaml['userGroups'][userGroupName] + ['permissions']['daemonAtSigns'] ?? + []) { + _daemonAtSigns.add(daemonAtSign); + } + } + print(_daemonAtSigns); + + // Create a map of userAtSign->daemonAtSign->deviceNames/deviceGroupNames->[PermitOpens] + for (String userGroupName in yaml['userGroups'].keys ?? []) { + final group = yaml['userGroups'][userGroupName]; + for (String userAtSign in group['userAtSigns']) { + _userAtSigns.putIfAbsent( + userAtSign, () => {'userGroups': [], 'daemons': {}}); + ((_userAtSigns[userAtSign] as Map)['userGroups'] as List) + .add(userGroupName); + Map userDaemonPermissions = + (_userAtSigns[userAtSign] as Map)['daemons'] as Map; + for (String daemonAtSign + in group['permissions']['daemonAtSigns'] ?? []) { + userDaemonPermissions.putIfAbsent( + daemonAtSign, () => {'deviceNames': {}, 'deviceGroupNames': {}}); + + for (String deviceName + in (group['permissions']['deviceNames'] ?? {}).keys) { + Map devicesMap = + userDaemonPermissions[daemonAtSign]['deviceNames'] as Map; + devicesMap.putIfAbsent(deviceName, () => []); + final devicePermissions = devicesMap[deviceName] as List; + for (String permitOpen in List.from( + group['permissions']['deviceNames'][deviceName] ?? [])) { + if (!devicePermissions.contains(permitOpen)) { + devicePermissions.add(permitOpen); + } + } + } + + for (String deviceGroupName + in (group['permissions']['deviceGroupNames'] ?? {}).keys) { + Map deviceGroupsMap = + userDaemonPermissions[daemonAtSign]['deviceGroupNames'] as Map; + deviceGroupsMap.putIfAbsent(deviceGroupName, () => []); + final deviceGroupPermissions = + deviceGroupsMap[deviceGroupName] as List; + for (String permitOpen in List.from(group['permissions'] + ['deviceGroupNames'][deviceGroupName] ?? + [])) { + if (!deviceGroupPermissions.contains(permitOpen)) { + deviceGroupPermissions.add(permitOpen); + } + } + } + } + } + } + for (final u in _userAtSigns.keys) { + final user = _userAtSigns[u] as Map; + print('$u is member of groups ${user['userGroups']}'); + for (final d in user['daemons'].keys) { + print(' daemon: $d'); + var daemon = user['daemons'][d]; + for (final dn in (daemon['deviceNames']).keys) { + print(' device $dn: ${daemon['deviceNames'][dn]}'); + } + for (final dgn in (daemon['deviceGroupNames']).keys) { + print(' deviceGroup $dgn: ${daemon['deviceGroupNames'][dgn]}'); + } + } + } + } + + Set get daemonAtsigns => _daemonAtSigns; + + @override + Future doAuthCheck( + NPAAuthCheckRequest authCheckRequest) async { + /// - The policy service needs to check if the clientAtsign is + /// - 1. permitted to talk to this daemonAtsign + /// - --> is there a value at _userAtSigns[@client][@daemon] + final clientEntry = _userAtSigns[authCheckRequest.clientAtsign]; + if (clientEntry == null) { + return NPAAuthCheckResponse( + authorized: false, + message: 'No permissions for ${authCheckRequest.clientAtsign}', + permitOpen: [], + ); + } + final daemonEntry = clientEntry['daemons'][authCheckRequest.daemonAtsign]; + if (daemonEntry == null) { + return NPAAuthCheckResponse( + authorized: false, + message: 'No permissions for ${authCheckRequest.clientAtsign}' + ' at ${authCheckRequest.daemonAtsign}', + permitOpen: [], + ); + } + + /// - and + /// - 2a. permitted to talk to this daemon's deviceName + /// - --> is there a value at permissions[@client][@daemon]['deviceNames'][$deviceName] + final deviceNames = daemonEntry['deviceNames']; + if (deviceNames != null) { + final deviceNameEntry = deviceNames[authCheckRequest.daemonDeviceName]; + if (deviceNameEntry != null) { + return NPAAuthCheckResponse( + authorized: true, + message: '${authCheckRequest.clientAtsign} has permission' + ' for device ${authCheckRequest.daemonDeviceName}' + ' at daemon ${authCheckRequest.daemonAtsign}', + permitOpen: List.from(deviceNameEntry), + ); + } + } + + /// - or + /// - 2b. permitted to talk to this daemon's deviceGroupName + /// - --> is there a value at permissions[@client][@daemon]['deviceGroupNames'][$deviceGroupName] + final deviceGroupNames = daemonEntry['deviceGroupNames']; + if (deviceGroupNames != null) { + final deviceGroupNameEntry = + deviceGroupNames[authCheckRequest.daemonDeviceGroupName]; + if (deviceGroupNameEntry != null) { + return NPAAuthCheckResponse( + authorized: true, + message: '${authCheckRequest.clientAtsign} has permission' + ' for device group ${authCheckRequest.daemonDeviceGroupName}' + ' at daemon ${authCheckRequest.daemonAtsign}', + permitOpen: List.from(deviceGroupNameEntry), + ); + } + } + + return NPAAuthCheckResponse( + authorized: false, + message: 'No permissions for ${authCheckRequest.clientAtsign}' + ' at ${authCheckRequest.daemonAtsign}' + ' for either the device ${authCheckRequest.daemonDeviceName}' + ' or the deviceGroup ${authCheckRequest.daemonDeviceGroupName}', + permitOpen: [], + ); + } +} diff --git a/packages/dart/sshnoports/buildArchive b/packages/dart/sshnoports/buildArchive index 927719421..37d714ee1 100755 --- a/packages/dart/sshnoports/buildArchive +++ b/packages/dart/sshnoports/buildArchive @@ -8,9 +8,12 @@ packageDir=$(pwd) echo "$(date) : Starting compilation" echo -rm -rf build/sshnp -mkdir -p build/sshnp +rm -rf build/sshnp build/sshnp.zip build/sshnp.tgz +mkdir -p build/sshnp/web/admin +dart pub get || exit 1 + +echo "Building core binaries" echo "Compiling at_activate"; dart compile exe --verbosity error bin/activate_cli.dart -o build/sshnp/at_activate & echo "Compiling srv"; dart compile exe --verbosity error bin/srv.dart -o build/sshnp/srv & echo "Compiling sshnpd"; dart compile exe --verbosity error bin/sshnpd.dart -o build/sshnp/sshnpd & @@ -19,16 +22,39 @@ echo "Compiling sshnp"; dart compile exe --verbosity error bin/sshnp.dart -o bui echo "Compiling npt"; dart compile exe --verbosity error bin/npt.dart -o build/sshnp/npt & wait +echo "$(date) : Compilation complete" -echo +echo "Compiling policy binaries" +echo "Compiling npp_file.dart to npp_file"; dart compile exe --verbosity error bin/npp_file.dart -o build/sshnp/npp_file & +echo "Compiling npp_atserver (BETA)"; dart compile exe --verbosity error bin/npp_atserver.dart -o build/sshnp/npp_atserver & + +wait echo "$(date) : Compilation complete" +echo "Building admin API and webapp - BETA"; +pushd ../../../apps/admin/admin_api || exit 1 +dart pub get || exit 1 +echo "Compiling admin_api"; dart compile exe --verbosity error bin/np_admin.dart -o "${packageDir}/build/sshnp/np_admin" || exit 1 +wait +cd ../webapp || exit 1 +echo "Building admin webapp" +npm install || exit 1 +npm run build || exit 1 + +wait +echo +echo "$(date) : Build complete" + +popd echo "$(date) : Copying bundles" cp -r bundles/core/* build/sshnp/ cp -r bundles/shell/* build/sshnp/ cp LICENSE build/sshnp +echo "$(date) : Copying webapp files - BETA" +cp -r ../../../apps/admin/webapp/dist/* build/sshnp/web/admin/ + cd build case "$(uname)" in @@ -36,6 +62,9 @@ case "$(uname)" in echo "$(date) : Creating zip" ditto -c -k --keepParent sshnp sshnp.zip echo "$(date) : Created $packageDir/build/sshnp.zip" + echo "$(date) : Creating tgz" + tar -cvzf sshnp.tgz sshnp + echo "$(date) : Created $packageDir/build/sshnp.tgz" ;; Linux) echo "$(date) : Creating tgz" diff --git a/packages/dart/sshnoports/buildBinaries b/packages/dart/sshnoports/buildBinaries index 4aae01160..2840b9d00 100755 --- a/packages/dart/sshnoports/buildBinaries +++ b/packages/dart/sshnoports/buildBinaries @@ -9,6 +9,8 @@ echo "Compiling sshnpd"; dart compile exe --verbosity error bin/sshnpd.dart -o b echo "Compiling srvd"; dart compile exe --verbosity error bin/srvd.dart -o bin/srvd & echo "Compiling sshnp"; dart compile exe --verbosity error bin/sshnp.dart -o bin/sshnp & echo "Compiling npt"; dart compile exe --verbosity error bin/npt.dart -o bin/npt & +echo "Compiling npp_file"; dart compile exe --verbosity error bin/npp_file.dart -o bin/npp_file & +echo "Compiling npp_atserver"; dart compile exe --verbosity error bin/npp_atserver.dart -o bin/npp_atserver & echo "Compiling demo/npa_always_deny"; dart compile exe --verbosity error bin/demo/npa_always_deny.dart -o bin/demo/npa_always_deny & echo "Compiling demo/npa_cli"; dart compile exe --verbosity error bin/demo/npa_cli.dart -o bin/demo/npa_cli & diff --git a/packages/dart/sshnoports/bundles/shell/headless/sshnpd.sh b/packages/dart/sshnoports/bundles/shell/headless/sshnpd.sh index 58b2eb34d..915396c0c 100755 --- a/packages/dart/sshnoports/bundles/shell/headless/sshnpd.sh +++ b/packages/dart/sshnoports/bundles/shell/headless/sshnpd.sh @@ -10,6 +10,7 @@ user="$(whoami)" # MANDATORY: Username v="-v" # Comment to disable verbose logging s="-s" # Comment to disable sending public keys u="-u" # Comment to disable sending user information +delegate_policy="" # END METADATA sleep 10 # allow machine to bring up network @@ -17,6 +18,6 @@ export USER="$user" while true; do # The line below runs the sshnpd service, with the options set above. # You can edit this line to further customize the service to your needs. - "$binary_path"/sshnpd -a "$device_atsign" -m "$manager_atsign" -d "$device_name" "$s" "$u" "$v" + "$binary_path"/sshnpd -a "$device_atsign" -m "$manager_atsign" -d "$device_name" "$delegate_policy" "$s" "$u" "$v" sleep 10 done diff --git a/packages/dart/sshnoports/bundles/shell/install.sh b/packages/dart/sshnoports/bundles/shell/install.sh index 93d9bfca8..8dc691bda 100755 --- a/packages/dart/sshnoports/bundles/shell/install.sh +++ b/packages/dart/sshnoports/bundles/shell/install.sh @@ -232,7 +232,7 @@ install_systemd_srvd() { systemd() { if is_darwin; then - echo "Unknown command: systemd" + echo "systemd is not supported on MacOS" usage exit 1 fi diff --git a/packages/dart/sshnoports/bundles/shell/launchd/com.atsign.sshnpd.plist b/packages/dart/sshnoports/bundles/shell/launchd/com.atsign.sshnpd.plist index 2166e2e70..8a20c2c20 100644 --- a/packages/dart/sshnoports/bundles/shell/launchd/com.atsign.sshnpd.plist +++ b/packages/dart/sshnoports/bundles/shell/launchd/com.atsign.sshnpd.plist @@ -14,10 +14,12 @@ -d [device_name] -su + [policy_option] + [policy_atsign] KeepAlive - RunAtLoad - + RunAtLoad + diff --git a/packages/dart/sshnoports/bundles/shell/systemd/sshnpd.service b/packages/dart/sshnoports/bundles/shell/systemd/sshnpd.service index bed7ee19f..8f7620e88 100644 --- a/packages/dart/sshnoports/bundles/shell/systemd/sshnpd.service +++ b/packages/dart/sshnoports/bundles/shell/systemd/sshnpd.service @@ -31,6 +31,9 @@ Environment=manager_atsign="@example_client" # MANDATORY: Device address (atSign) Environment=device_atsign="@example_device" +# OPTIONAL: Delegated access policy management +Environment=delegate_policy="" + # Device name Environment=device_name="default" @@ -45,4 +48,4 @@ Environment=v="-v" # The line below runs the sshnpd service, with the options set above. # You can edit this line to further customize the service to your needs. -ExecStart=/usr/local/bin/sshnpd -a "$device_atsign" -m "$manager_atsign" -d "$device_name" "$s" "$u" "$v" +ExecStart=/usr/local/bin/sshnpd -a "$device_atsign" -m "$manager_atsign" -d "$device_name" "$delegate_policy" "$s" "$u" "$v" diff --git a/packages/dart/sshnoports/bundles/universal.sh b/packages/dart/sshnoports/bundles/universal.sh index 349cd13af..3e9742b8b 100755 --- a/packages/dart/sshnoports/bundles/universal.sh +++ b/packages/dart/sshnoports/bundles/universal.sh @@ -51,6 +51,7 @@ quiet=false ### Client/ Device Install Variables client_atsign="" device_atsign="" +policy_atsign="" ### Client Install Variables unset magic_script @@ -148,9 +149,10 @@ usage() { echo "Device Options:" echo " -c, --client-atsign Set the client atSign" echo " -d, --device-atsign Set the device atSign" + echo " -p, --policy-atsign Set the access policy atSign" echo " -n, --device-name Set the device name" echo " --dt, --device-type Set the device type (launchd, systemd, tmux, headless)" - echo " --no-sudo Deliberately install without sudo priveleges" + echo " --no-sudo Deliberately install without sudo privileges" } @@ -330,6 +332,10 @@ parse_args() { shift device_atsign="$1" ;; + -p | --policy-atsign) + shift + policy_atsign="$1" + ;; -r | --region) # notice that --region and --rv-atsign are basically the same under the hood, # if region's input starts with an "@" it will be equivalent to using --rv-atsign @@ -377,6 +383,9 @@ parse_args() { >&2 echo "Error: Missing required information for device installation. (-c, -d, -n)" exit 1 fi + if [ -z "$policy_atsign" ]; then + >&2 echo "Info: No policy atSign provided; continuing." + fi elif [ "$install_type" = "client" ]; then if [ -z "$client_atsign" ] || [ -z "$device_atsign" ] || [ -z "$host_atsign" ]; then >&2 echo "Error: Missing required information for client installation. (-c, -d, -r)" @@ -670,24 +679,43 @@ check_ssh_keys() { validate_activation(){ device_output=$(echo "$(at_activate status -a $device_atsign 2>&1)") device_status=$(echo $device_output | grep -oE 'returning [0-9]+' | grep -oE '[0-9]+') + client_output=$(echo "$(at_activate status -a $client_atsign 2>&1)") client_status=$(echo $client_output | grep -oE 'returning [0-9]+' | grep -oE '[0-9]+') - if [ "$device_status" -ne 0 ]; then - echo - echo "Activating $device_atsign..." - if [ "$device_status" -eq 3 ]; then - echo $device_output - fi - at_activate onboard -a $device_atsign + + if [ -z "$policy_atsign" ]; then + policy_status=0 + else + policy_output=$(echo "$(at_activate status -a $policy_atsign 2>&1)") + policy_status=$(echo $policy_output | grep -oE 'returning [0-9]+' | grep -oE '[0-9]+') + fi + + if [ "$device_status" -ne 0 ]; then + echo + echo "Activating $device_atsign..." + if [ "$device_status" -eq 3 ]; then + echo $device_output fi - if [ "$client_status" -ne 0 ]; then - echo - echo "Activiating $client_atsign.." - if [ "$client_status" -eq 3 ]; then - echo $client_output - fi - at_activate onboard -a $client_atsign + at_activate onboard -a $device_atsign + fi + + if [ "$client_status" -ne 0 ]; then + echo + echo "Activating $client_atsign.." + if [ "$client_status" -eq 3 ]; then + echo $client_output fi + at_activate onboard -a $client_atsign + fi + + if [ "$policy_status" -ne 0 ]; then + echo + echo "Activating $policy_atsign.." + if [ "$policy_status" -eq 3 ]; then + echo "$policy_output" + fi + at_activate onboard -a "$policy_atsign" + fi } # CLIENT INSTALLATION # @@ -831,6 +859,9 @@ device() { device_atsign="$selectedatsign" fi + # Note: policy_atsign is not mandatory so, if none was supplied, + # we will not prompt for it + while [ -z "$device_name" ]; do printf "Enter device name: " read -r device_name @@ -851,8 +882,19 @@ device() { case "$device_install_type" in launchd) + if [ -n "$policy_atsign" ]; then + policy_option="-p" + policy_atsign="$(norm_atsign "$client_atsign")" + else + policy_option="" + fi launchd_plist="$HOME/Library/LaunchAgents/com.atsign.sshnpd.plist" - write_program_arguments_plist "$launchd_plist" "$bin_path/sshnpd" "-m" "$(norm_atsign "$client_atsign")" "-a" "$(norm_atsign "$device_atsign")" "-d" "$device_name" "-su" + write_program_arguments_plist "$launchd_plist" "$bin_path/sshnpd" \ + "-m" "$(norm_atsign "$client_atsign")" \ + "-a" "$(norm_atsign "$device_atsign")" \ + "-d" "$device_name" "-su" \ + "$policy_option" "$policy_atsign" + launchctl unload "$launchd_plist" launchctl load "$launchd_plist" echo "sshnpd installed with launchd" @@ -862,9 +904,14 @@ device() { write_systemd_user "$systemd_service" "$user" write_systemd_environment "$systemd_service" "manager_atsign" "$(norm_atsign "$client_atsign")" write_systemd_environment "$systemd_service" "device_atsign" "$(norm_atsign "$device_atsign")" + if [ -n "$policy_atsign" ]; then + write_systemd_environment "$systemd_service" "delegate_policy" "-p $(norm_atsign "$policy_atsign")" + fi write_systemd_environment "$systemd_service" "device_name" "$device_name" + systemctl enable sshnpd systemctl start sshnpd + echo "sshnpd installed with systemd. To see logs use:" echo "journalctl -u sshnpd.service -f" ;; @@ -873,6 +920,9 @@ device() { write_metadata "$shell_script" "manager_atsign" "$(norm_atsign "$client_atsign")" write_metadata "$shell_script" "device_atsign" "$(norm_atsign "$device_atsign")" write_metadata "$shell_script" "device_name" "$device_name" + if [ -n "$policy_atsign" ]; then + write_metadata "$shell_script" "delegate_policy" "-p $(norm_atsign "$policy_atsign")" + fi # split install output by lines, then grab the output after the line that says "To start immediately" eval "$(echo "$install_output" | grep -A1 "To start .* immediately:" | tail -n1)" ;; diff --git a/packages/dart/sshnoports/lib/npa_bootstrapper.dart b/packages/dart/sshnoports/lib/npa_bootstrapper.dart index 9e9654ac8..d5e1a2d5d 100644 --- a/packages/dart/sshnoports/lib/npa_bootstrapper.dart +++ b/packages/dart/sshnoports/lib/npa_bootstrapper.dart @@ -8,7 +8,10 @@ import 'package:sshnoports/src/print_version.dart'; import 'package:sshnoports/src/service_factories.dart'; Future run( - NPARequestHandler handler, List commandLineArgs) async { + NPARequestHandler handler, + List commandLineArgs, { + Set? daemonAtsigns, +}) async { AtSignLogger.root_level = 'SHOUT'; AtSignLogger.defaultLoggingHandler = AtSignLogger.stdErrLoggingHandler; late final NPA sshnpa; @@ -17,6 +20,7 @@ Future run( sshnpa = await NPA.fromCommandLineArgs( commandLineArgs, handler: handler, + daemonAtsigns: daemonAtsigns, atClientGenerator: (NPAParams p) => createAtClientCli( atsign: p.authorizerAtsign, atKeysFilePath: p.atKeysFilePath, diff --git a/packages/dart/sshnoports/pubspec.lock b/packages/dart/sshnoports/pubspec.lock index be6716be6..0205dd663 100644 --- a/packages/dart/sshnoports/pubspec.lock +++ b/packages/dart/sshnoports/pubspec.lock @@ -83,13 +83,13 @@ packages: source: hosted version: "1.1.0" at_client: - dependency: transitive + dependency: "direct main" description: name: at_client - sha256: "12b7f5cacbb726e33a76ed4d069cb552df1333d30026cb9237f3b8b83bc0e6e4" + sha256: "76a24bbf17867b64a5e827bb33d7c5f3ca2edfd5766d0920211e068d30ae748f" url: "https://pub.dev" source: hosted - version: "3.0.78" + version: "3.1.0" at_commons: dependency: transitive description: @@ -907,7 +907,7 @@ packages: source: hosted version: "6.5.0" yaml: - dependency: transitive + dependency: "direct main" description: name: yaml sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" diff --git a/packages/dart/sshnoports/pubspec.yaml b/packages/dart/sshnoports/pubspec.yaml index 907b2b3b0..cf48d0617 100644 --- a/packages/dart/sshnoports/pubspec.yaml +++ b/packages/dart/sshnoports/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: path: "../noports_core" at_onboarding_cli: 1.6.2 at_cli_commons: ^1.1.0 + at_client: ^3.1.0 args: 2.5.0 socket_connector: ^2.2.0 dartssh2: 2.8.2 @@ -18,6 +19,7 @@ dependencies: at_utils: 3.0.16 logging: ^1.2.0 chalkdart: ^2.2.1 + yaml: ^3.1.2 dependency_overrides: dartssh2: diff --git a/packages/dart/sshnoports/tools/Dockerfile.package b/packages/dart/sshnoports/tools/Dockerfile.package deleted file mode 100644 index a45026cc1..000000000 --- a/packages/dart/sshnoports/tools/Dockerfile.package +++ /dev/null @@ -1,33 +0,0 @@ -# Dockerfile.package -# A dockerfile for packaging SSH No Ports releases using docker buildx - -FROM atsigncompany/buildimage:3.5.2_3.6.0-149.3.beta@sha256:df67b9e3271381fc0c5b20e7350cf4de8dad6ac62e075b49b1a866c49af47409 AS build -# Using atsigncompany/buildimage until official dart image has RISC-V support -WORKDIR /sshnoports -COPY . . -RUN set -eux; \ - case "$(dpkg --print-architecture)" in \ - amd64) ARCH="x64";; \ - armhf) ARCH="arm";; \ - arm64) ARCH="arm64";; \ - riscv64) ARCH="riscv64";; \ - esac; \ - cd sshnoports; \ - mkdir -p sshnp/debug; \ - mkdir tarball; \ - dart pub get --enforce-lockfile; \ - dart run build_runner build --delete-conflicting-outputs; \ - dart compile exe bin/activate_cli.dart -v -o sshnp/at_activate; \ - dart compile exe bin/sshnp.dart -v -o sshnp/sshnp; \ - dart compile exe bin/npt.dart -v -o sshnp/npt; \ - dart compile exe bin/sshnpd.dart -v -o sshnp/sshnpd; \ - dart compile exe bin/srv.dart -v -o sshnp/srv; \ - dart compile exe bin/srvd.dart -v -o sshnp/srvd; \ - dart compile exe bin/srvd.dart -D ENABLE_SNOOP=true -v -o sshnp/debug/srvd; \ - cp -r bundles/core/* sshnp/; \ - cp -r bundles/shell/* sshnp/; \ - cp LICENSE sshnp/; \ - tar -cvzf tarball/sshnp-linux-${ARCH}.tgz sshnp - -FROM scratch -COPY --from=build /sshnoports/sshnoports/tarball/* / diff --git a/packages/dart/sshnp_flutter/pubspec.lock b/packages/dart/sshnp_flutter/pubspec.lock index f668fbf65..0f23c0b89 100644 --- a/packages/dart/sshnp_flutter/pubspec.lock +++ b/packages/dart/sshnp_flutter/pubspec.lock @@ -86,10 +86,10 @@ packages: dependency: transitive description: name: at_client - sha256: "12b7f5cacbb726e33a76ed4d069cb552df1333d30026cb9237f3b8b83bc0e6e4" + sha256: "76a24bbf17867b64a5e827bb33d7c5f3ca2edfd5766d0920211e068d30ae748f" url: "https://pub.dev" source: hosted - version: "3.0.78" + version: "3.1.0" at_client_mobile: dependency: "direct main" description: diff --git a/tools/multibuild/Dockerfile.package b/tools/multibuild/Dockerfile.package new file mode 100644 index 000000000..8a8ccfabc --- /dev/null +++ b/tools/multibuild/Dockerfile.package @@ -0,0 +1,58 @@ +# Dockerfile.package +# A dockerfile for packaging SSH No Ports releases using docker buildx + +FROM atsigncompany/buildimage:3.5.2_3.6.0-149.3.beta@sha256:df67b9e3271381fc0c5b20e7350cf4de8dad6ac62e075b49b1a866c49af47409 AS build +# Using atsigncompany/buildimage until official dart image has RISC-V support +WORKDIR /noports + +# install node for later (keep at the top file to increase cache hits) +# hadolint ignore=DL3008 +RUN apt-get update; \ + apt-get install -y --no-install-recommends npm + +COPY . . + +# Build packages/dart/sshnoports +WORKDIR /noports/packages/dart/sshnoports +RUN set -eux; \ + mkdir -p /sshnp/debug; \ + mkdir -p /sshnp/web/admin; \ + mkdir /tarball; \ + dart pub get --enforce-lockfile; \ + dart run build_runner build --delete-conflicting-outputs; \ + dart compile exe bin/activate_cli.dart -v -o /sshnp/at_activate; \ + dart compile exe bin/sshnp.dart -v -o /sshnp/sshnp; \ + dart compile exe bin/npt.dart -v -o /sshnp/npt; \ + dart compile exe bin/npp_file.dart -v -o /sshnp/npp_file; \ + dart compile exe bin/npp_atserver.dart -v -o /sshnp/npp_atserver; \ + dart compile exe bin/sshnpd.dart -v -o /sshnp/sshnpd; \ + dart compile exe bin/srv.dart -v -o /sshnp/srv; \ + dart compile exe bin/srvd.dart -v -o /sshnp/srvd; \ + dart compile exe bin/srvd.dart -D ENABLE_SNOOP=true -v -o /sshnp/debug/srvd; \ + cp -r bundles/core/* /sshnp/; \ + cp -r bundles/shell/* /sshnp/; \ + cp LICENSE /sshnp/; + +# Build apps/admin/admin_api - BETA +WORKDIR /noports/apps/admin/admin_api +RUN dart pub get --enforce-lockfile; \ + dart compile exe bin/np_admin.dart -v -o /sshnp/np_admin + +# Build apps/admin/webapp +WORKDIR /noports/apps/admin/webapp +RUN npm install; \ + npm run build; \ + mkdir -p /sshnp/web/admin; \ + cp -r ./dist/* /sshnp/web/admin/ + +RUN set -eux; \ + case "$(dpkg --print-architecture)" in \ + amd64) ARCH="x64";; \ + armhf) ARCH="arm";; \ + arm64) ARCH="arm64";; \ + riscv64) ARCH="riscv64";; \ + esac; \ + tar -cvzf /tarball/sshnp-linux-"${ARCH}".tgz /sshnp + +FROM scratch +COPY --from=build /tarball/* /