From b864cac9afa31a3db7f523771581d2bc612b8171 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Mon, 11 Sep 2023 17:32:46 +0300 Subject: [PATCH 1/3] AppEcosystem -> AppAPI refactor(1) Signed-off-by: Alexander Piskun --- .github/workflows/analysis-coverage.yml | 58 +++++------ CHANGELOG.md | 7 ++ README.md | 2 +- docs/DevSetup.rst | 4 +- docs/NextcloudApp.rst | 16 ++-- docs/NextcloudTalkBot.rst | 2 +- .../{AppEcosystem.rst => AppAPI.rst} | 18 ++-- docs/index.rst | 2 +- examples/as_app/skeleton/Makefile | 14 +-- examples/as_app/talk_bot/HOW_TO_INSTALL.md | 10 +- examples/as_app/talk_bot/Makefile | 14 +-- examples/as_app/to_gif/Makefile | 18 ++-- nc_py_api/_misc.py | 16 +++- nc_py_api/_preferences_ex.py | 10 +- nc_py_api/_session.py | 96 ++++--------------- nc_py_api/_version.py | 2 +- nc_py_api/apps.py | 2 +- nc_py_api/ex_app/integration_fastapi.py | 9 +- nc_py_api/ex_app/ui/files.py | 4 +- nc_py_api/nextcloud.py | 12 +-- nc_py_api/notifications.py | 6 +- pyproject.toml | 1 - scripts/ci_register.sh | 4 +- scripts/dev_register.sh | 8 +- tests/actual_tests/logs_test.py | 10 +- tests/actual_tests/misc_test.py | 10 +- tests/actual_tests/nc_app_test.py | 4 +- tests/conftest.py | 2 +- tests/gfixture_set_env.py | 2 +- 29 files changed, 160 insertions(+), 203 deletions(-) rename docs/benchmarks/{AppEcosystem.rst => AppAPI.rst} (82%) diff --git a/.github/workflows/analysis-coverage.yml b/.github/workflows/analysis-coverage.yml index 4fb6bec8..95fedb03 100644 --- a/.github/workflows/analysis-coverage.yml +++ b/.github/workflows/analysis-coverage.yml @@ -117,16 +117,16 @@ jobs: working-directory: nc_py_api run: python3 -m pip -v install ".[dev]" - - name: Checkout AppEcosystemV2 + - name: Checkout AppAPI uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 with: - path: apps/app_ecosystem_v2 - repository: cloud-py-api/app_ecosystem_v2 + path: apps/app_api + repository: cloud-py-api/app_api - - name: Install AppEcosystemV2 + - name: Install AppAPI run: | - patch -p 1 -i apps/app_ecosystem_v2/base_php.patch - php occ app:enable app_ecosystem_v2 + patch -p 1 -i apps/app_api/base_php.patch + php occ app:enable app_api cd nc_py_api coverage run --data-file=.coverage.ci_install tests/_install.py & echo $! > /tmp/_install.pid @@ -272,18 +272,18 @@ jobs: working-directory: nc_py_api run: python3 -m pip -v install ".[dev]" - - name: Checkout AppEcosystemV2 + - name: Checkout AppAPI uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 if: ${{ !startsWith(matrix.nextcloud, '26.') }} with: - path: apps/app_ecosystem_v2 - repository: cloud-py-api/app_ecosystem_v2 + path: apps/app_api + repository: cloud-py-api/app_api - - name: Install AppEcosystemV2 + - name: Install AppAPI if: ${{ !startsWith(matrix.nextcloud, '26.') }} run: | - patch -p 1 -i apps/app_ecosystem_v2/base_php.patch - php occ app:enable app_ecosystem_v2 + patch -p 1 -i apps/app_api/base_php.patch + php occ app:enable app_api cd nc_py_api coverage run --data-file=.coverage.ci_install tests/_install.py & echo $! > /tmp/_install.pid @@ -416,16 +416,16 @@ jobs: working-directory: nc_py_api run: python3 -m pip -v install ".[dev]" - - name: Checkout AppEcosystemV2 + - name: Checkout AppAPI uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 with: - path: apps/app_ecosystem_v2 - repository: cloud-py-api/app_ecosystem_v2 + path: apps/app_api + repository: cloud-py-api/app_api - - name: Install AppEcosystemV2 + - name: Install AppAPI run: | - patch -p 1 -i apps/app_ecosystem_v2/base_php.patch - php occ app:enable app_ecosystem_v2 + patch -p 1 -i apps/app_api/base_php.patch + php occ app:enable app_api cd nc_py_api coverage run --data-file=.coverage.ci_install tests/_install.py & echo $! > /tmp/_install.pid @@ -536,15 +536,15 @@ jobs: working-directory: nc_py_api run: python3 -m pip -v install ".[dev]" - - name: Checkout AppEcosystemV2 + - name: Checkout AppAPI uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 with: - path: apps/app_ecosystem_v2 - repository: cloud-py-api/app_ecosystem_v2 + path: apps/app_api + repository: cloud-py-api/app_api - - name: Install AppEcosystemV2 + - name: Install AppAPI run: | - php occ app:enable app_ecosystem_v2 + php occ app:enable app_api cd nc_py_api coverage run --data-file=.coverage.ci_install tests/_install.py & echo $! > /tmp/_install.pid @@ -799,19 +799,19 @@ jobs: working-directory: nc_py_api run: python3 -m pip -v install ".[dev]" - - name: Checkout AppEcosystemV2 + - name: Checkout AppAPI uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4 with: - path: apps/app_ecosystem_v2 - repository: cloud-py-api/app_ecosystem_v2 + path: apps/app_api + repository: cloud-py-api/app_api - name: Patch base.php if: ${{ startsWith(matrix.nextcloud, 'stable26') }} - run: patch -p 1 -i apps/app_ecosystem_v2/base_php.patch + run: patch -p 1 -i apps/app_api/base_php.patch - - name: Install AppEcosystemV2 + - name: Install AppAPI run: | - php occ app:enable app_ecosystem_v2 + php occ app:enable app_api cd nc_py_api coverage run --data-file=.coverage.ci_install tests/_install.py & echo $! > /tmp/_install.pid diff --git a/CHANGELOG.md b/CHANGELOG.md index d7536233..26b819a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to this project will be documented in this file. +## [0.2.0 - 2023-09-11] + +### Changed + +- AppEcosystem_V2 Project was renamed to App_API, adjust all routes, examples, and docs for this. +- The Application Authentication mechanism was changed to a much simple one. + ## [0.1.0 - 2023-09-06] ### Added diff --git a/README.md b/README.md index 9ec5ae2f..5e8a4508 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ The **Nextcloud** class functions as a standard Nextcloud client, enabling you to make API requests using a username and password. On the other hand, the **NextcloudApp** class is designed for creating applications for Nextcloud.
-It uses the [AppEcosystem](https://github.com/cloud-py-api/app_ecosystem_v2) to allow +It uses the [AppAPI](https://github.com/cloud-py-api/app_api) to allow applications to impersonate users through a separate authentication mechanism. Both classes offer most of the same APIs, diff --git a/docs/DevSetup.rst b/docs/DevSetup.rst index b476beb2..510c0561 100644 --- a/docs/DevSetup.rst +++ b/docs/DevSetup.rst @@ -10,7 +10,7 @@ Suggested IDE: **PyCharm**, but of course you can use any IDE you like for this Steps to setup up the development environment: #. Setup Nextcloud locally or remotely. -#. Install `AppEcosystem `_, follow it's steps to register ``deploy daemon`` if needed. +#. Install `AppAPI `_, follow it's steps to register ``deploy daemon`` if needed. #. Clone the `nc_py_api `_ with :command:`shell`:: git clone https://github.com/cloud-py-api/nc_py_api.git @@ -39,7 +39,7 @@ Steps to setup up the development environment: pre-commit install -#. If ``deploy daemon`` is registered for AppEcosystem, register **nc_py_api** as an application with :command:`shell`:: +#. If ``deploy daemon`` is registered for AppAPI, register **nc_py_api** as an application with :command:`shell`:: make register28 diff --git a/docs/NextcloudApp.rst b/docs/NextcloudApp.rst index e3cd6a51..5d98955b 100644 --- a/docs/NextcloudApp.rst +++ b/docs/NextcloudApp.rst @@ -1,7 +1,7 @@ Writing a Nextcloud Application =============================== -This chapter assumes that you are already familiar with the `concepts `_ of the AppEcosystem. +This chapter assumes that you are already familiar with the `concepts `_ of the AppAPI. As a first step, let's take a look at the structure of a basic Python application. @@ -47,7 +47,7 @@ First register ``manual_install`` daemon: .. code-block:: shell - php occ app_ecosystem_v2:daemon:register manual_install "Manual Install" manual-install 0 0 0 + php occ app_api:daemon:register manual_install "Manual Install" manual-install 0 0 0 Then, launch your application. Since this is a manual deployment, it's your responsibility to set minimum of the environment variables. Here they are: @@ -56,7 +56,7 @@ Here they are: * APP_PORT - Port on which application listen for the requests from the Nextcloud. * APP_SECRET - Secret for ``hmac`` signature generation. * APP_VERSION - Version of the application. -* AE_VERSION - Version of the AppEcosystem. +* AE_VERSION - Version of the AppAPI. * NEXTCLOUD_URL - URL at which the application can access the Nextcloud API. You can find values for these environment variables in the **Skeleton** or **ToGif** run configurations. @@ -65,7 +65,7 @@ After launching your application, execute the following command in the Nextcloud .. code-block:: shell - php occ app_ecosystem_v2:app:register YOUR_APP_ID manual_install --json-info \ + php occ app_api:app:register YOUR_APP_ID manual_install --json-info \ "{\"appid\":\"YOUR_APP_ID\",\"name\":\"YOUR_APP_DISPLAY_NAME\",\"daemon_config_name\":\"manual_install\",\"version\":\"YOU_APP_VERSION\",\"secret\":\"YOUR_APP_SECRET\",\"host\":\"host.docker.internal\",\"scopes\":{\"required\":[2, 10, 11],\"optional\":[30, 31, 32, 33]},\"port\":SELECTED_PORT,\"protocol\":\"http\",\"system_app\":0}" \ -e --force-scopes @@ -79,7 +79,7 @@ Examples for such Makefiles can be found in this repository: `ToGif `_ , `nc_py_api `_ -During the execution of `php occ app_ecosystem_v2:app:register`, the **enabled_handler** will be called, +During the execution of `php occ app_api:app:register`, the **enabled_handler** will be called, as we pass the flag ``-e``, meaning ``enable after registration``. This is likely all you need to start debugging and developing an application for Nextcloud. @@ -89,7 +89,7 @@ Pack & Deploy Before reading this chapter, please review the basic information about deployment and the currently supported types of -`deployments configurations `_ in the AppEcosystem documentation. +`deployments configurations `_ in the AppAPI documentation. Docker Deploy Daemon """""""""""""""""""" @@ -140,7 +140,7 @@ First of all, we modernize info.ixml, add the API groups we need for this to wor -.. note:: Full list of avalaible API scopes can be found `here `_. +.. note:: Full list of avalaible API scopes can be found `here `_. After that we extend the **enabled** handler and include there registration of the drop-down list element: @@ -194,7 +194,7 @@ heavy calculations and we cannot guarantee a fast completion time, it is recomme an empty response (which will be a status of 200) and in the background already slowly perform operations. The last parameter is a structure describing the action and the file on which it needs to be performed, -which is passed by the Appecosystem when clicking on the drop-down context menu of the file. +which is passed by the AppAPI when clicking on the drop-down context menu of the file. We use the built method :py:meth:`~nc_py_api.ex_app.ui.files.UiActionFileInfo.to_fs_node` into the structure to convert it into a standard :py:class:`~nc_py_api.files.FsNode` class that describes the file and pass the FsNode class instance to the background task. diff --git a/docs/NextcloudTalkBot.rst b/docs/NextcloudTalkBot.rst index 4b475c1c..ea9953b0 100644 --- a/docs/NextcloudTalkBot.rst +++ b/docs/NextcloudTalkBot.rst @@ -1,7 +1,7 @@ Nextcloud Talk Bot API in Applications ====================================== -The AppEcosystem is an excellent choice for developing and deploying bots for Nextcloud Talk. +The AppAPI is an excellent choice for developing and deploying bots for Nextcloud Talk. Bots for Nextcloud Talk, in essence, don't differ significantly from regular external applications. The functionality of an external application can include just the bot or provide additional functionalities as well. diff --git a/docs/benchmarks/AppEcosystem.rst b/docs/benchmarks/AppAPI.rst similarity index 82% rename from docs/benchmarks/AppEcosystem.rst rename to docs/benchmarks/AppAPI.rst index 038be726..dfbffe2e 100644 --- a/docs/benchmarks/AppEcosystem.rst +++ b/docs/benchmarks/AppAPI.rst @@ -1,7 +1,7 @@ -AppEcosystem Benchmarks -======================= +AppAPI Benchmarks +================= -In the current implementation, applications written and using the AppEcosystem +In the current implementation, applications written and using the AppAPI so far in most cases will be authenticated at the beginning of each action. *A future enhancement that can be implemented is the addition of a session cache. @@ -9,12 +9,12 @@ This feature would eliminate the need for re-authorization for subsequent action by the same user within a certain time frame. The implementation of this session cache would be seamless for developers and require no additional actions.* -It is important to note that the AppEcosystem authentication type is currently the fastest among available options. +It is important to note that the AppAPI authentication type is currently the fastest among available options. Compared to traditional username/password authentication and app password authentication, -both of which are considerably slower, the AppEcosystem provides a significant advantage in terms of speed. +both of which are considerably slower, the AppAPI provides a significant advantage in terms of speed. When considering data transfer speed, it is worth mentioning -that the AppEcosystem's upload speed may be slightly lower, around 6-8 percent, for large data transfers. +that the AppAPI's upload speed may be slightly lower, around 6-8 percent, for large data transfers. This decrease in speed is due to the authentication process, which involves hashing all data. However, for loading any data, there is no slowdown compared to standard methods. @@ -26,14 +26,14 @@ this aspect is beyond the scope of the discussed issue. Conclusion ---------- -In summary, the AppEcosystem authentication offers fast and secure access to user data. +In summary, the AppAPI authentication offers fast and secure access to user data. With the potential addition of a session cache in the future, the authentication process can become even more efficient and seamless for users. The slight decrease in upload speed for large data transfers is a trade-off for the enhanced security provided by the authentication process. -Overall, the AppEcosystem authentication proves to be a reliable and effective method for application authentication. +Overall, the AppAPI authentication proves to be a reliable and effective method for application authentication. -.. _appecosystem-bench-results: +.. _appapi-bench-results: Detailed Benchmark Results -------------------------- diff --git a/docs/index.rst b/docs/index.rst index 879eae05..01167cda 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,7 +36,7 @@ Have a great time with Python and Nextcloud! Options reference/index.rst DevSetup - benchmarks/AppEcosystem.rst + benchmarks/AppAPI.rst Indices and tables ================== diff --git a/examples/as_app/skeleton/Makefile b/examples/as_app/skeleton/Makefile index ce6e56de..ed957c28 100644 --- a/examples/as_app/skeleton/Makefile +++ b/examples/as_app/skeleton/Makefile @@ -26,24 +26,24 @@ build-push: .PHONY: deploy deploy: - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:deploy skeleton docker_dev \ + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:deploy skeleton docker_dev \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/skeleton/appinfo/info.xml .PHONY: run28 run28: - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:unregister skeleton --silent || true - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:register skeleton docker_dev -e --force-scopes \ + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister skeleton --silent || true + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register skeleton docker_dev -e --force-scopes \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/skeleton/appinfo/info.xml .PHONY: run27 run27: - docker exec master-stable27-1 sudo -u www-data php occ app_ecosystem_v2:app:unregister skeleton --silent || true - docker exec master-stable27-1 sudo -u www-data php occ app_ecosystem_v2:app:register skeleton docker_dev -e --force-scopes \ + docker exec master-stable27-1 sudo -u www-data php occ app_api:app:unregister skeleton --silent || true + docker exec master-stable27-1 sudo -u www-data php occ app_api:app:register skeleton docker_dev -e --force-scopes \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/skeleton/appinfo/info.xml .PHONY: manual_register manual_register: - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:unregister skeleton --silent || true - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:register skeleton manual_install --json-info \ + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister skeleton --silent || true + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register skeleton manual_install --json-info \ "{\"appid\":\"skeleton\",\"name\":\"App Skeleton\",\"daemon_config_name\":\"manual_install\",\"version\":\"1.0.0\",\"secret\":\"12345\",\"host\":\"host.docker.internal\",\"port\":9030,\"scopes\":{\"required\":[],\"optional\":[]},\"protocol\":\"http\",\"system_app\":0}" \ -e --force-scopes diff --git a/examples/as_app/talk_bot/HOW_TO_INSTALL.md b/examples/as_app/talk_bot/HOW_TO_INSTALL.md index eced096b..54364116 100644 --- a/examples/as_app/talk_bot/HOW_TO_INSTALL.md +++ b/examples/as_app/talk_bot/HOW_TO_INSTALL.md @@ -1,19 +1,19 @@ How To Install ============== -Currently, while AppEcosystem hasn't been published on the App Store, and App Store support hasn't been added yet, +Currently, while AppAPI hasn't been published on the App Store, and App Store support hasn't been added yet, installation is a little bit tricky. Steps to Install: -1. [Install AppEcosystem](https://cloud-py-api.github.io/app_ecosystem_v2/Installation.html) -2. Create a deployment daemon according to the [instructions](https://cloud-py-api.github.io/app_ecosystem_v2/CreationOfDeployDaemon.html#create-deploy-daemon) of the Appecosystem -3. php occ app_ecosystem_v2:app:deploy talk_bot "daemon_deploy_name" \ +1. [Install AppEcosystem](https://cloud-py-api.github.io/app_api/Installation.html) +2. Create a deployment daemon according to the [instructions](https://cloud-py-api.github.io/app_api/CreationOfDeployDaemon.html#create-deploy-daemon) of the AppPI +3. php occ app_api:app:deploy talk_bot "daemon_deploy_name" \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/talk_bot/appinfo/info.xml to deploy a docker image with Bot to docker. -4. php occ app_ecosystem_v2:app:register talk_bot "daemon_deploy_name" -e --force-scopes \ +4. php occ app_api:app:register talk_bot "daemon_deploy_name" -e --force-scopes \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/talk_bot/appinfo/info.xml to call its **enable** handler and accept all required API scopes by default. diff --git a/examples/as_app/talk_bot/Makefile b/examples/as_app/talk_bot/Makefile index 1972fde3..47ac5a9b 100644 --- a/examples/as_app/talk_bot/Makefile +++ b/examples/as_app/talk_bot/Makefile @@ -26,24 +26,24 @@ build-push: .PHONY: deploy deploy: - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:deploy talk_bot docker_dev \ + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:deploy talk_bot docker_dev \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/talk_bot/appinfo/info.xml .PHONY: run28 run28: - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:unregister talk_bot --silent || true - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:register talk_bot docker_dev -e --force-scopes \ + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister talk_bot --silent || true + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register talk_bot docker_dev -e --force-scopes \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/talk_bot/appinfo/info.xml .PHONY: run27 run27: - docker exec master-stable27-1 sudo -u www-data php occ app_ecosystem_v2:app:unregister talk_bot --silent || true - docker exec master-stable27-1 sudo -u www-data php occ app_ecosystem_v2:app:register talk_bot docker_dev -e --force-scopes \ + docker exec master-stable27-1 sudo -u www-data php occ app_api:app:unregister talk_bot --silent || true + docker exec master-stable27-1 sudo -u www-data php occ app_api:app:register talk_bot docker_dev -e --force-scopes \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/talk_bot/appinfo/info.xml .PHONY: manual_register manual_register: - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:unregister talk_bot --silent || true - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:register talk_bot manual_install --json-info \ + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister talk_bot --silent || true + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register talk_bot manual_install --json-info \ "{\"appid\":\"talk_bot\",\"name\":\"TalkBot\",\"daemon_config_name\":\"manual_install\",\"version\":\"1.0.0\",\"secret\":\"12345\",\"host\":\"host.docker.internal\",\"port\":9032,\"scopes\":{\"required\":[\"TALK\", \"TALK_BOT\"],\"optional\":[]},\"protocol\":\"http\",\"system_app\":0}" \ -e --force-scopes diff --git a/examples/as_app/to_gif/Makefile b/examples/as_app/to_gif/Makefile index 25f4b93a..df567ad9 100644 --- a/examples/as_app/to_gif/Makefile +++ b/examples/as_app/to_gif/Makefile @@ -27,31 +27,31 @@ build-push: .PHONY: deploy deploy: - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:deploy to_gif docker_dev \ + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:deploy to_gif docker_dev \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/to_gif/appinfo/info.xml .PHONY: run28 run28: - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:unregister to_gif --silent || true - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:register to_gif docker_dev -e --force-scopes \ + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister to_gif --silent || true + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register to_gif docker_dev -e --force-scopes \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/to_gif/appinfo/info.xml .PHONY: manual_register28 manual_register28: - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:unregister to_gif --silent || true - docker exec master-nextcloud-1 sudo -u www-data php occ app_ecosystem_v2:app:register to_gif manual_install --json-info \ + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:unregister to_gif --silent || true + docker exec master-nextcloud-1 sudo -u www-data php occ app_api:app:register to_gif manual_install --json-info \ "{\"appid\":\"to_gif\",\"name\":\"to_gif\",\"daemon_config_name\":\"manual_install\",\"version\":\"1.0.0\",\"secret\":\"12345\",\"host\":\"host.docker.internal\",\"port\":9031,\"scopes\":{\"required\":[\"FILES\", \"NOTIFICATIONS\"],\"optional\":[]},\"protocol\":\"http\",\"system_app\":0}" \ -e --force-scopes .PHONY: run27 run27: - docker exec master-stable27-1 sudo -u www-data php occ app_ecosystem_v2:app:unregister to_gif --silent || true - docker exec master-stable27-1 sudo -u www-data php occ app_ecosystem_v2:app:register to_gif docker_dev -e --force-scopes \ + docker exec master-stable27-1 sudo -u www-data php occ app_api:app:unregister to_gif --silent || true + docker exec master-stable27-1 sudo -u www-data php occ app_api:app:register to_gif docker_dev -e --force-scopes \ --info-xml https://raw.githubusercontent.com/cloud-py-api/nc_py_api/main/examples/as_app/to_gif/appinfo/info.xml .PHONY: manual_register27 manual_register27: - docker exec master-stable27-1 sudo -u www-data php occ app_ecosystem_v2:app:unregister to_gif --silent || true - docker exec master-stable27-1 sudo -u www-data php occ app_ecosystem_v2:app:register to_gif manual_install --json-info \ + docker exec master-stable27-1 sudo -u www-data php occ app_api:app:unregister to_gif --silent || true + docker exec master-stable27-1 sudo -u www-data php occ app_api:app:register to_gif manual_install --json-info \ "{\"appid\":\"to_gif\",\"name\":\"to_gif\",\"daemon_config_name\":\"manual_install\",\"version\":\"1.0.0\",\"secret\":\"12345\",\"host\":\"host.docker.internal\",\"port\":9031,\"scopes\":{\"required\":[\"FILES\", \"NOTIFICATIONS\"],\"optional\":[]},\"protocol\":\"http\",\"system_app\":0}" \ -e --force-scopes diff --git a/nc_py_api/_misc.py b/nc_py_api/_misc.py index 070899af..a82ac1c3 100644 --- a/nc_py_api/_misc.py +++ b/nc_py_api/_misc.py @@ -2,7 +2,8 @@ For internal use, prototypes can change between versions. """ -import datetime +from base64 import b64decode +from datetime import datetime, timezone from random import choice from string import ascii_lowercase, ascii_uppercase, digits from typing import Callable, Union @@ -68,9 +69,16 @@ def random_string(size: int) -> str: return "".join(choice(letters) for _ in range(size)) -def nc_iso_time_to_datetime(iso8601_time: str) -> datetime.datetime: +def nc_iso_time_to_datetime(iso8601_time: str) -> datetime: """Returns parsed ``datetime`` or datetime(1970, 1, 1) in case of error.""" try: - return datetime.datetime.fromisoformat(iso8601_time) + return datetime.fromisoformat(iso8601_time) except (ValueError, TypeError): - return datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) + return datetime(1970, 1, 1, tzinfo=timezone.utc) + + +def get_username_secret_from_headers(headers: dict) -> tuple[str, str]: + """Returns tuple with ``username`` and ``app_secret`` from headers.""" + auth_aa = b64decode(headers.get("AUTHORIZATION-APP-API", "")).decode("UTF-8") + username, app_secret = auth_aa.split(":", maxsplit=1) + return username, app_secret diff --git a/nc_py_api/_preferences_ex.py b/nc_py_api/_preferences_ex.py index 087673ac..fc1a4908 100644 --- a/nc_py_api/_preferences_ex.py +++ b/nc_py_api/_preferences_ex.py @@ -29,7 +29,7 @@ def get_value(self, key: str, default=None) -> typing.Optional[str]: """Returns the value of the key, if found, or the specified default value.""" if not key: raise ValueError("`key` parameter can not be empty") - require_capabilities("app_ecosystem_v2", self._session.capabilities) + require_capabilities("app_api", self._session.capabilities) r = self.get_values([key]) if r: return r[0].value @@ -41,7 +41,7 @@ def get_values(self, keys: list[str]) -> list[CfgRecord]: return [] if not all(keys): raise ValueError("`key` parameter can not be empty") - require_capabilities("app_ecosystem_v2", self._session.capabilities) + require_capabilities("app_api", self._session.capabilities) data = {"configKeys": keys} results = self._session.ocs( method="POST", path=f"{self._session.ae_url}/{self._url_suffix}/get-values", json=data @@ -56,7 +56,7 @@ def delete(self, keys: typing.Union[str, list[str]], not_fail=True) -> None: return if not all(keys): raise ValueError("`key` parameter can not be empty") - require_capabilities("app_ecosystem_v2", self._session.capabilities) + require_capabilities("app_api", self._session.capabilities) try: self._session.ocs( method="DELETE", path=f"{self._session.ae_url}/{self._url_suffix}", json={"configKeys": keys} @@ -75,7 +75,7 @@ def set_value(self, key: str, value: str) -> None: """Sets a value for a key.""" if not key: raise ValueError("`key` parameter can not be empty") - require_capabilities("app_ecosystem_v2", self._session.capabilities) + require_capabilities("app_api", self._session.capabilities) params = {"configKey": key, "configValue": value} self._session.ocs(method="POST", path=f"{self._session.ae_url}/{self._url_suffix}", json=params) @@ -94,7 +94,7 @@ def set_value(self, key: str, value: str, sensitive: typing.Optional[bool] = Non """ if not key: raise ValueError("`key` parameter can not be empty") - require_capabilities("app_ecosystem_v2", self._session.capabilities) + require_capabilities("app_api", self._session.capabilities) params: dict = {"configKey": key, "configValue": value} if sensitive is not None: params["sensitive"] = sensitive diff --git a/nc_py_api/_session.py b/nc_py_api/_session.py index 3ad92773..ab646d68 100644 --- a/nc_py_api/_session.py +++ b/nc_py_api/_session.py @@ -1,13 +1,10 @@ """Session represents one connection to Nextcloud. All related stuff for these live here.""" -import asyncio -import hmac from abc import ABC, abstractmethod +from base64 import b64encode from collections.abc import Iterator from dataclasses import dataclass -from datetime import datetime, timezone from enum import IntEnum -from hashlib import sha256 from json import dumps, loads from os import environ from typing import Optional, TypedDict, Union @@ -18,14 +15,6 @@ from httpx import Headers as HttpxHeaders from httpx import Limits, ReadTimeout, Response -try: - from xxhash import xxh64 -except ImportError as ex: - from ._deffered_error import DeferredError - - xxh64 = DeferredError(ex) - - from . import options from ._exceptions import ( NextcloudException, @@ -33,6 +22,7 @@ NextcloudExceptionNotModified, check_error, ) +from ._misc import get_username_secret_from_headers class OCSRespond(IntEnum): @@ -125,7 +115,7 @@ class AppConfig(BasicConfig): """Application ID""" app_version: str """Application version""" - app_secret: bytes + app_secret: str """Application authentication secret""" def __init__(self, **kwargs): @@ -135,7 +125,7 @@ def __init__(self, **kwargs): self.ae_version = "1.0.0" self.app_name = self._get_config_value("app_id", **kwargs) self.app_version = self._get_config_value("app_version", **kwargs) - self.app_secret = self._get_config_value("app_secret", **kwargs).encode("UTF-8") + self.app_secret = self._get_config_value("app_secret", **kwargs) class NcSessionBasic(ABC): @@ -312,7 +302,7 @@ def nc_version(self) -> ServerVersion: @property def ae_url(self) -> str: """Return base url for the App Ecosystem endpoints.""" - return "/ocs/v1.php/apps/app_ecosystem_v2/api/v1" + return "/ocs/v1.php/apps/app_api/api/v1" class NcSession(NcSessionBasic): @@ -334,76 +324,42 @@ def __init__(self, **kwargs): super().__init__(**kwargs) def _get_stream(self, path_params: str, headers: dict, **kwargs) -> Iterator[Response]: - self.sign_request("GET", path_params, headers, None) + self.sign_request(headers) return super()._get_stream(path_params, headers, **kwargs) def _ocs(self, method: str, path_params: str, headers: dict, data: Optional[bytes], **kwargs): - self.sign_request(method, path_params, headers, data) + self.sign_request(headers) return super()._ocs(method, path_params, headers, data, **kwargs) def _dav(self, method: str, path: str, headers: dict, data: Optional[bytes], **kwargs) -> Response: - self.sign_request(method, path, headers, data) + self.sign_request(headers) return super()._dav(method, path, headers, data, **kwargs) def _dav_stream(self, method: str, path: str, headers: dict, data: Optional[bytes], **kwargs) -> Iterator[Response]: - self.sign_request(method, path, headers, data) + self.sign_request(headers) return super()._dav_stream(method, path, headers, data, **kwargs) def _create_adapter(self) -> Client: adapter = Client(follow_redirects=True, limits=self.limits, verify=self.cfg.options.nc_cert) adapter.headers.update( { - "AE-VERSION": self.cfg.ae_version, + "AA-VERSION": self.cfg.ae_version, "EX-APP-ID": self.cfg.app_name, "EX-APP-VERSION": self.cfg.app_version, } ) return adapter - def sign_request(self, method: str, url_params: str, headers: dict, data: Optional[bytes]) -> None: - data_hash = xxh64() - if data and method != "GET": - data_hash.update(data) - - sign_headers = { - "AE-VERSION": self.adapter.headers.get("AE-VERSION"), - "EX-APP-ID": self.adapter.headers.get("EX-APP-ID"), - "EX-APP-VERSION": self.adapter.headers.get("EX-APP-VERSION"), - "NC-USER-ID": self.user, - "AE-DATA-HASH": data_hash.hexdigest(), - "AE-SIGN-TIME": str(int(datetime.now(timezone.utc).timestamp())), - } - if not sign_headers["NC-USER-ID"]: - sign_headers.pop("NC-USER-ID") - - request_to_sign = ( - method.encode("UTF-8") - + url_params.encode("UTF-8") - + dumps(sign_headers, separators=(",", ":")).encode("UTF-8") - ) - hmac_sign = hmac.new(self.cfg.app_secret, request_to_sign, digestmod=sha256) - headers.update( - { - "AE-SIGNATURE": hmac_sign.hexdigest(), - "AE-DATA-HASH": sign_headers["AE-DATA-HASH"], - "AE-SIGN-TIME": sign_headers["AE-SIGN-TIME"], - } - ) - if "NC-USER-ID" in sign_headers: - headers["NC-USER-ID"] = sign_headers["NC-USER-ID"] + def sign_request(self, headers: dict) -> None: + headers["AUTHORIZATION-APP-API"] = b64encode(f"{self.user}:{self.cfg.app_secret}".encode("UTF=8")) def sign_check(self, request: Request) -> None: - current_time = int(datetime.now(timezone.utc).timestamp()) headers = { - "AE-VERSION": request.headers.get("AE-VERSION", ""), + "AA-VERSION": request.headers.get("AA-VERSION", ""), "EX-APP-ID": request.headers.get("EX-APP-ID", ""), "EX-APP-VERSION": request.headers.get("EX-APP-VERSION", ""), - "NC-USER-ID": request.headers.get("NC-USER-ID", ""), - "AE-DATA-HASH": request.headers.get("AE-DATA-HASH", ""), - "AE-SIGN-TIME": request.headers.get("AE-SIGN-TIME", ""), + "AUTHORIZATION-APP-API": request.headers.get("AUTHORIZATION-APP-API", ""), } - if not headers["NC-USER-ID"]: - headers.pop("NC-USER-ID") empty_headers = [k for k, v in headers.items() if not v] if empty_headers: @@ -413,25 +369,9 @@ def sign_check(self, request: Request) -> None: if headers["EX-APP-VERSION"] != our_version: raise ValueError(f"Invalid EX-APP-VERSION:{headers['EX-APP-VERSION']} <=> {our_version}") - request_time = int(headers["AE-SIGN-TIME"]) - if request_time < current_time - 5 * 60 or request_time > current_time + 5 * 60: - raise ValueError(f"Invalid AE-SIGN-TIME:{request_time} <=> {current_time}") - - query_params = f"?{request.url.components.query}" if request.url.components.query else "" - request_to_sign = ( - request.method.upper() + request.url.components.path + query_params + dumps(headers, separators=(",", ":")) - ) - hmac_sign = hmac.new(self.cfg.app_secret, request_to_sign.encode("UTF-8"), digestmod=sha256).hexdigest() - request_ae_sign = request.headers.get("AE-SIGNATURE", "") - if hmac_sign != request_ae_sign: - raise ValueError(f"Invalid AE-SIGNATURE:{hmac_sign} != {request_ae_sign}") - - data_hash = xxh64() - data = asyncio.run(request.body()) - if data: - data_hash.update(data) - ae_data_hash = data_hash.hexdigest() - if ae_data_hash != headers["AE-DATA-HASH"]: - raise ValueError(f"Invalid AE-DATA-HASH:{ae_data_hash} !={headers['AE-DATA-HASH']}") if headers["EX-APP-ID"] != self.cfg.app_name: raise ValueError(f"Invalid EX-APP-ID:{headers['EX-APP-ID']} != {self.cfg.app_name}") + + app_secret = get_username_secret_from_headers(headers)[1] + if app_secret != self.cfg.app_secret: + raise ValueError(f"Invalid App secret:{app_secret} != {self.cfg.app_secret}") diff --git a/nc_py_api/_version.py b/nc_py_api/_version.py index 5ea9bef7..2805a2a7 100644 --- a/nc_py_api/_version.py +++ b/nc_py_api/_version.py @@ -1,3 +1,3 @@ """Version of nc_py_api.""" -__version__ = "0.1.0" +__version__ = "0.2.0.dev0" diff --git a/nc_py_api/apps.py b/nc_py_api/apps.py index 4a64c434..dfac3a76 100644 --- a/nc_py_api/apps.py +++ b/nc_py_api/apps.py @@ -112,7 +112,7 @@ def ex_app_get_list(self, enabled: bool = False) -> list[ExAppInfo]: :param enabled: Flag indicating whether to return only enabled applications or all applications. Default = **False**. """ - require_capabilities("app_ecosystem_v2", self._session.capabilities) + require_capabilities("app_api", self._session.capabilities) url_param = "enabled" if enabled else "all" r = self._session.ocs(method="GET", path=f"{self._session.ae_url}/ex-app/{url_param}") return [ExAppInfo(i) for i in r] diff --git a/nc_py_api/ex_app/integration_fastapi.py b/nc_py_api/ex_app/integration_fastapi.py index 2d52f433..8ea19d9a 100644 --- a/nc_py_api/ex_app/integration_fastapi.py +++ b/nc_py_api/ex_app/integration_fastapi.py @@ -8,15 +8,18 @@ from fastapi import Depends, FastAPI, HTTPException, Request, responses, status +from .._misc import get_username_secret_from_headers from ..nextcloud import NextcloudApp from ..talk_bot import TalkBotMessage, get_bot_secret def nc_app(request: Request) -> NextcloudApp: """Authentication handler for requests from Nextcloud to the application.""" - user = request.headers.get("NC-USER-ID", "") - request_id = request.headers.get("AE-REQUEST-ID", None) - headers = {"AE-REQUEST-ID": request_id} if request_id else {} + user = get_username_secret_from_headers( + {"AUTHORIZATION-APP-API": request.headers.get("AUTHORIZATION-APP-API", "")} + )[0] + request_id = request.headers.get("AA-REQUEST-ID", None) + headers = {"AA-REQUEST-ID": request_id} if request_id else {} nextcloud_app = NextcloudApp(user=user, headers=headers) if not nextcloud_app.request_sign_check(request): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) diff --git a/nc_py_api/ex_app/ui/files.py b/nc_py_api/ex_app/ui/files.py index 9b8c6ded..ebd5ec48 100644 --- a/nc_py_api/ex_app/ui/files.py +++ b/nc_py_api/ex_app/ui/files.py @@ -95,7 +95,7 @@ def __init__(self, session: NcSessionApp): def register(self, name: str, display_name: str, callback_url: str, **kwargs) -> None: """Registers the files a dropdown menu element.""" - require_capabilities("app_ecosystem_v2", self._session.capabilities) + require_capabilities("app_api", self._session.capabilities) params = { "fileActionMenuParams": { "name": name, @@ -112,7 +112,7 @@ def register(self, name: str, display_name: str, callback_url: str, **kwargs) -> def unregister(self, name: str, not_fail=True) -> None: """Removes files dropdown menu element.""" - require_capabilities("app_ecosystem_v2", self._session.capabilities) + require_capabilities("app_api", self._session.capabilities) params = {"fileActionMenuName": name} try: self._session.ocs(method="DELETE", path=f"{self._session.ae_url}/{self._ep_suffix}", json=params) diff --git a/nc_py_api/nextcloud.py b/nc_py_api/nextcloud.py index 4f6fc737..75cc0ce9 100644 --- a/nc_py_api/nextcloud.py +++ b/nc_py_api/nextcloud.py @@ -153,9 +153,9 @@ def log(self, log_lvl: LogLvl, content: str) -> None: :param log_lvl: level of the log, content belongs to. :param content: string to write into the log. """ - if self.check_capabilities("app_ecosystem_v2"): + if self.check_capabilities("app_api"): return - if int(log_lvl) < self.capabilities["app_ecosystem_v2"].get("loglevel", 0): + if int(log_lvl) < self.capabilities["app_api"].get("loglevel", 0): return self._session.ocs( method="POST", path=f"{self._session.ae_url}/log", json={"level": int(log_lvl), "message": content} @@ -170,9 +170,9 @@ def scope_allowed(self, scope: ApiScope) -> bool: Useful for applications that declare optional scopes to check if they are allowed. """ - if self.check_capabilities("app_ecosystem_v2"): + if self.check_capabilities("app_api"): return False - return scope in self.capabilities["app_ecosystem_v2"]["scopes"] + return scope in self.capabilities["app_api"]["scopes"] @property def user(self) -> str: @@ -206,7 +206,7 @@ def register_talk_bot(self, callback_url: str, display_name: str, description: s :param description: Optional description shown in the admin settings. :return: The secret used for signing requests. """ - require_capabilities("app_ecosystem_v2", self._session.capabilities) + require_capabilities("app_api", self._session.capabilities) require_capabilities("spreed.features.bots-v1", self._session.capabilities) params = { "name": display_name, @@ -222,7 +222,7 @@ def unregister_talk_bot(self, callback_url: str) -> bool: :param callback_url: URL suffix for fetching new messages. MUST be ``UNIQ`` for each bot the app provides. :return: The secret used for signing requests. """ - require_capabilities("app_ecosystem_v2", self._session.capabilities) + require_capabilities("app_api", self._session.capabilities) require_capabilities("spreed.features.bots-v1", self._session.capabilities) params = { "route": callback_url, diff --git a/nc_py_api/notifications.py b/nc_py_api/notifications.py index eea2af04..da17e90c 100644 --- a/nc_py_api/notifications.py +++ b/nc_py_api/notifications.py @@ -91,16 +91,16 @@ def create( raise NotImplementedError("Sending notifications is only supported for `App` mode.") if not subject: raise ValueError("`subject` cannot be empty string.") - require_capabilities(["app_ecosystem_v2", "notifications"], self._session.capabilities) + require_capabilities(["app_api", "notifications"], self._session.capabilities) if subject_params is None: subject_params = {} if message_params is None: message_params = {} params: dict = { "params": { - "object": "app_ecosystem_v2", + "object": "app_api", "object_id": random_string(56), - "subject_type": "app_ecosystem_v2_ex_app", + "subject_type": "app_api_ex_app", "subject_params": { "rich_subject": subject, "rich_subject_params": subject_params, diff --git a/pyproject.toml b/pyproject.toml index 2b08ebb8..fe8d3202 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,6 @@ dependencies = [ [project.optional-dependencies] app = [ "uvicorn[standard]>=0.23.2", - "xxhash>=3.3", ] bench = [ "matplotlib", diff --git a/scripts/ci_register.sh b/scripts/ci_register.sh index 75c14569..92d18f6b 100755 --- a/scripts/ci_register.sh +++ b/scripts/ci_register.sh @@ -3,7 +3,7 @@ # Parameters: # APP_ID, VERSION, SECRET, HOST, PORT -php occ app_ecosystem_v2:daemon:register manual_install "Manual Install" manual-install 0 0 0 -php occ app_ecosystem_v2:app:register "$1" manual_install --json-info \ +php occ app_api:daemon:register manual_install "Manual Install" manual-install 0 0 0 +php occ app_api:app:register "$1" manual_install --json-info \ "{\"appid\":\"$1\",\"name\":\"$1\",\"daemon_config_name\":\"manual_install\",\"version\":\"$2\",\"secret\":\"$3\",\"host\":\"$4\",\"scopes\":{\"required\":[\"SYSTEM\", \"FILES\", \"FILES_SHARING\"],\"optional\":[\"USER_INFO\", \"USER_STATUS\", \"NOTIFICATIONS\", \"WEATHER_STATUS\", \"TALK\", \"TALK_BOT\", \"ACTIVITIES\"]},\"port\":$5,\"protocol\":\"http\",\"system_app\":1}" \ -e --force-scopes diff --git a/scripts/dev_register.sh b/scripts/dev_register.sh index a36b754d..c06340b5 100644 --- a/scripts/dev_register.sh +++ b/scripts/dev_register.sh @@ -1,18 +1,18 @@ #!/bin/bash echo "removing 'manual_install' deploy daemon for $1 container" -docker exec "$1" sudo -u www-data php occ app_ecosystem_v2:daemon:unregister manual_install || true +docker exec "$1" sudo -u www-data php occ app_api:daemon:unregister manual_install || true echo "creating 'manual_install' deploy daemon for $1 container" -docker exec "$1" sudo -u www-data php occ app_ecosystem_v2:daemon:register \ +docker exec "$1" sudo -u www-data php occ app_api:daemon:register \ manual_install "Manual Install" manual-install 0 0 0 echo "unregistering nc_py_api as an app for $1 container" -docker exec "$1" sudo -u www-data php occ app_ecosystem_v2:app:unregister nc_py_api --silent || true +docker exec "$1" sudo -u www-data php occ app_api:app:unregister nc_py_api --silent || true echo "registering nc_py_api as an app for $1 container" NEXTCLOUD_URL="http://$2" APP_PORT=9009 APP_ID="nc_py_api" APP_SECRET="12345" APP_VERSION="1.0.0" \ python3 tests/_install.py > /dev/null 2>&1 & echo $! > /tmp/_install.pid python3 tests/_install_wait.py "http://localhost:9009/heartbeat" "\"status\":\"ok\"" 15 0.5 -docker exec "$1" sudo -u www-data php occ app_ecosystem_v2:app:register nc_py_api manual_install --json-info \ +docker exec "$1" sudo -u www-data php occ app_api:app:register nc_py_api manual_install --json-info \ "{\"appid\":\"nc_py_api\",\"name\":\"nc_py_api\",\"daemon_config_name\":\"manual_install\",\"version\":\"1.0.0\",\"secret\":\"12345\",\"host\":\"host.docker.internal\",\"scopes\":{\"required\":[\"SYSTEM\", \"FILES\", \"FILES_SHARING\"],\"optional\":[\"USER_INFO\", \"USER_STATUS\", \"NOTIFICATIONS\", \"WEATHER_STATUS\", \"TALK\", \"TALK_BOT\", \"ACTIVITIES\"]},\"port\":9009,\"protocol\":\"http\",\"system_app\":1}" \ -e --force-scopes cat /tmp/_install.pid diff --git a/tests/actual_tests/logs_test.py b/tests/actual_tests/logs_test.py index faca8edb..e9c2ca3f 100644 --- a/tests/actual_tests/logs_test.py +++ b/tests/actual_tests/logs_test.py @@ -33,12 +33,12 @@ def test_empty_log(nc_app): def test_loglvl_equal(nc_app): - current_log_lvl = nc_app.capabilities["app_ecosystem_v2"].get("loglevel", LogLvl.FATAL) + current_log_lvl = nc_app.capabilities["app_api"].get("loglevel", LogLvl.FATAL) nc_app.log(current_log_lvl, "log should be written") def test_loglvl_less(nc_app): - current_log_lvl = nc_app.capabilities["app_ecosystem_v2"].get("loglevel", LogLvl.FATAL) + current_log_lvl = nc_app.capabilities["app_api"].get("loglevel", LogLvl.FATAL) if current_log_lvl == LogLvl.DEBUG: pytest.skip("Log lvl to low") with mock.patch("tests.conftest.NC_APP._session._ocs") as _ocs: @@ -48,11 +48,11 @@ def test_loglvl_less(nc_app): assert _ocs.call_count > 0 -def test_log_without_app_ecosystem_v2(nc_app): +def test_log_without_app_api(nc_app): srv_capabilities = deepcopy(nc_app.capabilities) srv_version = deepcopy(nc_app.srv_version) - log_lvl = srv_capabilities["app_ecosystem_v2"].pop("loglevel") - srv_capabilities.pop("app_ecosystem_v2") + log_lvl = srv_capabilities["app_api"].pop("loglevel") + srv_capabilities.pop("app_api") patched_capabilities = {"capabilities": srv_capabilities, "version": srv_version} with ( mock.patch.dict("tests.conftest.NC_APP._session._capabilities", patched_capabilities, clear=True), diff --git a/tests/actual_tests/misc_test.py b/tests/actual_tests/misc_test.py index 8cf398e6..6da3c613 100644 --- a/tests/actual_tests/misc_test.py +++ b/tests/actual_tests/misc_test.py @@ -28,16 +28,16 @@ def test_nc_exception_to_str(): def test_require_capabilities(nc_app): - require_capabilities("app_ecosystem_v2", nc_app.capabilities) - require_capabilities(["app_ecosystem_v2", "theming"], nc_app.capabilities) + require_capabilities("app_api", nc_app.capabilities) + require_capabilities(["app_api", "theming"], nc_app.capabilities) with pytest.raises(NextcloudException): require_capabilities("non_exist_capability", nc_app.capabilities) with pytest.raises(NextcloudException): - require_capabilities(["non_exist_capability", "app_ecosystem_v2"], nc_app.capabilities) + require_capabilities(["non_exist_capability", "app_api"], nc_app.capabilities) with pytest.raises(NextcloudException): - require_capabilities(["non_exist_capability", "non_exist_capability2", "app_ecosystem_v2"], nc_app.capabilities) + require_capabilities(["non_exist_capability", "non_exist_capability2", "app_api"], nc_app.capabilities) with pytest.raises(NextcloudException): - require_capabilities("app_ecosystem_v2.non_exist_capability", nc_app.capabilities) + require_capabilities("app_api.non_exist_capability", nc_app.capabilities) def test_config_get_value(): diff --git a/tests/actual_tests/nc_app_test.py b/tests/actual_tests/nc_app_test.py index af3de348..4d372e6c 100644 --- a/tests/actual_tests/nc_app_test.py +++ b/tests/actual_tests/nc_app_test.py @@ -25,13 +25,13 @@ def test_app_cfg(nc_app): def test_scope_allow_app_ecosystem_disabled(nc_client, nc_app): assert nc_app.scope_allowed(ApiScope.FILES) - nc_client.apps.disable("app_ecosystem_v2") + nc_client.apps.disable("app_api") try: assert nc_app.scope_allowed(ApiScope.FILES) nc_app.update_server_info() assert not nc_app.scope_allowed(ApiScope.FILES) finally: - nc_client.apps.enable("app_ecosystem_v2") + nc_client.apps.enable("app_api") nc_app.update_server_info() diff --git a/tests/conftest.py b/tests/conftest.py index da27e9d9..116f1a49 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ NC_APP = None else: NC_APP = NextcloudApp(user="admin") - if "app_ecosystem_v2" not in NC_APP.capabilities: + if "app_api" not in NC_APP.capabilities: NC_APP = None if NC_CLIENT is None and NC_APP is None: raise EnvironmentError("Tests require at least Nextcloud or NextcloudApp.") diff --git a/tests/gfixture_set_env.py b/tests/gfixture_set_env.py index bc6f5542..d0987555 100644 --- a/tests/gfixture_set_env.py +++ b/tests/gfixture_set_env.py @@ -3,7 +3,7 @@ if not environ.get("CI", False): # For local tests environ["NC_AUTH_USER"] = "admin" environ["NC_AUTH_PASS"] = "admin" # "MrtGY-KfY24-iiDyg-cr4n4-GLsNZ" - environ["NEXTCLOUD_URL"] = environ.get("NEXTCLOUD_URL", "http://nextcloud.local") + environ["NEXTCLOUD_URL"] = environ.get("NEXTCLOUD_URL", "http://stable27.local") environ["APP_ID"] = "nc_py_api" environ["APP_VERSION"] = "1.0.0" environ["APP_SECRET"] = "12345" From 158364cbe3366523265e8f23b67bad11e8d27e81 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Mon, 11 Sep 2023 20:37:54 +0300 Subject: [PATCH 2/3] renaming all `ae` to `aa` (2) Signed-off-by: Alexander Piskun --- .github/workflows/analysis-coverage.yml | 2 +- nc_py_api/_session.py | 12 +-- nc_py_api/nextcloud.py | 4 +- tests/_app_security_checks.py | 104 ++++++++---------------- tests/conftest.py | 2 +- 5 files changed, 46 insertions(+), 78 deletions(-) diff --git a/.github/workflows/analysis-coverage.yml b/.github/workflows/analysis-coverage.yml index 95fedb03..758bfce2 100644 --- a/.github/workflows/analysis-coverage.yml +++ b/.github/workflows/analysis-coverage.yml @@ -705,7 +705,7 @@ jobs: coverage run --data-file=.coverage.at_the_end -m pytest tests/_tests_at_the_end.py coverage combine && coverage xml && coverage html env: - SKIP_AE_TESTS: 1 + SKIP_AA_TESTS: 1 NPA_NC_CERT: '' - name: HTML coverage to artifacts diff --git a/nc_py_api/_session.py b/nc_py_api/_session.py index ab646d68..f9d8a0a7 100644 --- a/nc_py_api/_session.py +++ b/nc_py_api/_session.py @@ -109,8 +109,8 @@ def __init__(self, **kwargs): class AppConfig(BasicConfig): """Application configuration.""" - ae_version: str - """AppEcosystem version""" + aa_version: str + """AppAPI version""" app_name: str """Application ID""" app_version: str @@ -120,9 +120,9 @@ class AppConfig(BasicConfig): def __init__(self, **kwargs): super().__init__(**kwargs) - self.ae_version = self._get_config_value("ae_version", raise_not_found=False, **kwargs) - if not self.ae_version: - self.ae_version = "1.0.0" + self.aa_version = self._get_config_value("aa_version", raise_not_found=False, **kwargs) + if not self.aa_version: + self.aa_version = "1.0.0" self.app_name = self._get_config_value("app_id", **kwargs) self.app_version = self._get_config_value("app_version", **kwargs) self.app_secret = self._get_config_value("app_secret", **kwargs) @@ -343,7 +343,7 @@ def _create_adapter(self) -> Client: adapter = Client(follow_redirects=True, limits=self.limits, verify=self.cfg.options.nc_cert) adapter.headers.update( { - "AA-VERSION": self.cfg.ae_version, + "AA-VERSION": self.cfg.aa_version, "EX-APP-ID": self.cfg.app_name, "EX-APP-VERSION": self.cfg.app_version, } diff --git a/nc_py_api/nextcloud.py b/nc_py_api/nextcloud.py index 75cc0ce9..6cf07048 100644 --- a/nc_py_api/nextcloud.py +++ b/nc_py_api/nextcloud.py @@ -193,13 +193,13 @@ def user(self, value: str): @property def app_cfg(self) -> AppConfig: - """Returns deploy config, with AppEcosystem version, Application version and name.""" + """Returns deploy config, with AppAPI version, Application version and name.""" return self._session.cfg def register_talk_bot(self, callback_url: str, display_name: str, description: str = "") -> tuple[str, str]: """Registers Talk BOT. - .. note:: AppEcosystem will add a record in a case of successful registration to the ``appconfig_ex`` table. + .. note:: AppAPI will add a record in a case of successful registration to the ``appconfig_ex`` table. :param callback_url: URL suffix for fetching new messages. MUST be ``UNIQ`` for each bot the app provides. :param display_name: The name under which the messages will be posted. diff --git a/tests/_app_security_checks.py b/tests/_app_security_checks.py index 0132e5b4..5990ab6f 100644 --- a/tests/_app_security_checks.py +++ b/tests/_app_security_checks.py @@ -1,25 +1,12 @@ -import hmac -from datetime import datetime, timezone -from hashlib import sha256 -from json import dumps +from base64 import b64encode from os import environ from sys import argv import requests -from xxhash import xxh64 -def sign_request(url: str, req_headers: dict, time: int = 0): - data_hash = xxh64() - req_headers["AE-DATA-HASH"] = data_hash.hexdigest() - if time: - req_headers["AE-SIGN-TIME"] = str(time) - else: - req_headers["AE-SIGN-TIME"] = str(int(datetime.now(timezone.utc).timestamp())) - req_headers.pop("AE-SIGNATURE", None) - request_to_sign = "PUT" + url + dumps(req_headers, separators=(",", ":")) - hmac_sign = hmac.new(environ["APP_SECRET"].encode("UTF-8"), request_to_sign.encode("UTF-8"), digestmod=sha256) - req_headers["AE-SIGNATURE"] = hmac_sign.hexdigest() +def sign_request(req_headers: dict, user: str = ""): + req_headers["AUTHORIZATION-APP-API"] = b64encode(f"{user}:{environ['APP_SECRET']}".encode("UTF=8")) # params: app base url @@ -35,55 +22,36 @@ def sign_request(url: str, req_headers: dict, time: int = 0): "EX-APP-VERSION": environ.get("APP_VERSION", "1.0.0"), } ) - sign_request("/sec_check?value=1", headers) - result = requests.put(request_url, headers=headers) - assert result.status_code == 200 - # Invalid AE-SIGNATURE - request_url = argv[1] + "/sec_check?value=0" - result = requests.put(request_url, headers=headers) - assert result.status_code == 401 - sign_request("/sec_check?value=0", headers) - result = requests.put(request_url, headers=headers) - assert result.status_code == 200 - # Invalid EX-APP-ID - old_app_name = headers["EX-APP-ID"] - headers["EX-APP-ID"] = "unknown_app" - sign_request("/sec_check?value=0", headers) - result = requests.put(request_url, headers=headers) - assert result.status_code == 401 - headers["EX-APP-ID"] = old_app_name - sign_request("/sec_check?value=0", headers) - result = requests.put(request_url, headers=headers) - assert result.status_code == 200 - # Invalid AE-DATA-HASH - result = requests.put(request_url, headers=headers, data=b"some_data") - assert result.status_code == 401 - # Invalid EX-APP-VERSION - sign_request("/sec_check?value=0", headers) - result = requests.put(request_url, headers=headers) - assert result.status_code == 200 - old_version = headers["EX-APP-VERSION"] - headers["EX-APP-VERSION"] = "999.0.0" - sign_request("/sec_check?value=0", headers) - result = requests.put(request_url, headers=headers) - assert result.status_code == 401 - headers["EX-APP-VERSION"] = old_version - sign_request("/sec_check?value=0", headers) - result = requests.put(request_url, headers=headers) - assert result.status_code == 200 - # Sign time - sign_request("/sec_check?value=0", headers, time=int(datetime.now(timezone.utc).timestamp())) - result = requests.put(request_url, headers=headers) - assert result.status_code == 200 - sign_request("/sec_check?value=0", headers, time=int(datetime.now(timezone.utc).timestamp() - 4.0 * 60)) - result = requests.put(request_url, headers=headers) - assert result.status_code == 200 - sign_request("/sec_check?value=0", headers, time=int(datetime.now(timezone.utc).timestamp() - 5.0 * 60 - 3.0)) - result = requests.put(request_url, headers=headers) - assert result.status_code == 401 - sign_request("/sec_check?value=0", headers, time=int(datetime.now(timezone.utc).timestamp() + 4.0 * 60)) - result = requests.put(request_url, headers=headers) - assert result.status_code == 200 - sign_request("/sec_check?value=0", headers, time=int(datetime.now(timezone.utc).timestamp() + 5.0 * 60 + 3.0)) - result = requests.put(request_url, headers=headers) - assert result.status_code == 401 + sign_request(headers) + result = requests.put(request_url, headers=headers) + assert result.status_code == 200 + # # Invalid AA-SIGNATURE + # request_url = argv[1] + "/sec_check?value=0" + # result = requests.put(request_url, headers=headers) + # assert result.status_code == 401 + # sign_request(headers) + # result = requests.put(request_url, headers=headers) + # assert result.status_code == 200 + # # Invalid EX-APP-ID + # old_app_name = headers["EX-APP-ID"] + # headers["EX-APP-ID"] = "unknown_app" + # sign_request(headers) + # result = requests.put(request_url, headers=headers) + # assert result.status_code == 401 + # headers["EX-APP-ID"] = old_app_name + # sign_request(headers) + # result = requests.put(request_url, headers=headers) + # assert result.status_code == 200 + # # Invalid EX-APP-VERSION + # sign_request(headers) + # result = requests.put(request_url, headers=headers) + # assert result.status_code == 200 + # old_version = headers["EX-APP-VERSION"] + # headers["EX-APP-VERSION"] = "999.0.0" + # sign_request(headers) + # result = requests.put(request_url, headers=headers) + # assert result.status_code == 401 + # headers["EX-APP-VERSION"] = old_version + # sign_request(headers) + # result = requests.put(request_url, headers=headers) + # assert result.status_code == 200 diff --git a/tests/conftest.py b/tests/conftest.py index 116f1a49..742f1fa7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ _TEST_FAILED_INCREMENTAL: dict[str, dict[tuple[int, ...], str]] = {} NC_CLIENT = None if environ.get("SKIP_NC_CLIENT_TESTS", False) else Nextcloud() -if environ.get("SKIP_AE_TESTS", False): +if environ.get("SKIP_AA_TESTS", False): NC_APP = None else: NC_APP = NextcloudApp(user="admin") From 62b565659cc99dae7c8d2660f044c831eaa28554 Mon Sep 17 00:00:00 2001 From: Alexander Piskun Date: Tue, 12 Sep 2023 22:51:44 +0300 Subject: [PATCH 3/3] adjusted forgotten places (3) Signed-off-by: Alexander Piskun --- nc_py_api/ex_app/defs.py | 2 ++ tests/actual_tests/nc_app_test.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nc_py_api/ex_app/defs.py b/nc_py_api/ex_app/defs.py index 5dfd5210..d34156b0 100644 --- a/nc_py_api/ex_app/defs.py +++ b/nc_py_api/ex_app/defs.py @@ -39,3 +39,5 @@ class ApiScope(enum.IntEnum): """Allows access to Talk API endpoints.""" TALK_BOT = 60 """Allows to register Talk Bots.""" + ACTIVITIES = 110 + """Activity App endpoints.""" diff --git a/tests/actual_tests/nc_app_test.py b/tests/actual_tests/nc_app_test.py index 4d372e6c..4aca9430 100644 --- a/tests/actual_tests/nc_app_test.py +++ b/tests/actual_tests/nc_app_test.py @@ -20,7 +20,7 @@ def test_app_cfg(nc_app): app_cfg = nc_app.app_cfg assert app_cfg.app_name == environ["APP_ID"] assert app_cfg.app_version == environ["APP_VERSION"] - assert app_cfg.app_secret == environ["APP_SECRET"].encode("UTF-8") + assert app_cfg.app_secret == environ["APP_SECRET"] def test_scope_allow_app_ecosystem_disabled(nc_client, nc_app):