From 0ad703e241b8214b9e2109dcdb768f611b5e6315 Mon Sep 17 00:00:00 2001 From: fmrsabino <3332770+fmrsabino@users.noreply.github.com> Date: Wed, 5 Jan 2022 14:00:18 +0100 Subject: [PATCH 01/17] Remove serializer validation when ENABLE_OWNERS_ENDPOINT is false --- safe_transaction_service/history/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/safe_transaction_service/history/views.py b/safe_transaction_service/history/views.py index 6a168d602..49b7cb430 100644 --- a/safe_transaction_service/history/views.py +++ b/safe_transaction_service/history/views.py @@ -1042,6 +1042,7 @@ def get_owners(self, address): def get_owners_empty(self): serializer = self.serializer_class(data={"safes": []}) + assert serializer.is_valid() return Response(status=status.HTTP_200_OK, data=serializer.data) From c5e7bd1382ab599ac37286a6cf43ecf18f0217ef Mon Sep 17 00:00:00 2001 From: fmrsabino <3332770+fmrsabino@users.noreply.github.com> Date: Wed, 5 Jan 2022 14:04:49 +0100 Subject: [PATCH 02/17] Bump service version to 3.4.24 --- safe_transaction_service/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/safe_transaction_service/__init__.py b/safe_transaction_service/__init__.py index 16d7d3ee9..b3b01ae12 100644 --- a/safe_transaction_service/__init__.py +++ b/safe_transaction_service/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.4.23" +__version__ = "3.4.24" __version_info__ = tuple( [ int(num) if num.isdigit() else num From e60879deb721466c609159bcae3d2385a9304144 Mon Sep 17 00:00:00 2001 From: Frederico Sabino <3332770+fmrsabino@users.noreply.github.com> Date: Fri, 7 Jan 2022 12:56:23 +0100 Subject: [PATCH 03/17] Require token auth for OwnersView (#611) - OwnersView now requires authentication via a Token - This Token should be set for users who want to access this endpoint (this can be done via the admin interface) - Given a valid token example-token, the token can be used by adding the following header to the request for this endpoint: Authorisation: Token example-token - Unauthenticated requests return a 401 - Reverted changes made for releases 3.4.22, 3.4.23 and 3.4.24 --- README.md | 23 +++++++++++++++ config/settings/base.py | 8 ++++- safe_transaction_service/__init__.py | 2 +- safe_transaction_service/history/admin.py | 9 ++++++ .../history/tests/test_views.py | 29 ++++++++++++++++--- safe_transaction_service/history/views.py | 15 +++------- 6 files changed, 69 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 5aac75347..996cc658a 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,29 @@ A user must be created to get access: docker exec -it safe-transaction-service_web_1 python manage.py createsuperuser ``` +## Authenticated Endpoints + +Currently most public endpoints do not have any authentication in place. However some endpoiints do require authentication in order to be used such as `/api/v1/owners//safes/`. For accessing such endpoints a token needs to be created (which will be associated to a specific user). + +### 1. Create an authorization token: + +1a. This can be done via the admin interface under `/admin/authtoken/tokenproxy/` + +OR + +1b. By using the following command: + +```shell +python manage.py drf_create_token +``` + +### 2. Use the generated token to access authenticated endpoints: + + +```shell +curl -X GET "http://127.0.0.1:8000/api/v1/owners//safes/" -H 'Authorization: Token ' +``` + ## Safe Contract ABIs and addresses - [v1.3.0](https://github.com/gnosis/safe-deployments/blob/main/src/assets/v1.3.0/gnosis_safe.json) - [v1.3.0 L2](https://github.com/gnosis/safe-deployments/blob/main/src/assets/v1.3.0/gnosis_safe_l2.json) diff --git a/config/settings/base.py b/config/settings/base.py index 54f6949c4..035b9f4a7 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -83,6 +83,7 @@ "rest_framework", "drf_yasg", "django_s3_storage", + "rest_framework.authtoken", ] LOCAL_APPS = [ "safe_transaction_service.contracts.apps.ContractsConfig", @@ -415,4 +416,9 @@ ETHERSCAN_API_KEY = env("ETHERSCAN_API_KEY", default=None) IPFS_GATEWAY = env("IPFS_GATEWAY", default="https://cloudflare-ipfs.com/") -ENABLE_OWNERS_ENDPOINT = env.bool("ENABLE_OWNERS_ENDPOINT", default=True) + +SWAGGER_SETTINGS = { + "SECURITY_DEFINITIONS": { + "api_key": {"type": "apiKey", "in": "header", "name": "Authorization"} + }, +} diff --git a/safe_transaction_service/__init__.py b/safe_transaction_service/__init__.py index b3b01ae12..538cfdd78 100644 --- a/safe_transaction_service/__init__.py +++ b/safe_transaction_service/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.4.24" +__version__ = "3.4.25" __version_info__ = tuple( [ int(num) if num.isdigit() else num diff --git a/safe_transaction_service/history/admin.py b/safe_transaction_service/history/admin.py index f4812160f..c0177c9b2 100644 --- a/safe_transaction_service/history/admin.py +++ b/safe_transaction_service/history/admin.py @@ -6,6 +6,7 @@ from django.db.transaction import atomic from hexbytes import HexBytes +from rest_framework.authtoken.admin import TokenAdmin from gnosis.eth import EthereumClientProvider @@ -30,6 +31,14 @@ ) from .services import IndexServiceProvider +# By default, TokenAdmin doesn't allow key edition +# IFF you have a service that requests from multiple safe-transaction-service +# you might want to share that key for convenience between instances. +TokenAdmin.fields = ( + "user", + "key", +) + # Inline objects ------------------------------ class ERC20TransferInline(admin.TabularInline): diff --git a/safe_transaction_service/history/tests/test_views.py b/safe_transaction_service/history/tests/test_views.py index 58c0b9b51..78772e945 100644 --- a/safe_transaction_service/history/tests/test_views.py +++ b/safe_transaction_service/history/tests/test_views.py @@ -3,6 +3,7 @@ from unittest import mock from unittest.mock import MagicMock +from django.contrib.auth.models import User from django.urls import reverse from django.utils import timezone @@ -2839,21 +2840,39 @@ def test_analytics_multisig_txs_by_safe_view(self): ], ) + def test_owners_without_auth(self): + owner_address = Account.create().address + + response = self.client.get(reverse("v1:history:owners", args=(owner_address,))) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + def test_owners_view(self): + from rest_framework.authtoken.models import Token + + user = User.objects.create_user("test", "test@test.com", "test") + token = Token.objects.create(user=user, key="test_token") + auth_header = f"Token {token.key}" invalid_address = "0x2A" response = self.client.get( - reverse("v1:history:owners", args=(invalid_address,)) + reverse("v1:history:owners", args=(invalid_address,)), + HTTP_AUTHORIZATION=auth_header, ) self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) owner_address = Account.create().address - response = self.client.get(reverse("v1:history:owners", args=(owner_address,))) + response = self.client.get( + reverse("v1:history:owners", args=(owner_address,)), + HTTP_AUTHORIZATION=auth_header, + ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["safes"], []) safe_status = SafeStatusFactory(owners=[owner_address]) response = self.client.get( - reverse("v1:history:owners", args=(owner_address,)), format="json" + reverse("v1:history:owners", args=(owner_address,)), + format="json", + HTTP_AUTHORIZATION=auth_header, ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["safes"], [safe_status.address]) @@ -2861,7 +2880,9 @@ def test_owners_view(self): safe_status_2 = SafeStatusFactory(owners=[owner_address]) SafeStatusFactory() # Test that other SafeStatus don't appear response = self.client.get( - reverse("v1:history:owners", args=(owner_address,)), format="json" + reverse("v1:history:owners", args=(owner_address,)), + format="json", + HTTP_AUTHORIZATION=auth_header, ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertCountEqual( diff --git a/safe_transaction_service/history/views.py b/safe_transaction_service/history/views.py index 49b7cb430..b516e5b28 100644 --- a/safe_transaction_service/history/views.py +++ b/safe_transaction_service/history/views.py @@ -11,6 +11,7 @@ from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import status +from rest_framework.authentication import TokenAuthentication from rest_framework.filters import OrderingFilter from rest_framework.generics import ( DestroyAPIView, @@ -20,6 +21,7 @@ RetrieveAPIView, get_object_or_404, ) +from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.views import APIView @@ -1006,6 +1008,8 @@ class MasterCopiesView(ListAPIView): class OwnersView(APIView): + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] serializer_class = serializers.OwnerResponseSerializer @swagger_auto_schema( @@ -1029,22 +1033,11 @@ def get(self, request, address, *args, **kwargs): }, ) - if settings.ENABLE_OWNERS_ENDPOINT: - return self.get_owners(address) - else: - return self.get_owners_empty() - - def get_owners(self, address): safes_for_owner = SafeStatus.objects.addresses_for_owner(address) serializer = self.serializer_class(data={"safes": safes_for_owner}) assert serializer.is_valid() return Response(status=status.HTTP_200_OK, data=serializer.data) - def get_owners_empty(self): - serializer = self.serializer_class(data={"safes": []}) - assert serializer.is_valid() - return Response(status=status.HTTP_200_OK, data=serializer.data) - class DataDecoderView(GenericAPIView): def get_serializer_class(self): From 80ee7ad9f7ffed9f94480f6c34d478ecbf3f6670 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jan 2022 06:00:58 +0000 Subject: [PATCH 04/17] Bump celery from 5.2.1 to 5.2.3 Bumps [celery](https://github.com/celery/celery) from 5.2.1 to 5.2.3. - [Release notes](https://github.com/celery/celery/releases) - [Changelog](https://github.com/celery/celery/blob/master/Changelog.rst) - [Commits](https://github.com/celery/celery/compare/v5.2.1...v5.2.3) --- updated-dependencies: - dependency-name: celery dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 27355c099..ed67ad22b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cachetools==4.2.4 -celery==5.2.1 +celery==5.2.3 django==3.2.10 django-authtools==1.7.0 django-cache-memoize==0.1.10 From 388a8e158da527c612fd8e947c3554b7bc8b2fca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Jan 2022 06:00:51 +0000 Subject: [PATCH 05/17] Bump pillow from 8.4.0 to 9.0.0 Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.4.0 to 9.0.0. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/8.4.0...9.0.0) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ed67ad22b..c7300a5b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ gnosis-py[django]==3.7.5 gunicorn[gevent]==20.1.0 hexbytes==0.2.2 packaging>=21.0 -pillow==8.4.0 +pillow==9.0.0 psycogreen==1.0.2 psycopg2==2.9.2 redis==4.0.2 From 70544a2aabf90cd8e36e8bdb3df30ab8d52f0d4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Dec 2021 06:00:36 +0000 Subject: [PATCH 06/17] Bump redis from 4.0.2 to 4.1.0 Bumps [redis](https://github.com/redis/redis-py) from 4.0.2 to 4.1.0. - [Release notes](https://github.com/redis/redis-py/releases) - [Changelog](https://github.com/redis/redis-py/blob/master/CHANGES) - [Commits](https://github.com/redis/redis-py/compare/v4.0.2...v4.1.0) --- updated-dependencies: - dependency-name: redis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c7300a5b2..03ad06934 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,6 +27,6 @@ packaging>=21.0 pillow==9.0.0 psycogreen==1.0.2 psycopg2==2.9.2 -redis==4.0.2 +redis==4.1.0 requests==2.26.0 web3==5.24.0 From 8f2cdc206dd0b38170e781d611f29c5fa3b45c70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 09:18:34 +0000 Subject: [PATCH 07/17] Bump mypy from 0.930 to 0.931 Bumps [mypy](https://github.com/python/mypy) from 0.930 to 0.931. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.930...v0.931) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 3e1c7b629..d699e4ab8 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -3,7 +3,7 @@ coverage==6.2 django-stubs==1.9.0 factory-boy==3.2.1 faker==10.0.0 -mypy==0.930 +mypy==0.931 pytest==6.2.5 pytest-celery==0.0.0 pytest-django==4.5.2 From 7dd1fb9f3197d2d2023beade209e9b9eb41a94e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 09:28:50 +0000 Subject: [PATCH 08/17] Bump faker from 10.0.0 to 11.3.0 Bumps [faker](https://github.com/joke2k/faker) from 10.0.0 to 11.3.0. - [Release notes](https://github.com/joke2k/faker/releases) - [Changelog](https://github.com/joke2k/faker/blob/master/CHANGELOG.md) - [Commits](https://github.com/joke2k/faker/compare/v10.0.0...v11.3.0) --- updated-dependencies: - dependency-name: faker dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index d699e4ab8..6576f73ea 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ coverage==6.2 django-stubs==1.9.0 factory-boy==3.2.1 -faker==10.0.0 +faker==11.3.0 mypy==0.931 pytest==6.2.5 pytest-celery==0.0.0 From 682a7f3f1b419d263029e19a599a7b4179e4ed9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 09:18:18 +0000 Subject: [PATCH 09/17] Bump psycopg2 from 2.9.2 to 2.9.3 Bumps [psycopg2](https://github.com/psycopg/psycopg2) from 2.9.2 to 2.9.3. - [Release notes](https://github.com/psycopg/psycopg2/releases) - [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS) - [Commits](https://github.com/psycopg/psycopg2/commits) --- updated-dependencies: - dependency-name: psycopg2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 03ad06934..fc37a9993 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,7 +26,7 @@ hexbytes==0.2.2 packaging>=21.0 pillow==9.0.0 psycogreen==1.0.2 -psycopg2==2.9.2 +psycopg2==2.9.3 redis==4.1.0 requests==2.26.0 web3==5.24.0 From a56dc7ef6f8a549877a474cbee958199b7dd5852 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Fri, 24 Dec 2021 11:40:52 +0100 Subject: [PATCH 10/17] Add CLA to github actions --- .github/workflows/cla.yml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/cla.yml diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 000000000..cf4b401d7 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,36 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [ created ] + pull_request_target: + types: [ opened,closed,synchronize ] + +jobs: + CLAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + # Beta Release + uses: cla-assistant/github-action@v2.1.3-beta + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # the below token should have repo scope and must be manually added by you in the repository's secret + PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + with: + path-to-signatures: 'signatures/version1/cla.json' + path-to-document: 'https://gnosis-safe.io/cla/' + # branch should not be protected + branch: 'cla-signatures' + allowlist: jpalvarezl,fmrsabino,luarx,rmeissner,Uxio0,*bot # may need to update this expression if we add new bots + + #below are the optional inputs - If the optional inputs are not given, then default values will be taken + #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) + #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) + #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' + #signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo' + #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' + #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' + #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' + #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) + #use-dco-flag: true - If you are using DCO instead of CLA From 17f8a50a3710a33b93b7453c10374805dc31efd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jan 2022 09:34:15 +0000 Subject: [PATCH 11/17] Bump requests from 2.26.0 to 2.27.1 Bumps [requests](https://github.com/psf/requests) from 2.26.0 to 2.27.1. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.26.0...v2.27.1) --- updated-dependencies: - dependency-name: requests dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fc37a9993..2c1e1e97f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,5 +28,5 @@ pillow==9.0.0 psycogreen==1.0.2 psycopg2==2.9.3 redis==4.1.0 -requests==2.26.0 +requests==2.27.1 web3==5.24.0 From a269e32178779d040c623b702614ee16cee1c8a2 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Mon, 10 Jan 2022 12:33:30 +0100 Subject: [PATCH 12/17] Revert remove of IgnoreSucceededNone log filter --- config/settings/base.py | 4 ++++ safe_transaction_service/utils/tests/test_loggers.py | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index 035b9f4a7..a82e424c4 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -265,6 +265,9 @@ "disable_existing_loggers": False, "filters": { "require_debug_false": {"()": "django.utils.log.RequireDebugFalse"}, + "ignore_succeeded_none": { + "()": "safe_transaction_service.utils.loggers.IgnoreSucceededNone" + }, }, "formatters": { "short": {"format": "%(asctime)s %(message)s"}, @@ -294,6 +297,7 @@ }, "celery_console": { "level": "DEBUG", + "filters": [] if DEBUG else ["ignore_succeeded_none"], "class": "logging.StreamHandler", "formatter": "celery_verbose", }, diff --git a/safe_transaction_service/utils/tests/test_loggers.py b/safe_transaction_service/utils/tests/test_loggers.py index 07bdecd89..c8791b112 100644 --- a/safe_transaction_service/utils/tests/test_loggers.py +++ b/safe_transaction_service/utils/tests/test_loggers.py @@ -2,8 +2,6 @@ from django.test import TestCase -import pytest - from ..loggers import IgnoreCheckUrl, IgnoreSucceededNone @@ -23,7 +21,6 @@ def test_ignore_check_url(self): self.assertFalse(ignore_check_url.filter(check_log)) self.assertTrue(ignore_check_url.filter(other_log)) - @pytest.mark.skip(reason="Filter is disabled temporarily") # TODO def test_ignore_succeeded_none(self): name = "name" level = 1 From cc5a3dc054b0ca0e24605bd3f283cfe22d3c5ebf Mon Sep 17 00:00:00 2001 From: Frederico Sabino <3332770+fmrsabino@users.noreply.github.com> Date: Mon, 10 Jan 2022 13:04:38 +0100 Subject: [PATCH 13/17] Disable authentication in OwnersView (#622) - OwnersView no longer requires authentication - Tokens can still be created as before (the API token feature was not removed) --- README.md | 23 --------------- .../history/tests/test_views.py | 29 +++---------------- safe_transaction_service/history/views.py | 4 --- 3 files changed, 4 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 996cc658a..5aac75347 100644 --- a/README.md +++ b/README.md @@ -141,29 +141,6 @@ A user must be created to get access: docker exec -it safe-transaction-service_web_1 python manage.py createsuperuser ``` -## Authenticated Endpoints - -Currently most public endpoints do not have any authentication in place. However some endpoiints do require authentication in order to be used such as `/api/v1/owners//safes/`. For accessing such endpoints a token needs to be created (which will be associated to a specific user). - -### 1. Create an authorization token: - -1a. This can be done via the admin interface under `/admin/authtoken/tokenproxy/` - -OR - -1b. By using the following command: - -```shell -python manage.py drf_create_token -``` - -### 2. Use the generated token to access authenticated endpoints: - - -```shell -curl -X GET "http://127.0.0.1:8000/api/v1/owners//safes/" -H 'Authorization: Token ' -``` - ## Safe Contract ABIs and addresses - [v1.3.0](https://github.com/gnosis/safe-deployments/blob/main/src/assets/v1.3.0/gnosis_safe.json) - [v1.3.0 L2](https://github.com/gnosis/safe-deployments/blob/main/src/assets/v1.3.0/gnosis_safe_l2.json) diff --git a/safe_transaction_service/history/tests/test_views.py b/safe_transaction_service/history/tests/test_views.py index 78772e945..58c0b9b51 100644 --- a/safe_transaction_service/history/tests/test_views.py +++ b/safe_transaction_service/history/tests/test_views.py @@ -3,7 +3,6 @@ from unittest import mock from unittest.mock import MagicMock -from django.contrib.auth.models import User from django.urls import reverse from django.utils import timezone @@ -2840,39 +2839,21 @@ def test_analytics_multisig_txs_by_safe_view(self): ], ) - def test_owners_without_auth(self): - owner_address = Account.create().address - - response = self.client.get(reverse("v1:history:owners", args=(owner_address,))) - - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - def test_owners_view(self): - from rest_framework.authtoken.models import Token - - user = User.objects.create_user("test", "test@test.com", "test") - token = Token.objects.create(user=user, key="test_token") - auth_header = f"Token {token.key}" invalid_address = "0x2A" response = self.client.get( - reverse("v1:history:owners", args=(invalid_address,)), - HTTP_AUTHORIZATION=auth_header, + reverse("v1:history:owners", args=(invalid_address,)) ) self.assertEqual(response.status_code, status.HTTP_422_UNPROCESSABLE_ENTITY) owner_address = Account.create().address - response = self.client.get( - reverse("v1:history:owners", args=(owner_address,)), - HTTP_AUTHORIZATION=auth_header, - ) + response = self.client.get(reverse("v1:history:owners", args=(owner_address,))) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["safes"], []) safe_status = SafeStatusFactory(owners=[owner_address]) response = self.client.get( - reverse("v1:history:owners", args=(owner_address,)), - format="json", - HTTP_AUTHORIZATION=auth_header, + reverse("v1:history:owners", args=(owner_address,)), format="json" ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["safes"], [safe_status.address]) @@ -2880,9 +2861,7 @@ def test_owners_view(self): safe_status_2 = SafeStatusFactory(owners=[owner_address]) SafeStatusFactory() # Test that other SafeStatus don't appear response = self.client.get( - reverse("v1:history:owners", args=(owner_address,)), - format="json", - HTTP_AUTHORIZATION=auth_header, + reverse("v1:history:owners", args=(owner_address,)), format="json" ) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertCountEqual( diff --git a/safe_transaction_service/history/views.py b/safe_transaction_service/history/views.py index b516e5b28..2edb5ea21 100644 --- a/safe_transaction_service/history/views.py +++ b/safe_transaction_service/history/views.py @@ -11,7 +11,6 @@ from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import status -from rest_framework.authentication import TokenAuthentication from rest_framework.filters import OrderingFilter from rest_framework.generics import ( DestroyAPIView, @@ -21,7 +20,6 @@ RetrieveAPIView, get_object_or_404, ) -from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.views import APIView @@ -1008,8 +1006,6 @@ class MasterCopiesView(ListAPIView): class OwnersView(APIView): - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] serializer_class = serializers.OwnerResponseSerializer @swagger_auto_schema( From 0fcb49540b4c48a6e7be563dabc1727dc5cb7826 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Mon, 10 Jan 2022 13:57:14 +0100 Subject: [PATCH 14/17] Normalize IPFS addresses when using HTTP gateway - Ipfs addresses can be ipfs://hash or ipfs://ipfs/hash. Both ways will be converted to the same http url --- config/settings/base.py | 2 +- .../history/services/collectibles_service.py | 2 ++ .../history/tests/test_collectibles_service.py | 8 +++++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index a82e424c4..c3f56e415 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -419,7 +419,7 @@ ) ETHERSCAN_API_KEY = env("ETHERSCAN_API_KEY", default=None) -IPFS_GATEWAY = env("IPFS_GATEWAY", default="https://cloudflare-ipfs.com/") +IPFS_GATEWAY = env("IPFS_GATEWAY", default="https://cloudflare-ipfs.com/ipfs/") SWAGGER_SETTINGS = { "SECURITY_DEFINITIONS": { diff --git a/safe_transaction_service/history/services/collectibles_service.py b/safe_transaction_service/history/services/collectibles_service.py index 142d1e650..32f54a54a 100644 --- a/safe_transaction_service/history/services/collectibles_service.py +++ b/safe_transaction_service/history/services/collectibles_service.py @@ -41,6 +41,7 @@ class MetadataRetrievalException(CollectiblesServiceException): def ipfs_to_http(uri: Optional[str]) -> Optional[str]: if uri and uri.startswith("ipfs://"): + uri = uri.replace("ipfs://ipfs/", "ipfs://") return urljoin( settings.IPFS_GATEWAY, uri.replace("ipfs://", "", 1) ) # Use ipfs gateway @@ -159,6 +160,7 @@ def _retrieve_metadata_from_uri(self, uri: str) -> Dict[Any, Any]: """ Get metadata from uri. Maybe at some point support IPFS or another protocols. Currently just http/https is supported + :param uri: Uri starting with the protocol, like http://example.org/token/3 :return: Metadata as a decoded json """ diff --git a/safe_transaction_service/history/tests/test_collectibles_service.py b/safe_transaction_service/history/tests/test_collectibles_service.py index 4e1b72338..e45c1c980 100644 --- a/safe_transaction_service/history/tests/test_collectibles_service.py +++ b/safe_transaction_service/history/tests/test_collectibles_service.py @@ -38,7 +38,13 @@ def test_ipfs_to_http(self): ipfs_url = "ipfs://testing-url/path/?arguments" result = ipfs_to_http(ipfs_url) self.assertTrue(result.startswith("http")) - self.assertIn("testing-url/path/?arguments", result) + self.assertIn("ipfs/testing-url/path/?arguments", result) + + ipfs_with_path_url = "ipfs://ipfs/testing-url/path/?arguments" + result = ipfs_to_http(ipfs_with_path_url) + self.assertTrue(result.startswith("http")) + self.assertNotIn("ipfs/ipfs", result) + self.assertIn("ipfs/testing-url/path/?arguments", result) def test_get_collectibles(self): mainnet_node = just_test_if_mainnet_node() From 152c42183f5e96161c4ad9912a5ffa3da1af6eb4 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Mon, 10 Jan 2022 12:45:14 +0100 Subject: [PATCH 15/17] Set low priority for webhook/notification tasks - Regular priority for Celery is 3, we are setting 4 for these tasks (the higher the less priority when Redis is set) - Fix contract indexing priority. It's supposed to have a very low priority --- config/settings/base.py | 5 ++++- safe_transaction_service/contracts/tasks.py | 4 ++-- safe_transaction_service/history/signals.py | 8 ++++++-- safe_transaction_service/history/tests/test_signals.py | 4 ++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index c3f56e415..62292d744 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -229,7 +229,10 @@ # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_always_eager CELERY_ALWAYS_EAGER = False # https://docs.celeryproject.org/en/latest/userguide/configuration.html#task-default-priority -CELERY_TASK_DEFAULT_PRIORITY = 5 # Higher = more priority +# Higher = more priority on RabbitMQ, opposite on Redis ¯\_(ツ)_/¯ +CELERY_TASK_DEFAULT_PRIORITY = 3 +# https://docs.celeryproject.org/en/stable/userguide/configuration.html#task-queue-max-priority +CELERY_TASK_QUEUE_MAX_PRIORITY = 10 # https://docs.celeryproject.org/en/latest/userguide/configuration.html#broker-transport-options CELERY_BROKER_TRANSPORT_OPTIONS = { "queue_order_strategy": "priority", diff --git a/safe_transaction_service/contracts/tasks.py b/safe_transaction_service/contracts/tasks.py index 98a09528d..df8c57c0a 100644 --- a/safe_transaction_service/contracts/tasks.py +++ b/safe_transaction_service/contracts/tasks.py @@ -36,7 +36,7 @@ def create_missing_contracts_with_metadata_task() -> int: for address in addresses: logger.info("Detected missing contract %s", address) create_or_update_contract_with_metadata_task.apply_async( - (address,), priority=0 + (address,), priority=5 ) # Lowest priority i += 1 return i @@ -55,7 +55,7 @@ def reindex_contracts_without_metadata_task() -> int: Contract.objects.without_metadata().values_list("address", flat=True).iterator() ): logger.info("Reindexing contract %s", address) - create_or_update_contract_with_metadata_task.apply_async((address,), priority=0) + create_or_update_contract_with_metadata_task.apply_async((address,), priority=5) i += 1 return i diff --git a/safe_transaction_service/history/signals.py b/safe_transaction_service/history/signals.py index dfd844631..9b33cfc1c 100644 --- a/safe_transaction_service/history/signals.py +++ b/safe_transaction_service/history/signals.py @@ -247,9 +247,13 @@ def process_webhook( ) for payload in payloads: if address := payload.get("address"): - send_webhook_task.delay(address, payload) + send_webhook_task.apply_async( + args=(address, payload), priority=4 + ) # Almost the lowest priority if is_relevant_notification(sender, instance, created): - send_notification_task.apply_async(args=(address, payload), countdown=5) + send_notification_task.apply_async( + args=(address, payload), countdown=5, priority=4 + ) else: logger.debug( "Notification will not be sent for created=%s object=%s", diff --git a/safe_transaction_service/history/tests/test_signals.py b/safe_transaction_service/history/tests/test_signals.py index 9ca4d1945..9581dde6a 100644 --- a/safe_transaction_service/history/tests/test_signals.py +++ b/safe_transaction_service/history/tests/test_signals.py @@ -79,7 +79,7 @@ def test_build_webhook_payload(self): @factory.django.mute_signals(post_save) def test_process_webhook(self): multisig_confirmation = MultisigConfirmationFactory() - with mock.patch.object(send_webhook_task, "delay") as webhook_task_mock: + with mock.patch.object(send_webhook_task, "apply_async") as webhook_task_mock: with mock.patch.object( send_notification_task, "apply_async" ) as send_notification_task_mock: @@ -88,7 +88,7 @@ def test_process_webhook(self): send_notification_task_mock.assert_called() multisig_confirmation.created -= timedelta(minutes=45) - with mock.patch.object(send_webhook_task, "delay") as webhook_task_mock: + with mock.patch.object(send_webhook_task, "apply_async") as webhook_task_mock: with mock.patch.object( send_notification_task, "apply_async" ) as send_notification_task_mock: From 53082260f063d065e769f63867d1323522046d36 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Mon, 10 Jan 2022 14:00:52 +0100 Subject: [PATCH 16/17] Set version 4.0.0 --- requirements.txt | 2 +- safe_transaction_service/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2c1e1e97f..4ae39d7c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ docutils==0.18.1 drf-yasg[validation]==1.20.0 ethereum==2.3.2 firebase-admin==5.2.0 -gnosis-py[django]==3.7.5 +gnosis-py[django]==3.7.6 gunicorn[gevent]==20.1.0 hexbytes==0.2.2 packaging>=21.0 diff --git a/safe_transaction_service/__init__.py b/safe_transaction_service/__init__.py index 538cfdd78..ddc3efd63 100644 --- a/safe_transaction_service/__init__.py +++ b/safe_transaction_service/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.4.25" +__version__ = "4.0.0" __version_info__ = tuple( [ int(num) if num.isdigit() else num From 4d1abf97dd0d909b17d1cefa57f03a129cbe5735 Mon Sep 17 00:00:00 2001 From: Uxio Fuentefria Date: Tue, 11 Jan 2022 14:14:17 +0100 Subject: [PATCH 17/17] Increase connection pool for webhook HTTP requests - Updated from 10 -> 100 concurrent connections - Related to #620 --- safe_transaction_service/history/tasks.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/safe_transaction_service/history/tasks.py b/safe_transaction_service/history/tasks.py index c8685001f..e2637f306 100644 --- a/safe_transaction_service/history/tasks.py +++ b/safe_transaction_service/history/tasks.py @@ -355,7 +355,15 @@ def check_reorgs_task(self) -> Optional[int]: @cache def get_webhook_http_session(webhook_url: str) -> requests.Session: logger.debug("Getting http session for url=%s", webhook_url) - return requests.Session() + session = requests.Session() + adapter = requests.adapters.HTTPAdapter( + pool_connections=1, # Doing all the connections to the same url + pool_maxsize=100, # Number of concurrent connections + pool_block=False, + ) + session.mount("http://", adapter) + session.mount("https://", adapter) + return session @app.shared_task(