From d3738f89239ff6025b6dea6bd79c72045a889dfa Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Thu, 26 Oct 2023 22:37:42 +0200 Subject: [PATCH 1/7] wildcard ACL for generate_secure_url #264 --- HISTORY.md | 12 +++++++++ docs/core_api.rst | 11 +++++++-- pyproject.toml | 2 +- pyuploadcare/client.py | 6 +++-- pyuploadcare/secure_url.py | 10 +++++--- tests/functional/test_secure_url.py | 38 +++++++++++++++++++++++++++++ 6 files changed, 70 insertions(+), 9 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 89dcbd95..d87ef39a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.1.4](https://github.com/uploadcare/pyuploadcare/compare/v4.1.3...v4.1.4) - 2023-10-27 + +This update introduces the ability to generate secure URLs with the same signature valid not only for the base URL of the file (e.g., `https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/`) but also for all of its transformations. This is optional by default. + +### Added + +- For `Uploadcare` + - added optional `wildcard` parameter to `generate_secure_url` method. + +- For `AkamaiSecureUrlBuilder`: + - added optional `wildcard` parameter to `build` method. + ## [4.1.3](https://github.com/uploadcare/pyuploadcare/compare/v4.1.2...v4.1.3) - 2023-10-05 ### Added diff --git a/docs/core_api.rst b/docs/core_api.rst index d31e21ae..6a4b124c 100644 --- a/docs/core_api.rst +++ b/docs/core_api.rst @@ -387,7 +387,7 @@ Secure delivery You can use your own custom domain and CDN provider for deliver files with authenticated URLs (see `original documentation`_). -Generate secure for file:: +Generate secure url for file:: from pyuploadcare import Uploadcare from pyuploadcare.secure_url import AkamaiSecureUrlBuilder @@ -402,12 +402,19 @@ Generate secure for file:: secure_url = uploadcare.generate_secure_url('52da3bfc-7cd8-4861-8b05-126fef7a6994') -Generate secure for file with transformations:: +Generate secure url for file with transformations:: secure_url = uploadcare.generate_secure_url( '52da3bfc-7cd8-4861-8b05-126fef7a6994/-/resize/640x/other/transformations/' ) +Generate secure url for file, with the same signature valid for its transformations:: + + secure_url = uploadcare.generate_secure_url( + '52da3bfc-7cd8-4861-8b05-126fef7a6994', + wildcard=True + ) + Useful links ------------ diff --git a/pyproject.toml b/pyproject.toml index ecbfa95e..68337c0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyuploadcare" -version = "4.1.3" +version = "4.1.4" description = "Python library for Uploadcare.com" authors = ["Uploadcare Inc "] readme = "README.md" diff --git a/pyuploadcare/client.py b/pyuploadcare/client.py index 84cb6d8e..18be9b2e 100644 --- a/pyuploadcare/client.py +++ b/pyuploadcare/client.py @@ -814,7 +814,9 @@ def get_project_info(self) -> ProjectInfo: return self.project_api.retrieve() - def generate_secure_url(self, uuid: Union[str, UUID]) -> str: + def generate_secure_url( + self, uuid: Union[str, UUID], wildcard: bool = False + ) -> str: """Generate authenticated URL.""" if isinstance(uuid, UUID): uuid = str(uuid) @@ -822,4 +824,4 @@ def generate_secure_url(self, uuid: Union[str, UUID]) -> str: if not self.secure_url_builder: raise ValueError("secure_url_builder must be set") - return self.secure_url_builder.build(uuid) + return self.secure_url_builder.build(uuid, wildcard=wildcard) diff --git a/pyuploadcare/secure_url.py b/pyuploadcare/secure_url.py index 035017f0..9f591666 100644 --- a/pyuploadcare/secure_url.py +++ b/pyuploadcare/secure_url.py @@ -8,7 +8,7 @@ class BaseSecureUrlBuilder(ABC): @abstractmethod - def build(self, uuid: str) -> str: + def build(self, uuid: str, wildcard: bool = False) -> str: raise NotImplementedError @@ -34,12 +34,12 @@ def __init__( self.window = window self.hash_algo = hash_algo - def build(self, uuid: str) -> str: + def build(self, uuid: str, wildcard: bool = False) -> str: uuid = uuid.lstrip("/").rstrip("/") expire = self._build_expire_time() - acl = self._format_acl(uuid) + acl = self._format_acl(uuid, wildcard=wildcard) signature = self._build_signature(expire, acl) @@ -75,7 +75,9 @@ def _build_token(self, expire: int, acl: Optional[str], signature: str): ] return self.field_delimeter.join(token_parts) - def _format_acl(self, uuid: str) -> str: + def _format_acl(self, uuid: str, wildcard: bool) -> str: + if wildcard: + return f"/{uuid}/*" return f"/{uuid}/" def _build_expire_time(self) -> int: diff --git a/tests/functional/test_secure_url.py b/tests/functional/test_secure_url.py index 3ff7bd46..69af37aa 100644 --- a/tests/functional/test_secure_url.py +++ b/tests/functional/test_secure_url.py @@ -42,6 +42,22 @@ def test_generate_secure_url_with_transformation(): ) +@pytest.mark.freeze_time("2021-10-12") +def test_generate_secure_url_with_wildcard(): + secure_url_bulder = AkamaiSecureUrlBuilder( + "cdn.yourdomain.com", known_secret + ) + secure_url = secure_url_bulder.build( + "52da3bfc-7cd8-4861-8b05-126fef7a6994", wildcard=True + ) + assert secure_url == ( + "https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/?token=" + "exp=1633997100~" + "acl=/52da3bfc-7cd8-4861-8b05-126fef7a6994/*~" + "hmac=b2c7526a29d0588b121aa78bc2b2c9399bfb6e1cad3d95397efed722fdbc5a78" + ) + + @pytest.mark.freeze_time("2021-10-12") def test_client_generate_secure_url(): secure_url_bulder = AkamaiSecureUrlBuilder( @@ -62,3 +78,25 @@ def test_client_generate_secure_url(): "acl=/52da3bfc-7cd8-4861-8b05-126fef7a6994/~" "hmac=81852547d9dbd9eefd24bee2cada6eab02244b9013533bc8511511923098df72" ) + + +@pytest.mark.freeze_time("2021-10-12") +def test_client_generate_secure_url_with_wildcard(): + secure_url_bulder = AkamaiSecureUrlBuilder( + "cdn.yourdomain.com", known_secret + ) + + uploadcare = Uploadcare( + public_key="public", + secret_key="secret", + secure_url_builder=secure_url_bulder, + ) + secure_url = uploadcare.generate_secure_url( + "52da3bfc-7cd8-4861-8b05-126fef7a6994", wildcard=True + ) + assert secure_url == ( + "https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/?token=" + "exp=1633997100~" + "acl=/52da3bfc-7cd8-4861-8b05-126fef7a6994/*~" + "hmac=b2c7526a29d0588b121aa78bc2b2c9399bfb6e1cad3d95397efed722fdbc5a78" + ) From fa87d162e862936e549361ce08ef52361b92b514 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 27 Oct 2023 01:26:35 +0200 Subject: [PATCH 2/7] url-token builder #263 --- HISTORY.md | 12 +++- docs/core_api.rst | 28 +++++++- pyuploadcare/secure_url.py | 100 ++++++++++++++++++++++++---- tests/functional/test_secure_url.py | 40 ++++++++--- 4 files changed, 151 insertions(+), 29 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index d87ef39a..25f63bb7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -8,16 +8,24 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [4.1.4](https://github.com/uploadcare/pyuploadcare/compare/v4.1.3...v4.1.4) - 2023-10-27 -This update introduces the ability to generate secure URLs with the same signature valid not only for the base URL of the file (e.g., `https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/`) but also for all of its transformations. This is optional by default. +This update introduces the ability to generate secure URLs with the same signature valid not only for the base URL of the file (e.g., `https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/`) but also for all of its transformations (#264). This is optional by default. + +Also, `AkamaiSecureUrlBuilderWithUrlToken` class has been implemented (#263). ### Added - For `Uploadcare` - added optional `wildcard` parameter to `generate_secure_url` method. -- For `AkamaiSecureUrlBuilder`: +- For `AkamaiSecureUrlBuilderWithAclToken`: - added optional `wildcard` parameter to `build` method. +- `AkamaiSecureUrlBuilderWithUrlToken` class. + +### Changed + +- `AkamaiSecureUrlBuilder` has been renamed to `AkamaiSecureUrlBuilderWithAclToken`. It is still available under the old name and works as before, but it will give you a deprecation warning when you use it. + ## [4.1.3](https://github.com/uploadcare/pyuploadcare/compare/v4.1.2...v4.1.3) - 2023-10-05 ### Added diff --git a/docs/core_api.rst b/docs/core_api.rst index 6a4b124c..afedd6cc 100644 --- a/docs/core_api.rst +++ b/docs/core_api.rst @@ -390,9 +390,12 @@ You can use your own custom domain and CDN provider for deliver files with authe Generate secure url for file:: from pyuploadcare import Uploadcare - from pyuploadcare.secure_url import AkamaiSecureUrlBuilder + from pyuploadcare.secure_url import AkamaiSecureUrlBuilderWithAclToken - secure_url_bulder = AkamaiSecureUrlBuilder("your cdn>", "") + secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken( + "", + "" + ) uploadcare = Uploadcare( public_key='', @@ -415,6 +418,27 @@ Generate secure url for file, with the same signature valid for its transformati wildcard=True ) +Generate secure url for file by its URL (please notice the usage of a different builder class):: + + from pyuploadcare import Uploadcare + from pyuploadcare.secure_url import AkamaiSecureUrlBuilderWithUrlToken + + secure_url_bulder = AkamaiSecureUrlBuilderWithAUrlToken( + "", + "" + ) + + uploadcare = Uploadcare( + public_key='', + secret_key='', + secure_url_builder=secure_url_bulder, + ) + + secure_url = uploadcare.generate_secure_url( + 'https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/' + ) + + Useful links ------------ diff --git a/pyuploadcare/secure_url.py b/pyuploadcare/secure_url.py index 9f591666..1780625c 100644 --- a/pyuploadcare/secure_url.py +++ b/pyuploadcare/secure_url.py @@ -2,6 +2,7 @@ import hashlib import hmac import time +import warnings from abc import ABC, abstractmethod from typing import Optional @@ -12,14 +13,13 @@ def build(self, uuid: str, wildcard: bool = False) -> str: raise NotImplementedError -class AkamaiSecureUrlBuilder(BaseSecureUrlBuilder): +class BaseAkamaiSecureUrlBuilder(BaseSecureUrlBuilder): """Akamai secure url builder. See https://uploadcare.com/docs/security/secure_delivery/ for more details. """ - template = "https://{cdn}/{uuid}/?token={token}" field_delimeter = "~" def __init__( @@ -34,6 +34,28 @@ def __init__( self.window = window self.hash_algo = hash_algo + def _build_expire_time(self) -> int: + return int(time.time()) + self.window + + def _build_signature( + self, expire: int, acl: Optional[str] = None, url: Optional[str] = None + ) -> str: + assert bool(acl) != bool(url) + + hash_source = [f"exp={expire}", f"acl={acl}" if acl else f"url={url}"] + + signature = hmac.new( + binascii.a2b_hex(self.secret_key.encode()), + self.field_delimeter.join(hash_source).encode(), + self.hash_algo, + ).hexdigest() + + return signature + + +class AkamaiSecureUrlBuilderWithAclToken(BaseAkamaiSecureUrlBuilder): + template = "https://{cdn}/{uuid}/?token={token}" + def build(self, uuid: str, wildcard: bool = False) -> str: uuid = uuid.lstrip("/").rstrip("/") @@ -41,7 +63,7 @@ def build(self, uuid: str, wildcard: bool = False) -> str: acl = self._format_acl(uuid, wildcard=wildcard) - signature = self._build_signature(expire, acl) + signature = self._build_signature(expire, acl=acl) secure_url = self._build_url(uuid, expire, acl, signature) return secure_url @@ -80,19 +102,69 @@ def _format_acl(self, uuid: str, wildcard: bool) -> str: return f"/{uuid}/*" return f"/{uuid}/" - def _build_expire_time(self) -> int: - return int(time.time()) + self.window - def _build_signature(self, expire: int, acl: str) -> str: - hash_source = [ +class AkamaiSecureUrlBuilderWithUrlToken(BaseAkamaiSecureUrlBuilder): + template = "{url}?token={token}" + + def build(self, uuid: str, wildcard: bool = False) -> str: + if wildcard: + raise ValueError( + "Wildcards are not supported in AkamaiSecureUrlBuilderWithUrlToken." + ) + + url = uuid + + expire = self._build_expire_time() + + signature = self._build_signature(expire, url=url) + + secure_url = self._build_url(url, expire, signature) + + return secure_url + + def _build_url( + self, + url: str, + expire: int, + signature: str, + ) -> str: + req_parameters = [ + f"exp={expire}", + f"hmac={signature}", + ] + + token = self.field_delimeter.join(req_parameters) + + return self.template.format( + url=url, + token=token, + ) + + def _build_token(self, expire: int, url: str, signature: str): + token_parts = [ f"exp={expire}", - f"acl={acl}", + f"url={url}", + f"hmac={signature}", ] + return self.field_delimeter.join(token_parts) - signature = hmac.new( - binascii.a2b_hex(self.secret_key.encode()), - self.field_delimeter.join(hash_source).encode(), - self.hash_algo, - ).hexdigest() - return signature +class AkamaiSecureUrlBuilder(AkamaiSecureUrlBuilderWithAclToken): + def __init__( + self, + cdn_url: str, + secret_key: str, + window: int = 300, + hash_algo=hashlib.sha256, + ): + warnings.warn( + "AkamaiSecureUrlBuilder class was renamed to AkamaiSecureUrlBuilderWithAclToken", + DeprecationWarning, + stacklevel=2, + ) + super().__init__( + cdn_url=cdn_url, + secret_key=secret_key, + window=window, + hash_algo=hash_algo, + ) diff --git a/tests/functional/test_secure_url.py b/tests/functional/test_secure_url.py index 69af37aa..0acbc34e 100644 --- a/tests/functional/test_secure_url.py +++ b/tests/functional/test_secure_url.py @@ -1,7 +1,10 @@ import pytest from pyuploadcare import Uploadcare -from pyuploadcare.secure_url import AkamaiSecureUrlBuilder +from pyuploadcare.secure_url import ( + AkamaiSecureUrlBuilderWithAclToken, + AkamaiSecureUrlBuilderWithUrlToken, +) known_secret = ( @@ -10,8 +13,8 @@ @pytest.mark.freeze_time("2021-10-12") -def test_generate_secure_url(): - secure_url_bulder = AkamaiSecureUrlBuilder( +def test_generate_secure_url_acl_token(): + secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken( "cdn.yourdomain.com", known_secret ) secure_url = secure_url_bulder.build( @@ -26,8 +29,8 @@ def test_generate_secure_url(): @pytest.mark.freeze_time("2021-10-12") -def test_generate_secure_url_with_transformation(): - secure_url_bulder = AkamaiSecureUrlBuilder( +def test_generate_secure_url_with_transformation_acl_token(): + secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken( "cdn.yourdomain.com", known_secret ) secure_url = secure_url_bulder.build( @@ -43,8 +46,8 @@ def test_generate_secure_url_with_transformation(): @pytest.mark.freeze_time("2021-10-12") -def test_generate_secure_url_with_wildcard(): - secure_url_bulder = AkamaiSecureUrlBuilder( +def test_generate_secure_url_with_wildcard_acl_token(): + secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken( "cdn.yourdomain.com", known_secret ) secure_url = secure_url_bulder.build( @@ -59,8 +62,8 @@ def test_generate_secure_url_with_wildcard(): @pytest.mark.freeze_time("2021-10-12") -def test_client_generate_secure_url(): - secure_url_bulder = AkamaiSecureUrlBuilder( +def test_client_generate_secure_url_acl_token(): + secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken( "cdn.yourdomain.com", known_secret ) @@ -81,8 +84,23 @@ def test_client_generate_secure_url(): @pytest.mark.freeze_time("2021-10-12") -def test_client_generate_secure_url_with_wildcard(): - secure_url_bulder = AkamaiSecureUrlBuilder( +def test_generate_secure_url_url_token(): + secure_url_bulder = AkamaiSecureUrlBuilderWithUrlToken( + "cdn.yourdomain.com", known_secret + ) + secure_url = secure_url_bulder.build( + "https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/" + ) + assert secure_url == ( + "https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/?token=" + "exp=1633997100~" + "hmac=32b696b855ddc911b366f11dcecb75789adf6211a72c1dbdf234b83f22aaa368" + ) + + +@pytest.mark.freeze_time("2021-10-12") +def test_client_generate_secure_url_with_wildcard_acl_token(): + secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken( "cdn.yourdomain.com", known_secret ) From 016f5ea40dad857b81f51262cfa49ba1e77cc24e Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 27 Oct 2023 03:34:56 +0200 Subject: [PATCH 3/7] refactor #263, expose get_token method --- HISTORY.md | 6 +- docs/core_api.rst | 6 +- pyuploadcare/client.py | 12 +++ pyuploadcare/secure_url.py | 145 +++++++++++++--------------- tests/functional/test_secure_url.py | 48 +++++++++ 5 files changed, 139 insertions(+), 78 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 25f63bb7..0bcac23d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -12,12 +12,16 @@ This update introduces the ability to generate secure URLs with the same signatu Also, `AkamaiSecureUrlBuilderWithUrlToken` class has been implemented (#263). +A new method called `generate_secure_url_token` is exposed for `Uploadcare`. Instead of full URL it will return just the token (unlike the `generate_secure_url` method). + ### Added -- For `Uploadcare` +- For `Uploadcare`: + - added `generate_secure_url_token` method. - added optional `wildcard` parameter to `generate_secure_url` method. - For `AkamaiSecureUrlBuilderWithAclToken`: + - added `get_token` method. - added optional `wildcard` parameter to `build` method. - `AkamaiSecureUrlBuilderWithUrlToken` class. diff --git a/docs/core_api.rst b/docs/core_api.rst index afedd6cc..ae1df139 100644 --- a/docs/core_api.rst +++ b/docs/core_api.rst @@ -405,6 +405,10 @@ Generate secure url for file:: secure_url = uploadcare.generate_secure_url('52da3bfc-7cd8-4861-8b05-126fef7a6994') +Generate just the token:: + + token = uploadcare.get_secure_url_token('52da3bfc-7cd8-4861-8b05-126fef7a6994') + Generate secure url for file with transformations:: secure_url = uploadcare.generate_secure_url( @@ -423,7 +427,7 @@ Generate secure url for file by its URL (please notice the usage of a different from pyuploadcare import Uploadcare from pyuploadcare.secure_url import AkamaiSecureUrlBuilderWithUrlToken - secure_url_bulder = AkamaiSecureUrlBuilderWithAUrlToken( + secure_url_bulder = AkamaiSecureUrlBuilderWithUrlToken( "", "" ) diff --git a/pyuploadcare/client.py b/pyuploadcare/client.py index 18be9b2e..c41f12a1 100644 --- a/pyuploadcare/client.py +++ b/pyuploadcare/client.py @@ -825,3 +825,15 @@ def generate_secure_url( raise ValueError("secure_url_builder must be set") return self.secure_url_builder.build(uuid, wildcard=wildcard) + + def generate_secure_url_token( + self, uuid: Union[str, UUID], wildcard: bool = False + ) -> str: + """Generate token for authenticated URL.""" + if isinstance(uuid, UUID): + uuid = str(uuid) + + if not self.secure_url_builder: + raise ValueError("secure_url_builder must be set") + + return self.secure_url_builder.get_token(uuid, wildcard=wildcard) diff --git a/pyuploadcare/secure_url.py b/pyuploadcare/secure_url.py index 1780625c..270f21af 100644 --- a/pyuploadcare/secure_url.py +++ b/pyuploadcare/secure_url.py @@ -12,6 +12,11 @@ class BaseSecureUrlBuilder(ABC): def build(self, uuid: str, wildcard: bool = False) -> str: raise NotImplementedError + def get_token(self, uuid: str, wildcard: bool = False) -> str: + raise NotImplementedError( + f"{self.__class__} doesn't provide get_token()" + ) + class BaseAkamaiSecureUrlBuilder(BaseSecureUrlBuilder): """Akamai secure url builder. @@ -20,6 +25,7 @@ class BaseAkamaiSecureUrlBuilder(BaseSecureUrlBuilder): for more details. """ + template = "{base}?token={token}" field_delimeter = "~" def __init__( @@ -34,15 +40,31 @@ def __init__( self.window = window self.hash_algo = hash_algo + def build(self, uuid: str, wildcard: bool = False) -> str: + uuid_or_url = self._format_uuid_or_url(uuid) + token = self.get_token(uuid_or_url, wildcard=wildcard) + secure_url = self._build_url(uuid_or_url, token) + return secure_url + + def get_token(self, uuid: str, wildcard: bool = False) -> str: + uuid_or_url = self._format_uuid_or_url(uuid) + expire = self._build_expire_time() + acl = self._format_acl(uuid_or_url, wildcard=wildcard) + signature = self._build_signature(uuid_or_url, expire, acl) + token = self._build_token(expire, acl, signature) + return token + def _build_expire_time(self) -> int: return int(time.time()) + self.window def _build_signature( - self, expire: int, acl: Optional[str] = None, url: Optional[str] = None + self, uuid_or_url: str, expire: int, acl: Optional[str] ) -> str: - assert bool(acl) != bool(url) - hash_source = [f"exp={expire}", f"acl={acl}" if acl else f"url={url}"] + hash_source = [ + f"exp={expire}", + f"acl={acl}" if acl else f"url={uuid_or_url}", + ] signature = hmac.new( binascii.a2b_hex(self.secret_key.encode()), @@ -52,101 +74,72 @@ def _build_signature( return signature + def _build_token(self, expire: int, acl: Optional[str], signature: str): -class AkamaiSecureUrlBuilderWithAclToken(BaseAkamaiSecureUrlBuilder): - template = "https://{cdn}/{uuid}/?token={token}" - - def build(self, uuid: str, wildcard: bool = False) -> str: - uuid = uuid.lstrip("/").rstrip("/") - - expire = self._build_expire_time() - - acl = self._format_acl(uuid, wildcard=wildcard) + token_parts = [ + f"exp={expire}", + f"acl={acl}" if acl else None, + f"hmac={signature}", + ] - signature = self._build_signature(expire, acl=acl) + return self.field_delimeter.join( + part for part in token_parts if part is not None + ) - secure_url = self._build_url(uuid, expire, acl, signature) - return secure_url + @abstractmethod + def _build_base_url(self, uuid_or_url: str): + raise NotImplementedError def _build_url( self, - uuid: str, - expire: int, - acl: str, - signature: str, + uuid_or_url: str, + token: str, ) -> str: - req_parameters = [ - f"exp={expire}", - f"acl={acl}", - f"hmac={signature}", - ] - - token = self.field_delimeter.join(req_parameters) - + base_url = self._build_base_url(uuid_or_url) return self.template.format( - cdn=self.cdn_url, - uuid=uuid, + base=base_url, token=token, ) - def _build_token(self, expire: int, acl: Optional[str], signature: str): - token_parts = [ - f"exp={expire}", - f"acl={acl}", - f"hmac={signature}", - ] - return self.field_delimeter.join(token_parts) + @abstractmethod + def _format_acl(self, uuid_or_url: str, wildcard: bool) -> Optional[str]: + raise NotImplementedError + + @abstractmethod + def _format_uuid_or_url(self, uuid_or_url: str) -> str: + raise NotImplementedError + + +class AkamaiSecureUrlBuilderWithAclToken(BaseAkamaiSecureUrlBuilder): + base_template = "https://{cdn}/{uuid}/" + + def _build_base_url(self, uuid_or_url: str): + return self.base_template.format(cdn=self.cdn_url, uuid=uuid_or_url) - def _format_acl(self, uuid: str, wildcard: bool) -> str: + def _format_acl(self, uuid_or_url: str, wildcard: bool) -> str: if wildcard: - return f"/{uuid}/*" - return f"/{uuid}/" + return f"/{uuid_or_url}/*" + return f"/{uuid_or_url}/" + + def _format_uuid_or_url(self, uuid_or_url: str) -> str: + return uuid_or_url.lstrip("/").rstrip("/") class AkamaiSecureUrlBuilderWithUrlToken(BaseAkamaiSecureUrlBuilder): - template = "{url}?token={token}" + def _build_base_url(self, uuid_or_url: str): + return uuid_or_url - def build(self, uuid: str, wildcard: bool = False) -> str: + def _format_acl(self, uuid_or_url: str, wildcard: bool) -> None: if wildcard: raise ValueError( "Wildcards are not supported in AkamaiSecureUrlBuilderWithUrlToken." ) + return None - url = uuid - - expire = self._build_expire_time() - - signature = self._build_signature(expire, url=url) - - secure_url = self._build_url(url, expire, signature) - - return secure_url - - def _build_url( - self, - url: str, - expire: int, - signature: str, - ) -> str: - req_parameters = [ - f"exp={expire}", - f"hmac={signature}", - ] - - token = self.field_delimeter.join(req_parameters) - - return self.template.format( - url=url, - token=token, - ) - - def _build_token(self, expire: int, url: str, signature: str): - token_parts = [ - f"exp={expire}", - f"url={url}", - f"hmac={signature}", - ] - return self.field_delimeter.join(token_parts) + def _format_uuid_or_url(self, uuid_or_url: str) -> str: + if "://" not in uuid_or_url: + raise ValueError(f"{uuid_or_url} doesn't look like a URL") + return uuid_or_url class AkamaiSecureUrlBuilder(AkamaiSecureUrlBuilderWithAclToken): diff --git a/tests/functional/test_secure_url.py b/tests/functional/test_secure_url.py index 0acbc34e..a7d9106c 100644 --- a/tests/functional/test_secure_url.py +++ b/tests/functional/test_secure_url.py @@ -12,6 +12,19 @@ ) +@pytest.mark.freeze_time("2021-10-12") +def test_get_acl_token(): + secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken( + "cdn.yourdomain.com", known_secret + ) + token = secure_url_bulder.get_token("52da3bfc-7cd8-4861-8b05-126fef7a6994") + assert token == ( + "exp=1633997100~" + "acl=/52da3bfc-7cd8-4861-8b05-126fef7a6994/~" + "hmac=81852547d9dbd9eefd24bee2cada6eab02244b9013533bc8511511923098df72" + ) + + @pytest.mark.freeze_time("2021-10-12") def test_generate_secure_url_acl_token(): secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken( @@ -83,6 +96,41 @@ def test_client_generate_secure_url_acl_token(): ) +@pytest.mark.freeze_time("2021-10-12") +def test_client_generate_secure_url_token_acl_token(): + secure_url_bulder = AkamaiSecureUrlBuilderWithAclToken( + "cdn.yourdomain.com", known_secret + ) + + uploadcare = Uploadcare( + public_key="public", + secret_key="secret", + secure_url_builder=secure_url_bulder, + ) + secure_url = uploadcare.generate_secure_url_token( + "52da3bfc-7cd8-4861-8b05-126fef7a6994" + ) + assert secure_url == ( + "exp=1633997100~" + "acl=/52da3bfc-7cd8-4861-8b05-126fef7a6994/~" + "hmac=81852547d9dbd9eefd24bee2cada6eab02244b9013533bc8511511923098df72" + ) + + +@pytest.mark.freeze_time("2021-10-12") +def test_get_url_token(): + secure_url_bulder = AkamaiSecureUrlBuilderWithUrlToken( + "cdn.yourdomain.com", known_secret + ) + token = secure_url_bulder.get_token( + "https://cdn.yourdomain.com/52da3bfc-7cd8-4861-8b05-126fef7a6994/" + ) + assert token == ( + "exp=1633997100~" + "hmac=32b696b855ddc911b366f11dcecb75789adf6211a72c1dbdf234b83f22aaa368" + ) + + @pytest.mark.freeze_time("2021-10-12") def test_generate_secure_url_url_token(): secure_url_bulder = AkamaiSecureUrlBuilderWithUrlToken( From 60ca7ed656efa9c2ccccd43bdca1c394054db109 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 10 Nov 2023 19:39:14 +0100 Subject: [PATCH 4/7] Update docs/core_api.rst Co-authored-by: Dmitry Mukhin --- docs/core_api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core_api.rst b/docs/core_api.rst index ae1df139..e3cf4425 100644 --- a/docs/core_api.rst +++ b/docs/core_api.rst @@ -387,7 +387,7 @@ Secure delivery You can use your own custom domain and CDN provider for deliver files with authenticated URLs (see `original documentation`_). -Generate secure url for file:: +Generate secure URL for file:: from pyuploadcare import Uploadcare from pyuploadcare.secure_url import AkamaiSecureUrlBuilderWithAclToken From 28272cf8ffa38fa3c19753eb32f5ca7b6e076c12 Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 10 Nov 2023 19:39:46 +0100 Subject: [PATCH 5/7] #263 #264 Update docs/core_api.rst Co-authored-by: Dmitry Mukhin --- docs/core_api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core_api.rst b/docs/core_api.rst index e3cf4425..946c68c8 100644 --- a/docs/core_api.rst +++ b/docs/core_api.rst @@ -409,7 +409,7 @@ Generate just the token:: token = uploadcare.get_secure_url_token('52da3bfc-7cd8-4861-8b05-126fef7a6994') -Generate secure url for file with transformations:: +Generate secure URL for file with transformations:: secure_url = uploadcare.generate_secure_url( '52da3bfc-7cd8-4861-8b05-126fef7a6994/-/resize/640x/other/transformations/' From 038eb13b9da6154c4901f74a37f61f61ee1fc9ef Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 10 Nov 2023 19:39:55 +0100 Subject: [PATCH 6/7] #263 #264 Update docs/core_api.rst Co-authored-by: Dmitry Mukhin --- docs/core_api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core_api.rst b/docs/core_api.rst index 946c68c8..ea33d4a9 100644 --- a/docs/core_api.rst +++ b/docs/core_api.rst @@ -415,7 +415,7 @@ Generate secure URL for file with transformations:: '52da3bfc-7cd8-4861-8b05-126fef7a6994/-/resize/640x/other/transformations/' ) -Generate secure url for file, with the same signature valid for its transformations:: +Generate secure URL for file, with the same signature valid for its transformations:: secure_url = uploadcare.generate_secure_url( '52da3bfc-7cd8-4861-8b05-126fef7a6994', From a394abec527ac2adc6f7f42a8e045a291b7c8f9e Mon Sep 17 00:00:00 2001 From: Evgeniy Kirov Date: Fri, 10 Nov 2023 19:40:05 +0100 Subject: [PATCH 7/7] #263 #264 Update docs/core_api.rst Co-authored-by: Dmitry Mukhin --- docs/core_api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core_api.rst b/docs/core_api.rst index ea33d4a9..e54f12eb 100644 --- a/docs/core_api.rst +++ b/docs/core_api.rst @@ -422,7 +422,7 @@ Generate secure URL for file, with the same signature valid for its transformati wildcard=True ) -Generate secure url for file by its URL (please notice the usage of a different builder class):: +Generate secure URL for file by its URL (please notice the usage of a different builder class):: from pyuploadcare import Uploadcare from pyuploadcare.secure_url import AkamaiSecureUrlBuilderWithUrlToken