iaptoolkit 0.3.7a1__tar.gz → 0.3.7a2__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.
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/PKG-INFO +1 -1
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/pyproject.toml +9 -1
- iaptoolkit-0.3.7a2/src/iaptoolkit/jwt/constants.py +9 -0
- iaptoolkit-0.3.7a2/src/iaptoolkit/jwt/flask.py +142 -0
- iaptoolkit-0.3.7a2/src/iaptoolkit/jwt/metrics.py +34 -0
- iaptoolkit-0.3.7a2/src/iaptoolkit/jwt/quart.py +143 -0
- iaptoolkit-0.3.7a2/src/iaptoolkit/utils/__init__.py +0 -0
- iaptoolkit-0.3.7a1/src/iaptoolkit/utils/flask.py +0 -52
- iaptoolkit-0.3.7a1/src/iaptoolkit/utils/quart.py +0 -52
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/LICENSE +0 -0
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/README.md +0 -0
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/src/iaptoolkit/__init__.py +0 -0
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/src/iaptoolkit/constants.py +0 -0
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/src/iaptoolkit/exceptions.py +0 -0
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/src/iaptoolkit/headers.py +0 -0
- {iaptoolkit-0.3.7a1/src/iaptoolkit/utils → iaptoolkit-0.3.7a2/src/iaptoolkit/jwt}/__init__.py +0 -0
- {iaptoolkit-0.3.7a1/src/iaptoolkit/utils → iaptoolkit-0.3.7a2/src/iaptoolkit/jwt}/verify.py +0 -0
- {iaptoolkit-0.3.7a1/src/iaptoolkit/utils → iaptoolkit-0.3.7a2/src/iaptoolkit/jwt}/verify_async.py +0 -0
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/src/iaptoolkit/tokens/__init__.py +0 -0
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/src/iaptoolkit/tokens/service_account.py +0 -0
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/src/iaptoolkit/tokens/structs.py +0 -0
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/src/iaptoolkit/tokens/token_datastore.py +0 -0
- {iaptoolkit-0.3.7a1 → iaptoolkit-0.3.7a2}/src/iaptoolkit/utils/urls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "iaptoolkit"
|
|
3
|
-
version = "0.3.7-
|
|
3
|
+
version = "0.3.7-a2"
|
|
4
4
|
description = "Library of common utils for interacting with Identity-Aware Proxies"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Rob Voigt", email = "code@ravoigt.com"}
|
|
@@ -35,6 +35,10 @@ requests = "^2.31.0"
|
|
|
35
35
|
toml = "^0.10.2"
|
|
36
36
|
kvcommon = {extras = ["k8s"], version = "^0.2.12"}
|
|
37
37
|
|
|
38
|
+
# flask = { version = "^0.20.0", optional = true }
|
|
39
|
+
# quart = { version = "^0.20.0", optional = true }
|
|
40
|
+
# prometheus-client = { version = "^0.20.0", optional = true }
|
|
41
|
+
|
|
38
42
|
[tool.poetry.group.dev.dependencies]
|
|
39
43
|
black = "*"
|
|
40
44
|
ipdb = "*"
|
|
@@ -45,3 +49,7 @@ pytest = "*"
|
|
|
45
49
|
pytest-cov = "*"
|
|
46
50
|
pytest-socket = "*"
|
|
47
51
|
pyfakefs = "^5.3.2"
|
|
52
|
+
|
|
53
|
+
flask = "*"
|
|
54
|
+
quart = "*"
|
|
55
|
+
prometheus_client = "*"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
class JWT_Event(StrEnum):
|
|
4
|
+
SUCCESS = "success"
|
|
5
|
+
FAIL_NO_HEADER = "fail_no_header"
|
|
6
|
+
FAIL_INVALID_JWT = "fail_invalid"
|
|
7
|
+
FAIL_NO_EMAIL = "fail_no_email"
|
|
8
|
+
FAIL_WRONG_USER = "fail_wrong_user"
|
|
9
|
+
FAIL_WRONG_AUDIENCE = "fail_wrong_audience"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
from flask import request
|
|
5
|
+
from flask.wrappers import Request
|
|
6
|
+
from flask.wrappers import Response
|
|
7
|
+
|
|
8
|
+
from iaptoolkit.constants import GOOGLE_IAP_JWT_HEADER_KEY
|
|
9
|
+
from iaptoolkit.exceptions import JWTDisallowedUser
|
|
10
|
+
from iaptoolkit.exceptions import JWTInvalidAudience
|
|
11
|
+
from iaptoolkit.exceptions import JWTInvalidData
|
|
12
|
+
from iaptoolkit.exceptions import JWTMalformed
|
|
13
|
+
|
|
14
|
+
from .constants import JWT_Event
|
|
15
|
+
from .metrics import Counter
|
|
16
|
+
from .metrics import default_metric
|
|
17
|
+
from .metrics import inc_metric
|
|
18
|
+
from .verify import verify_iap_jwt
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _verify_jwt(
|
|
22
|
+
request: Request,
|
|
23
|
+
jwt_header_key: str,
|
|
24
|
+
jwt_audience: str,
|
|
25
|
+
allowed_users: set[str] | None = None,
|
|
26
|
+
response_cls: t.Type[Response] = Response,
|
|
27
|
+
metric: Counter | None = default_metric
|
|
28
|
+
) -> Response | None:
|
|
29
|
+
jwt_header: str = request.headers.get(jwt_header_key.lower(), "")
|
|
30
|
+
if not jwt_header:
|
|
31
|
+
inc_metric(metric, event=JWT_Event.FAIL_NO_HEADER)
|
|
32
|
+
return response_cls(f"No Google IAP JWT header in request at key: '{jwt_header_key}'", status=401)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
user_email = verify_iap_jwt(iap_jwt=jwt_header, expected_audience=jwt_audience)
|
|
36
|
+
if not user_email:
|
|
37
|
+
raise JWTInvalidData("No user_email in decoded JWT")
|
|
38
|
+
|
|
39
|
+
if allowed_users and user_email not in allowed_users:
|
|
40
|
+
raise JWTDisallowedUser(message=f"User '{user_email}' from JWT not allowed for route")
|
|
41
|
+
|
|
42
|
+
except (JWTInvalidData, JWTMalformed) as ex:
|
|
43
|
+
inc_metric(metric, event=JWT_Event.FAIL_INVALID_JWT)
|
|
44
|
+
return response_cls(f"Forbidden: '{ex.message}'", status=401)
|
|
45
|
+
|
|
46
|
+
except JWTInvalidAudience as ex:
|
|
47
|
+
inc_metric(metric, event=JWT_Event.FAIL_WRONG_AUDIENCE)
|
|
48
|
+
return response_cls(f"Forbidden: '{ex.message}'", status=403)
|
|
49
|
+
|
|
50
|
+
except JWTDisallowedUser as ex:
|
|
51
|
+
inc_metric(metric, event=JWT_Event.FAIL_WRONG_USER)
|
|
52
|
+
return response_cls(f"Forbidden: '{ex.message}'", status=403)
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def requires_iap_jwt(
|
|
58
|
+
jwt_audience: str,
|
|
59
|
+
response_cls: t.Type[Response] = Response,
|
|
60
|
+
jwt_header_key: str = GOOGLE_IAP_JWT_HEADER_KEY,
|
|
61
|
+
metric: Counter | None = default_metric
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
A decorator that ensures the incoming request has a valid IAP JWT for a Flask route,
|
|
65
|
+
and that the user in the JWT has permission for the route.
|
|
66
|
+
|
|
67
|
+
Params:
|
|
68
|
+
jwt_audience: JWT Audience string (or IAP Client ID) to verify JWT against
|
|
69
|
+
response_cls: Flask response class or subclass thereof to return from decorator
|
|
70
|
+
jwt_header_key: request header key from which to retrieve the JWT (Default: 'x-goog-iap-jwt-assertion')
|
|
71
|
+
metric:
|
|
72
|
+
prometheus_client.Counter object (or None) to inc() for different outcomes.
|
|
73
|
+
Must have a single label: 'event'.
|
|
74
|
+
Set metric param to 'None' to disable metrics.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Flask response of type determined by response_cls param on JWT Failure, else result of decorated view function
|
|
78
|
+
"""
|
|
79
|
+
def decorator(f: t.Callable) -> t.Callable:
|
|
80
|
+
|
|
81
|
+
@wraps(f)
|
|
82
|
+
def decorated_function(*args, **kwargs) -> Response:
|
|
83
|
+
resp: Response | None = _verify_jwt(
|
|
84
|
+
request,
|
|
85
|
+
jwt_header_key=jwt_header_key,
|
|
86
|
+
jwt_audience=jwt_audience,
|
|
87
|
+
allowed_users=None,
|
|
88
|
+
response_cls=response_cls,
|
|
89
|
+
metric=metric
|
|
90
|
+
)
|
|
91
|
+
if resp is not None:
|
|
92
|
+
return resp
|
|
93
|
+
return f(*args, **kwargs)
|
|
94
|
+
|
|
95
|
+
return decorated_function
|
|
96
|
+
|
|
97
|
+
return decorator
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def requires_iap_jwt_valid_user(
|
|
101
|
+
jwt_audience: str,
|
|
102
|
+
allowed_users: set[str],
|
|
103
|
+
response_cls: t.Type[Response] = Response,
|
|
104
|
+
jwt_header_key: str = GOOGLE_IAP_JWT_HEADER_KEY,
|
|
105
|
+
metric: Counter | None = default_metric
|
|
106
|
+
):
|
|
107
|
+
"""
|
|
108
|
+
A decorator that ensures the incoming request has a valid IAP JWT for a Flask route,
|
|
109
|
+
and that the user in the JWT has permission for the route
|
|
110
|
+
|
|
111
|
+
Params:
|
|
112
|
+
jwt_audience: JWT Audience string (or IAP Client ID) to verify JWT against
|
|
113
|
+
allowed_users: set of email strings to check against user_email in JWT for permission to access decorated view func
|
|
114
|
+
response_cls: Flask response class or subclass thereof to return from decorator
|
|
115
|
+
jwt_header_key: request header key from which to retrieve the JWT (Default: 'x-goog-iap-jwt-assertion')
|
|
116
|
+
metric:
|
|
117
|
+
prometheus_client.Counter object (or None) to inc() for different outcomes.
|
|
118
|
+
Must have a single label: 'event'.
|
|
119
|
+
Set metric param to 'None' to disable metrics.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Flask response of type determined by response_cls param on JWT Failure, else result of decorated view function
|
|
123
|
+
"""
|
|
124
|
+
def decorator(f: t.Callable) -> t.Callable:
|
|
125
|
+
|
|
126
|
+
@wraps(f)
|
|
127
|
+
def decorated_function(*args, **kwargs) -> Response:
|
|
128
|
+
resp: Response | None = _verify_jwt(
|
|
129
|
+
request,
|
|
130
|
+
jwt_header_key=jwt_header_key,
|
|
131
|
+
jwt_audience=jwt_audience,
|
|
132
|
+
allowed_users=allowed_users,
|
|
133
|
+
response_cls=response_cls,
|
|
134
|
+
metric=metric
|
|
135
|
+
)
|
|
136
|
+
if resp is not None:
|
|
137
|
+
return resp
|
|
138
|
+
return f(*args, **kwargs)
|
|
139
|
+
|
|
140
|
+
return decorated_function
|
|
141
|
+
|
|
142
|
+
return decorator
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import typing as t
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from prometheus_client import Counter
|
|
6
|
+
# TODO move to constants
|
|
7
|
+
default_metric = Counter(
|
|
8
|
+
"iaptoolkit_jwt_event_total",
|
|
9
|
+
"Count of JWT verification events",
|
|
10
|
+
labelnames=["event"]
|
|
11
|
+
)
|
|
12
|
+
except ImportError:
|
|
13
|
+
Counter = t.TypeVar("Counter")
|
|
14
|
+
default_metric = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def inc_metric(metric: Counter | None, event: str):
|
|
18
|
+
if not metric:
|
|
19
|
+
return
|
|
20
|
+
metric.labels(event=event).inc()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def inc_metric_async(metric: Counter | None, event: str):
|
|
24
|
+
if not metric:
|
|
25
|
+
return
|
|
26
|
+
# TODO: Does prometheus_client have built-in async support?
|
|
27
|
+
await asyncio.to_thread(metric.labels(event=event).inc)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"default_metric",
|
|
31
|
+
"Counter",
|
|
32
|
+
"inc_metric",
|
|
33
|
+
"inc_metric_async"
|
|
34
|
+
]
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import typing as t
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
from quart import request
|
|
5
|
+
from quart.wrappers import Request
|
|
6
|
+
from quart.wrappers import Response
|
|
7
|
+
|
|
8
|
+
from iaptoolkit.constants import GOOGLE_IAP_JWT_HEADER_KEY
|
|
9
|
+
from iaptoolkit.exceptions import JWTDisallowedUser
|
|
10
|
+
from iaptoolkit.exceptions import JWTInvalidAudience
|
|
11
|
+
from iaptoolkit.exceptions import JWTInvalidData
|
|
12
|
+
from iaptoolkit.exceptions import JWTMalformed
|
|
13
|
+
from iaptoolkit.jwt.constants import JWT_Event
|
|
14
|
+
from .constants import JWT_Event
|
|
15
|
+
from .metrics import Counter
|
|
16
|
+
from .metrics import default_metric
|
|
17
|
+
from .metrics import inc_metric_async
|
|
18
|
+
from .verify_async import verify_iap_jwt_async
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _verify_jwt_async(
|
|
22
|
+
request: Request,
|
|
23
|
+
jwt_header_key: str,
|
|
24
|
+
jwt_audience: str,
|
|
25
|
+
allowed_users: set[str] | None = None,
|
|
26
|
+
response_cls: t.Type[Response] = Response,
|
|
27
|
+
metric: Counter | None = default_metric
|
|
28
|
+
) -> Response | None:
|
|
29
|
+
|
|
30
|
+
jwt_header: str = request.headers.get(jwt_header_key.lower(), "")
|
|
31
|
+
if not jwt_header:
|
|
32
|
+
await inc_metric_async(metric, event=JWT_Event.FAIL_NO_HEADER)
|
|
33
|
+
return response_cls(f"No Google IAP JWT header in request at key: '{jwt_header_key}'", status=401)
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
user_email = await verify_iap_jwt_async(iap_jwt=jwt_header, expected_audience=jwt_audience)
|
|
37
|
+
if not user_email:
|
|
38
|
+
raise JWTInvalidData("No user_email in decoded JWT")
|
|
39
|
+
|
|
40
|
+
if allowed_users and user_email not in allowed_users:
|
|
41
|
+
raise JWTDisallowedUser(message=f"User '{user_email}' from JWT not allowed for route")
|
|
42
|
+
|
|
43
|
+
except (JWTInvalidData, JWTMalformed) as ex:
|
|
44
|
+
await inc_metric_async(metric, event=JWT_Event.FAIL_INVALID_JWT)
|
|
45
|
+
return response_cls(f"Forbidden: '{ex.message}'", status=401)
|
|
46
|
+
|
|
47
|
+
except JWTInvalidAudience as ex:
|
|
48
|
+
await inc_metric_async(metric, event=JWT_Event.FAIL_WRONG_AUDIENCE)
|
|
49
|
+
return response_cls(f"Forbidden: '{ex.message}'", status=403)
|
|
50
|
+
|
|
51
|
+
except JWTDisallowedUser as ex:
|
|
52
|
+
await inc_metric_async(metric, event=JWT_Event.FAIL_WRONG_USER)
|
|
53
|
+
return response_cls(f"Forbidden: '{ex.message}'", status=403)
|
|
54
|
+
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def requires_iap_jwt(
|
|
59
|
+
jwt_audience: str,
|
|
60
|
+
response_cls: t.Type[Response] = Response,
|
|
61
|
+
jwt_header_key: str = GOOGLE_IAP_JWT_HEADER_KEY,
|
|
62
|
+
metric: Counter | None = default_metric
|
|
63
|
+
):
|
|
64
|
+
"""
|
|
65
|
+
A decorator that ensures the incoming request has a valid IAP JWT for a Quart route,
|
|
66
|
+
and that the user in the JWT has permission for the route.
|
|
67
|
+
|
|
68
|
+
Params:
|
|
69
|
+
jwt_audience: JWT Audience string (or IAP Client ID) to verify JWT against
|
|
70
|
+
response_cls: Quart response class or subclass thereof to return from decorator
|
|
71
|
+
jwt_header_key: request header key from which to retrieve the JWT (Default: 'x-goog-iap-jwt-assertion')
|
|
72
|
+
metric:
|
|
73
|
+
prometheus_client.Counter object (or None) to inc() for different outcomes.
|
|
74
|
+
Must have a single label: 'event'.
|
|
75
|
+
Set metric param to 'None' to disable metrics.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Quart response of type determined by response_cls param on JWT Failure, else result of decorated view function
|
|
79
|
+
"""
|
|
80
|
+
def decorator(f: t.Callable) -> t.Callable:
|
|
81
|
+
|
|
82
|
+
@wraps(f)
|
|
83
|
+
async def decorated_function(*args, **kwargs) -> Response:
|
|
84
|
+
resp: Response | None = await _verify_jwt_async(
|
|
85
|
+
request,
|
|
86
|
+
jwt_header_key=jwt_header_key,
|
|
87
|
+
jwt_audience=jwt_audience,
|
|
88
|
+
allowed_users=None,
|
|
89
|
+
response_cls=response_cls,
|
|
90
|
+
metric=metric
|
|
91
|
+
)
|
|
92
|
+
if resp is not None:
|
|
93
|
+
return resp
|
|
94
|
+
return await f(*args, **kwargs)
|
|
95
|
+
|
|
96
|
+
return decorated_function
|
|
97
|
+
|
|
98
|
+
return decorator
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def requires_iap_jwt_valid_user_async(
|
|
102
|
+
jwt_audience: str,
|
|
103
|
+
allowed_users: set[str],
|
|
104
|
+
response_cls: t.Type[Response] = Response,
|
|
105
|
+
jwt_header_key: str = GOOGLE_IAP_JWT_HEADER_KEY,
|
|
106
|
+
metric: Counter | None = default_metric
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
A decorator that ensures the incoming request has a valid IAP JWT for a Quart route,
|
|
110
|
+
and that the user in the JWT has permission for the route.
|
|
111
|
+
|
|
112
|
+
Params:
|
|
113
|
+
jwt_audience: JWT Audience string (or IAP Client ID) to verify JWT against
|
|
114
|
+
allowed_users: set of email strings to check against user_email in JWT for permission to access decorated view func
|
|
115
|
+
response_cls: Quart response class or subclass thereof to return from decorator
|
|
116
|
+
jwt_header_key: request header key from which to retrieve the JWT (Default: 'x-goog-iap-jwt-assertion')
|
|
117
|
+
metric:
|
|
118
|
+
prometheus_client.Counter object (or None) to inc() for different outcomes.
|
|
119
|
+
Must have a single label: 'event'.
|
|
120
|
+
Set metric param to 'None' to disable metrics.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Quart response of type determined by response_cls param on JWT Failure, else result of decorated view function
|
|
124
|
+
"""
|
|
125
|
+
def decorator(f: t.Callable) -> t.Callable:
|
|
126
|
+
|
|
127
|
+
@wraps(f)
|
|
128
|
+
async def decorated_function(*args, **kwargs) -> Response:
|
|
129
|
+
resp: Response | None = await _verify_jwt_async(
|
|
130
|
+
request,
|
|
131
|
+
jwt_header_key=jwt_header_key,
|
|
132
|
+
jwt_audience=jwt_audience,
|
|
133
|
+
allowed_users=allowed_users,
|
|
134
|
+
response_cls=response_cls,
|
|
135
|
+
metric=metric
|
|
136
|
+
)
|
|
137
|
+
if resp is not None:
|
|
138
|
+
return resp
|
|
139
|
+
return await f(*args, **kwargs)
|
|
140
|
+
|
|
141
|
+
return decorated_function
|
|
142
|
+
|
|
143
|
+
return decorator
|
|
File without changes
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import typing as t
|
|
2
|
-
from functools import wraps
|
|
3
|
-
|
|
4
|
-
from flask import request
|
|
5
|
-
from flask.wrappers import Response
|
|
6
|
-
|
|
7
|
-
from iaptoolkit.constants import GOOGLE_IAP_JWT_HEADER_KEY
|
|
8
|
-
from iaptoolkit.exceptions import JWTDisallowedUser
|
|
9
|
-
from iaptoolkit.exceptions import JWTInvalidAudience
|
|
10
|
-
from iaptoolkit.exceptions import JWTInvalidData
|
|
11
|
-
from iaptoolkit.exceptions import JWTMalformed
|
|
12
|
-
from iaptoolkit.utils.verify import verify_iap_jwt
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def requires_iap_jwt(
|
|
16
|
-
jwt_audience: str,
|
|
17
|
-
allowed_users: set[str],
|
|
18
|
-
response_cls: Response = Response,
|
|
19
|
-
jwt_header_key: str = GOOGLE_IAP_JWT_HEADER_KEY
|
|
20
|
-
):
|
|
21
|
-
"""
|
|
22
|
-
A decorator that ensures the incoming request has a valid IAP JWT for a Flask route,
|
|
23
|
-
and that the user in the JWT has permission for the route
|
|
24
|
-
"""
|
|
25
|
-
def decorator(f):
|
|
26
|
-
|
|
27
|
-
@wraps(f)
|
|
28
|
-
def decorated_function(*args, **kwargs):
|
|
29
|
-
|
|
30
|
-
jwt_header: str = request.headers.get(jwt_header_key.lower(), None)
|
|
31
|
-
if not jwt_header:
|
|
32
|
-
return response_cls(f"No Google IAP JWT header in request at key: '{jwt_header_key}'", status=401)
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
user_email = verify_iap_jwt(iap_jwt=jwt_header, expected_audience=jwt_audience)
|
|
36
|
-
if not user_email:
|
|
37
|
-
raise JWTInvalidData("No user_email in decoded JWT")
|
|
38
|
-
|
|
39
|
-
if allowed_users and user_email not in allowed_users:
|
|
40
|
-
raise JWTDisallowedUser(message=f"User '{user_email}' from JWT not allowed on route")
|
|
41
|
-
|
|
42
|
-
except (JWTInvalidData, JWTMalformed) as ex:
|
|
43
|
-
return response_cls(f"Forbidden: '{ex.message}'", status=401)
|
|
44
|
-
|
|
45
|
-
except (JWTInvalidAudience, JWTDisallowedUser) as ex:
|
|
46
|
-
return response_cls(f"Forbidden: '{ex.message}'", status=403)
|
|
47
|
-
|
|
48
|
-
return f(*args, **kwargs)
|
|
49
|
-
|
|
50
|
-
return decorated_function
|
|
51
|
-
|
|
52
|
-
return decorator
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import typing as t
|
|
2
|
-
from functools import wraps
|
|
3
|
-
|
|
4
|
-
from quart import request
|
|
5
|
-
from quart.wrappers import Response
|
|
6
|
-
|
|
7
|
-
from iaptoolkit.constants import GOOGLE_IAP_JWT_HEADER_KEY
|
|
8
|
-
from iaptoolkit.exceptions import JWTDisallowedUser
|
|
9
|
-
from iaptoolkit.exceptions import JWTInvalidAudience
|
|
10
|
-
from iaptoolkit.exceptions import JWTInvalidData
|
|
11
|
-
from iaptoolkit.exceptions import JWTMalformed
|
|
12
|
-
from iaptoolkit.utils.verify_async import verify_iap_jwt_async
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def requires_iap_jwt_async(
|
|
16
|
-
jwt_audience: str,
|
|
17
|
-
allowed_users: set[str],
|
|
18
|
-
response_cls: Response = Response,
|
|
19
|
-
jwt_header_key: str = GOOGLE_IAP_JWT_HEADER_KEY
|
|
20
|
-
):
|
|
21
|
-
"""
|
|
22
|
-
A decorator that ensures the incoming request has a valid IAP JWT for a Quart route,
|
|
23
|
-
and that the user in the JWT has permission for the route
|
|
24
|
-
"""
|
|
25
|
-
def decorator(f):
|
|
26
|
-
|
|
27
|
-
@wraps(f)
|
|
28
|
-
async def decorated_function(*args, **kwargs):
|
|
29
|
-
|
|
30
|
-
jwt_header: str = request.headers.get(jwt_header_key.lower(), None)
|
|
31
|
-
if not jwt_header:
|
|
32
|
-
return response_cls(f"No Google IAP JWT header in request at key: '{jwt_header_key}'", status=401)
|
|
33
|
-
|
|
34
|
-
try:
|
|
35
|
-
user_email = await verify_iap_jwt_async(iap_jwt=jwt_header, expected_audience=jwt_audience)
|
|
36
|
-
if not user_email:
|
|
37
|
-
raise JWTInvalidData("No user_email in decoded JWT")
|
|
38
|
-
|
|
39
|
-
if allowed_users and user_email not in allowed_users:
|
|
40
|
-
raise JWTDisallowedUser(message=f"User '{user_email}' from JWT not allowed on route")
|
|
41
|
-
|
|
42
|
-
except (JWTInvalidData, JWTMalformed) as ex:
|
|
43
|
-
return response_cls(f"Forbidden: '{ex.message}'", status=401)
|
|
44
|
-
|
|
45
|
-
except (JWTInvalidAudience, JWTDisallowedUser) as ex:
|
|
46
|
-
return response_cls(f"Forbidden: '{ex.message}'", status=403)
|
|
47
|
-
|
|
48
|
-
return await f(*args, **kwargs)
|
|
49
|
-
|
|
50
|
-
return decorated_function
|
|
51
|
-
|
|
52
|
-
return decorator
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{iaptoolkit-0.3.7a1/src/iaptoolkit/utils → iaptoolkit-0.3.7a2/src/iaptoolkit/jwt}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{iaptoolkit-0.3.7a1/src/iaptoolkit/utils → iaptoolkit-0.3.7a2/src/iaptoolkit/jwt}/verify_async.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|