pyramid_cloudflare_access 1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyramid_cloudflare_access-1.1/CHANGES.rst +17 -0
- pyramid_cloudflare_access-1.1/COPYING.txt +26 -0
- pyramid_cloudflare_access-1.1/PKG-INFO +91 -0
- pyramid_cloudflare_access-1.1/README.rst +60 -0
- pyramid_cloudflare_access-1.1/pyproject.toml +45 -0
- pyramid_cloudflare_access-1.1/pyramid_cloudflare_access/__init__.py +71 -0
- pyramid_cloudflare_access-1.1/pyramid_cloudflare_access/tests/__init__.py +0 -0
- pyramid_cloudflare_access-1.1/pyramid_cloudflare_access/tests/conftest.py +3 -0
- pyramid_cloudflare_access-1.1/pyramid_cloudflare_access/tests/test_access.py +115 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Copyright (c) 2022 Niteo GmbH
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without modification, are
|
|
5
|
+
permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this list of
|
|
8
|
+
conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list
|
|
11
|
+
of conditions and the following disclaimer in the documentation and/or other materials
|
|
12
|
+
provided with the distribution.
|
|
13
|
+
|
|
14
|
+
THIS SOFTWARE IS PROVIDED BY THE ABOVE COPYRIGHT HOLDERS ''AS IS'' AND ANY EXPRESS OR IMPLIED
|
|
15
|
+
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE ABOVE COPYRIGHT HOLDERS OR
|
|
17
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
18
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
19
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
20
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
21
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
|
22
|
+
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
23
|
+
|
|
24
|
+
The views and conclusions contained in the software and documentation are those of the
|
|
25
|
+
authors and should not be interpreted as representing official policies, either expressed
|
|
26
|
+
or implied, of their employers.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pyramid_cloudflare_access
|
|
3
|
+
Version: 1.1
|
|
4
|
+
Summary: A bunch of helpers for successfully running Pyramid on Heroku.
|
|
5
|
+
Home-page: https://github.com/teamniteo/pyramid_cloudflare_access
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
Keywords: pyramid,cloudflare,pylons,web
|
|
8
|
+
Author: Niteo
|
|
9
|
+
Author-email: info@niteo.co
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: Pylons
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Dist: cryptography
|
|
27
|
+
Requires-Dist: pyjwt
|
|
28
|
+
Requires-Dist: pyramid (>=1.7,<2.0)
|
|
29
|
+
Description-Content-Type: text/x-rst
|
|
30
|
+
|
|
31
|
+
pyramid_cloudflare_access
|
|
32
|
+
=========================
|
|
33
|
+
|
|
34
|
+
Introduction
|
|
35
|
+
------------
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
Installation
|
|
39
|
+
------------
|
|
40
|
+
|
|
41
|
+
Just do
|
|
42
|
+
|
|
43
|
+
``pip install pyramid_cloudflare_access``
|
|
44
|
+
|
|
45
|
+
or
|
|
46
|
+
|
|
47
|
+
``easy_install pyramid_cloudflare_access``
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
Compatibility
|
|
51
|
+
-------------
|
|
52
|
+
|
|
53
|
+
pyramid_cloudflare_access runs with pyramid>=1.7 and python>=3.6.
|
|
54
|
+
Other versions might also work.
|
|
55
|
+
|
|
56
|
+
Heroku Review Apps deployed on `*.herokuapp.com` subdomains automatically skip the cloudflare access check as their domains are dynamic and as such can't be configured in Cloudflare dashboard in advance.
|
|
57
|
+
|
|
58
|
+
Usage
|
|
59
|
+
-----
|
|
60
|
+
|
|
61
|
+
Add Cloudfalre config to a production.ini::
|
|
62
|
+
|
|
63
|
+
pyramid_cloudflare_access.policy_audience = "my_audience"
|
|
64
|
+
pyramid_cloudflare_access.team = "https://team.cloudfare-access.com"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
More information can be found at https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json#python-example
|
|
68
|
+
|
|
69
|
+
Usage example for the tween::
|
|
70
|
+
|
|
71
|
+
def main(global_config, **settings):
|
|
72
|
+
config = Configurator(settings=settings)
|
|
73
|
+
config.include('pyramid_cloudflare_access')
|
|
74
|
+
return config.make_wsgi_app()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
Releasing
|
|
78
|
+
---------
|
|
79
|
+
|
|
80
|
+
#. Update CHANGES.rst.
|
|
81
|
+
#. Update pyproject.toml version.
|
|
82
|
+
#. Run ``poetry check``.
|
|
83
|
+
#. Run ``poetry publish --build``.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
We're hiring!
|
|
87
|
+
-------------
|
|
88
|
+
|
|
89
|
+
At Niteo we regularly contribute back to the Open Source community. If you do too, we'd like to invite you to `join our team
|
|
90
|
+
<https://niteo.co/careers/>`_!
|
|
91
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
pyramid_cloudflare_access
|
|
2
|
+
=========================
|
|
3
|
+
|
|
4
|
+
Introduction
|
|
5
|
+
------------
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Installation
|
|
9
|
+
------------
|
|
10
|
+
|
|
11
|
+
Just do
|
|
12
|
+
|
|
13
|
+
``pip install pyramid_cloudflare_access``
|
|
14
|
+
|
|
15
|
+
or
|
|
16
|
+
|
|
17
|
+
``easy_install pyramid_cloudflare_access``
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Compatibility
|
|
21
|
+
-------------
|
|
22
|
+
|
|
23
|
+
pyramid_cloudflare_access runs with pyramid>=1.7 and python>=3.6.
|
|
24
|
+
Other versions might also work.
|
|
25
|
+
|
|
26
|
+
Heroku Review Apps deployed on `*.herokuapp.com` subdomains automatically skip the cloudflare access check as their domains are dynamic and as such can't be configured in Cloudflare dashboard in advance.
|
|
27
|
+
|
|
28
|
+
Usage
|
|
29
|
+
-----
|
|
30
|
+
|
|
31
|
+
Add Cloudfalre config to a production.ini::
|
|
32
|
+
|
|
33
|
+
pyramid_cloudflare_access.policy_audience = "my_audience"
|
|
34
|
+
pyramid_cloudflare_access.team = "https://team.cloudfare-access.com"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
More information can be found at https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json#python-example
|
|
38
|
+
|
|
39
|
+
Usage example for the tween::
|
|
40
|
+
|
|
41
|
+
def main(global_config, **settings):
|
|
42
|
+
config = Configurator(settings=settings)
|
|
43
|
+
config.include('pyramid_cloudflare_access')
|
|
44
|
+
return config.make_wsgi_app()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
Releasing
|
|
48
|
+
---------
|
|
49
|
+
|
|
50
|
+
#. Update CHANGES.rst.
|
|
51
|
+
#. Update pyproject.toml version.
|
|
52
|
+
#. Run ``poetry check``.
|
|
53
|
+
#. Run ``poetry publish --build``.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
We're hiring!
|
|
57
|
+
-------------
|
|
58
|
+
|
|
59
|
+
At Niteo we regularly contribute back to the Open Source community. If you do too, we'd like to invite you to `join our team
|
|
60
|
+
<https://niteo.co/careers/>`_!
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "pyramid_cloudflare_access"
|
|
3
|
+
version = "1.1"
|
|
4
|
+
description = "A bunch of helpers for successfully running Pyramid on Heroku."
|
|
5
|
+
readme = "README.rst"
|
|
6
|
+
include = [ "CHANGES.rst", "COPYING.txt" ]
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Operating System :: OS Independent",
|
|
9
|
+
"License :: OSI Approved :: BSD License",
|
|
10
|
+
"Programming Language :: Python",
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Framework :: Pylons",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.7",
|
|
16
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
17
|
+
"Topic :: Internet :: WWW/HTTP :: WSGI",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
]
|
|
20
|
+
packages = [
|
|
21
|
+
{ include = "pyramid_cloudflare_access" },
|
|
22
|
+
]
|
|
23
|
+
authors = [ "Niteo <info@niteo.co>" ]
|
|
24
|
+
license = "BSD-3-Clause"
|
|
25
|
+
homepage = "https://github.com/teamniteo/pyramid_cloudflare_access"
|
|
26
|
+
keywords = [ "pyramid", "cloudflare", "pylons", "web" ]
|
|
27
|
+
|
|
28
|
+
[tool.poetry.dependencies]
|
|
29
|
+
python = ">=3.9"
|
|
30
|
+
pyramid = ">=1.7, <2.0"
|
|
31
|
+
pyjwt = "*"
|
|
32
|
+
cryptography = "*"
|
|
33
|
+
|
|
34
|
+
[tool.poetry.dev-dependencies]
|
|
35
|
+
typecov = "*"
|
|
36
|
+
pytest-mock = "*"
|
|
37
|
+
pytest-cov = "*"
|
|
38
|
+
pytest = "*"
|
|
39
|
+
mypy = "*"
|
|
40
|
+
mock = "*"
|
|
41
|
+
isort = "*"
|
|
42
|
+
black = "*"
|
|
43
|
+
autoflake = "*"
|
|
44
|
+
"zope.testing" = "*"
|
|
45
|
+
pytest-freezegun = "*"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Verify Cloudflare Access requests."""
|
|
2
|
+
|
|
3
|
+
from jwt import decode as jwt_decode
|
|
4
|
+
from jwt import PyJWKClient
|
|
5
|
+
from jwt.api_jwk import PyJWK
|
|
6
|
+
from pyramid.config import Configurator
|
|
7
|
+
from pyramid.request import Request
|
|
8
|
+
from pyramid.tweens import INGRESS
|
|
9
|
+
|
|
10
|
+
import pyramid.httpexceptions as exc
|
|
11
|
+
import typing as t
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def includeme(config: Configurator) -> None: # pragma: no cover
|
|
15
|
+
# as possibly first tween in chain, order does not matter
|
|
16
|
+
config.add_tween("pyramid_cloudflare_access.CloudflareAccess", under=INGRESS)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_public_keys(team: str) -> t.List[PyJWK]:
|
|
20
|
+
"""Get JWK public keys for a team."""
|
|
21
|
+
|
|
22
|
+
url = f"{team}/cdn-cgi/access/certs"
|
|
23
|
+
client = PyJWKClient(url, cache_keys=False)
|
|
24
|
+
return client.get_signing_keys()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CloudflareAccess:
|
|
28
|
+
"""Deny access to Pyramid app when CF Access token is missing or invalid.
|
|
29
|
+
|
|
30
|
+
ref: https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json#python-example
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, handler, registry):
|
|
34
|
+
settings = getattr(registry, "settings", {})
|
|
35
|
+
self.handler = handler
|
|
36
|
+
self.policy_audience = settings["pyramid_cloudflare_access.policy_audience"]
|
|
37
|
+
self.public_keys = _get_public_keys(settings["pyramid_cloudflare_access.team"])
|
|
38
|
+
|
|
39
|
+
def authenticated_request(self, request: Request) -> bool:
|
|
40
|
+
token = request.cookies.get("CF_Authorization")
|
|
41
|
+
|
|
42
|
+
if not token:
|
|
43
|
+
raise exc.HTTPBadRequest()
|
|
44
|
+
|
|
45
|
+
# Loop through the keys since we can't pass the key set to the decoder
|
|
46
|
+
for signing_key in self.public_keys:
|
|
47
|
+
try:
|
|
48
|
+
# decode returns the claims and verifies token against public keys
|
|
49
|
+
claims = jwt_decode(
|
|
50
|
+
token,
|
|
51
|
+
key=signing_key.key,
|
|
52
|
+
audience=self.policy_audience,
|
|
53
|
+
algorithms=["RS256"],
|
|
54
|
+
)
|
|
55
|
+
return claims is not None
|
|
56
|
+
except:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def __call__(self, request: Request):
|
|
62
|
+
|
|
63
|
+
# Support for Heroku Review apps
|
|
64
|
+
if "herokuapp.com" in request.headers.get("Host", ""):
|
|
65
|
+
return self.handler(request)
|
|
66
|
+
|
|
67
|
+
if not self.authenticated_request(request):
|
|
68
|
+
raise exc.HTTPForbidden()
|
|
69
|
+
|
|
70
|
+
# Continue with processing requests
|
|
71
|
+
return self.handler(request)
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Tests for HerokuappAccess tween."""
|
|
2
|
+
|
|
3
|
+
from pyramid import testing
|
|
4
|
+
from pyramid.httpexceptions import HTTPBadRequest
|
|
5
|
+
from pyramid.httpexceptions import HTTPForbidden
|
|
6
|
+
from pyramid_cloudflare_access import CloudflareAccess
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
# Taken from example at https://pyjwt.readthedocs.io/en/latest/usage.html#retrieve-rsa-signing-keys-from-a-jwks-endpoint
|
|
11
|
+
sample_jwk = {
|
|
12
|
+
"keys": [
|
|
13
|
+
{
|
|
14
|
+
"alg": "RS256",
|
|
15
|
+
"kty": "RSA",
|
|
16
|
+
"use": "sig",
|
|
17
|
+
"n": "0wtlJRY9-ru61LmOgieeI7_rD1oIna9QpBMAOWw8wTuoIhFQFwcIi7MFB7IEfelCPj08vkfLsuFtR8cG07EE4uvJ78bAqRjMsCvprWp4e2p7hqPnWcpRpDEyHjzirEJle1LPpjLLVaSWgkbrVaOD0lkWkP1T1TkrOset_Obh8BwtO-Ww-UfrEwxTyz1646AGkbT2nL8PX0trXrmira8GnrCkFUgTUS61GoTdb9bCJ19PLX9Gnxw7J0BtR0GubopXq8KlI0ThVql6ZtVGN2dvmrCPAVAZleM5TVB61m0VSXvGWaF6_GeOhbFoyWcyUmFvzWhBm8Q38vWgsSI7oHTkEw",
|
|
18
|
+
"e": "AQAB",
|
|
19
|
+
"kid": "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw",
|
|
20
|
+
"x5t": "NEE1QURBOTM4MzI5RkFDNTYxOTU1MDg2ODgwQ0UzMTk1QjYyRkRFQw",
|
|
21
|
+
"x5c": [
|
|
22
|
+
"MIIDBzCCAe+gAwIBAgIJNtD9Ozi6j2jJMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi04N2V2eDlydS5hdXRoMC5jb20wHhcNMTkwNjIwMTU0NDU4WhcNMzMwMjI2MTU0NDU4WjAhMR8wHQYDVQQDExZkZXYtODdldng5cnUuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0wtlJRY9+ru61LmOgieeI7/rD1oIna9QpBMAOWw8wTuoIhFQFwcIi7MFB7IEfelCPj08vkfLsuFtR8cG07EE4uvJ78bAqRjMsCvprWp4e2p7hqPnWcpRpDEyHjzirEJle1LPpjLLVaSWgkbrVaOD0lkWkP1T1TkrOset/Obh8BwtO+Ww+UfrEwxTyz1646AGkbT2nL8PX0trXrmira8GnrCkFUgTUS61GoTdb9bCJ19PLX9Gnxw7J0BtR0GubopXq8KlI0ThVql6ZtVGN2dvmrCPAVAZleM5TVB61m0VSXvGWaF6/GeOhbFoyWcyUmFvzWhBm8Q38vWgsSI7oHTkEwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQlGXpmYaXFB7Q3eG69Uhjd4cFp/jAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAIzQOF/h4T5WWAdjhcIwdNS7hS2Deq+UxxkRv+uavj6O9mHLuRG1q5onvSFShjECXaYT6OGibn7Ufw/JSm3+86ZouMYjBEqGh4OvWRkwARy1YTWUVDGpT2HAwtIq3lfYvhe8P4VfZByp1N4lfn6X2NcJflG+Q+mfXNmRFyyft3Oq51PCZyyAkU7bTun9FmMOyBtmJvQjZ8RXgBLvu9nUcZB8yTVoeUEg4cLczQlli/OkiFXhWgrhVr8uF0/9klslMFXtm78iYSgR8/oC+k1pSNd1+ESSt7n6+JiAQ2Co+ZNKta7LTDGAjGjNDymyoCrZpeuYQwwnHYEHu/0khjAxhXo="
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"alg": "RS256",
|
|
27
|
+
"kty": "RSA",
|
|
28
|
+
"use": "sig",
|
|
29
|
+
"n": "qMDEywqsPbiQbnSPVoKOb1HrQ_2KxI4JDe-AK-kbpb2Q3QXFl6IM3pJCvfYOm-f3DuEtBpll_Rg28WWeXl8pXAhmHk3V2Ig57f81uzGXg5xFtZDrqAG0chgwCQPD15FG00xrLDTvSDkIEPJZq-Y4IlJ3NbzQ8gn_JiappjMc8FjqQMz_4uUF-iIPU_aUgbLLtN98moKeNLAVV2lV3H5kVhNP8Fqd6piiH-mdma_KdY--GahAFC7Lt72_QtxnxowalbdkdMDim7paTeqxoZUKKHJsRVPXbiGx1zB3cfgBH7meU8ILv7JX3odu0juy0y2gagSaMkEd9-mcTLr8Bg0-5Q",
|
|
30
|
+
"e": "AQAB",
|
|
31
|
+
"kid": "Dt0jkFkY7KkmYdDb2BaI1",
|
|
32
|
+
"x5t": "lhmczC7hbLpBZh6MBSygH1D9qeE",
|
|
33
|
+
"x5c": [
|
|
34
|
+
"MIIDBzCCAe+gAwIBAgIJHjJKyTNJE/wIMA0GCSqGSIb3DQEBCwUAMCExHzAdBgNVBAMTFmRldi04N2V2eDlydS5hdXRoMC5jb20wHhcNMjAwMzExMjA1OTM5WhcNMzMxMTE4MjA1OTM5WjAhMR8wHQYDVQQDExZkZXYtODdldng5cnUuYXV0aDAuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqMDEywqsPbiQbnSPVoKOb1HrQ/2KxI4JDe+AK+kbpb2Q3QXFl6IM3pJCvfYOm+f3DuEtBpll/Rg28WWeXl8pXAhmHk3V2Ig57f81uzGXg5xFtZDrqAG0chgwCQPD15FG00xrLDTvSDkIEPJZq+Y4IlJ3NbzQ8gn/JiappjMc8FjqQMz/4uUF+iIPU/aUgbLLtN98moKeNLAVV2lV3H5kVhNP8Fqd6piiH+mdma/KdY++GahAFC7Lt72/QtxnxowalbdkdMDim7paTeqxoZUKKHJsRVPXbiGx1zB3cfgBH7meU8ILv7JX3odu0juy0y2gagSaMkEd9+mcTLr8Bg0+5QIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQxVQqyrrAtXsBznZj2GN4nfJf+sTAOBgNVHQ8BAf8EBAMCAoQwDQYJKoZIhvcNAQELBQADggEBAGsIl6yxIG8GOkHlfcSEa4//4WDxwfw8lg6zNPri6nhYtF1kbHTO5PqUbE+kasMnvqEV5Y0QXvyxwIjLLbbYiySK6aWp5XS2Wy5hYlMjXOimAw6mwbkNVhujRsPjTY3P+bv/9eiv2zO9yEfzfmfr6jhYcmnOdTFgAujsL4AyDpUh4/jKDtDNFl6lMdn8J7DcdRNZM/8OsAk6GgZYlzStfh4aI/uE3ekJ84XAxxdHUzwDUu5B8CetmHvfxQvV9MjmozLR8SbkTEhUv4//tr8SfGc8jS78E5w8NJN6DtYVcUNweHrAlvQXRevCmBRu3D9hKARjXfTBqilBBF9nNt93mYM="
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
sample_token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik5FRTFRVVJCT1RNNE16STVSa0ZETlRZeE9UVTFNRGcyT0Rnd1EwVXpNVGsxUWpZeVJrUkZRdyJ9.eyJpc3MiOiJodHRwczovL2Rldi04N2V2eDlydS5hdXRoMC5jb20vIiwic3ViIjoiYVc0Q2NhNzl4UmVMV1V6MGFFMkg2a0QwTzNjWEJWdENAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vZXhwZW5zZXMtYXBpIiwiaWF0IjoxNTcyMDA2OTU0LCJleHAiOjE1NzIwMDY5NjQsImF6cCI6ImFXNENjYTc5eFJlTFdVejBhRTJINmtEME8zY1hCVnRDIiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIn0.PUxE7xn52aTCohGiWoSdMBZGiYAHwE5FYie0Y1qUT68IHSTXwXVd6hn02HTah6epvHHVKA2FqcFZ4GGv5VTHEvYpeggiiZMgbxFrmTEY0csL6VNkX1eaJGcuehwQCRBKRLL3zKmA5IKGy5GeUnIbpPHLHDxr-GXvgFzsdsyWlVQvPX2xjeaQ217r2PtxDeqjlf66UYl6oY6AqNS8DH3iryCvIfCcybRZkc_hdy-6ZMoKT6Piijvk_aXdm7-QQqKJFHLuEqrVSOuBqqiNfVrG27QzAPuPOxvfXTVLXL2jek5meH6n-VWgrBdoMFH93QEszEDowDAEhQPHVs0xj7SIzA"
|
|
40
|
+
sample_audience = "https://expenses-api"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@pytest.mark.freeze_time("2019-10-25 12:36:00")
|
|
44
|
+
def test_happy_path(mocker) -> None:
|
|
45
|
+
"""Test that JWT token is parsed and authorized."""
|
|
46
|
+
|
|
47
|
+
mocker.patch(
|
|
48
|
+
"pyramid_cloudflare_access.PyJWKClient.fetch_data", return_value=sample_jwk
|
|
49
|
+
)
|
|
50
|
+
tween_handler = mocker.Mock()
|
|
51
|
+
|
|
52
|
+
request = testing.DummyRequest()
|
|
53
|
+
request.cookies = {"CF_Authorization": sample_token}
|
|
54
|
+
request.registry.settings = {
|
|
55
|
+
"pyramid_cloudflare_access.policy_audience": sample_audience,
|
|
56
|
+
"pyramid_cloudflare_access.team": "https://foo.cloudflareaccess.com",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
CloudflareAccess(tween_handler, request.registry)(request)
|
|
60
|
+
tween_handler.assert_called_with(request)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_missing_cookie(mocker) -> None:
|
|
64
|
+
"""Test that access is denied on wrong request."""
|
|
65
|
+
|
|
66
|
+
mocker.patch(
|
|
67
|
+
"pyramid_cloudflare_access.PyJWKClient.fetch_data", return_value=sample_jwk
|
|
68
|
+
)
|
|
69
|
+
tween_handler = mocker.Mock()
|
|
70
|
+
|
|
71
|
+
request = testing.DummyRequest()
|
|
72
|
+
request.registry.settings = {
|
|
73
|
+
"pyramid_cloudflare_access.policy_audience": sample_audience,
|
|
74
|
+
"pyramid_cloudflare_access.team": "https://foo.cloudflareaccess.com",
|
|
75
|
+
}
|
|
76
|
+
with pytest.raises(HTTPBadRequest):
|
|
77
|
+
CloudflareAccess(tween_handler, request.registry)(request)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_auth_failed(mocker) -> None:
|
|
81
|
+
"""Test that access is denied on expired auth."""
|
|
82
|
+
|
|
83
|
+
mocker.patch(
|
|
84
|
+
"pyramid_cloudflare_access.PyJWKClient.fetch_data", return_value=sample_jwk
|
|
85
|
+
)
|
|
86
|
+
tween_handler = mocker.Mock()
|
|
87
|
+
|
|
88
|
+
request = testing.DummyRequest()
|
|
89
|
+
request.cookies = {"CF_Authorization": sample_token}
|
|
90
|
+
request.registry.settings = {
|
|
91
|
+
"pyramid_cloudflare_access.policy_audience": sample_audience,
|
|
92
|
+
"pyramid_cloudflare_access.team": "https://foo.cloudflareaccess.com",
|
|
93
|
+
}
|
|
94
|
+
with pytest.raises(HTTPForbidden):
|
|
95
|
+
CloudflareAccess(tween_handler, request.registry)(request)
|
|
96
|
+
|
|
97
|
+
def test_herokuapp(mocker) -> None:
|
|
98
|
+
"""Test that Cloudflare Access is skipped for Heroku-hosted apps.
|
|
99
|
+
|
|
100
|
+
This is to support Review Apps that have dynamic *.herokuapp.com hostname,
|
|
101
|
+
that cannot be configured as a domain for Cloudflare Access application
|
|
102
|
+
in Cloudflare dashboard.
|
|
103
|
+
"""
|
|
104
|
+
tween_handler = mocker.Mock()
|
|
105
|
+
|
|
106
|
+
request = testing.DummyRequest()
|
|
107
|
+
request.cookies = {}
|
|
108
|
+
request.registry.settings = {
|
|
109
|
+
"pyramid_cloudflare_access.policy_audience": sample_audience,
|
|
110
|
+
"pyramid_cloudflare_access.team": "https://foo.cloudflareaccess.com",
|
|
111
|
+
}
|
|
112
|
+
request.headers['Host'] = "foo.herokuapp.com"
|
|
113
|
+
|
|
114
|
+
CloudflareAccess(tween_handler, request.registry)(request)
|
|
115
|
+
tween_handler.assert_called_with(request)
|