aiohttp-msal 1.0.3__py3-none-any.whl → 1.0.4__py3-none-any.whl
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.
- aiohttp_msal/__init__.py +15 -3
- aiohttp_msal/helpers.py +136 -0
- aiohttp_msal/msal_async.py +18 -31
- aiohttp_msal/routes.py +30 -104
- aiohttp_msal/utils.py +32 -4
- {aiohttp_msal-1.0.3.dist-info → aiohttp_msal-1.0.4.dist-info}/METADATA +1 -1
- aiohttp_msal-1.0.4.dist-info/RECORD +11 -0
- aiohttp_msal/user_info.py +0 -32
- aiohttp_msal-1.0.3.dist-info/RECORD +0 -11
- {aiohttp_msal-1.0.3.dist-info → aiohttp_msal-1.0.4.dist-info}/WHEEL +0 -0
aiohttp_msal/__init__.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""aiohttp_msal."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import logging
|
|
4
5
|
from collections.abc import Awaitable, Callable
|
|
5
6
|
from functools import wraps
|
|
6
7
|
from inspect import getfullargspec, iscoroutinefunction
|
|
7
|
-
from typing import TypeVar, TypeVarTuple, cast
|
|
8
|
+
from typing import Any, TypeVar, TypeVarTuple, cast
|
|
8
9
|
|
|
9
10
|
from aiohttp import ClientSession, web
|
|
10
11
|
from aiohttp_session import get_session
|
|
@@ -12,6 +13,7 @@ from aiohttp_session import setup as _setup
|
|
|
12
13
|
|
|
13
14
|
from aiohttp_msal.msal_async import AsyncMSAL
|
|
14
15
|
from aiohttp_msal.settings import ENV
|
|
16
|
+
from aiohttp_msal.utils import retry
|
|
15
17
|
|
|
16
18
|
_LOGGER = logging.getLogger(__name__)
|
|
17
19
|
|
|
@@ -87,7 +89,11 @@ def auth_or(
|
|
|
87
89
|
|
|
88
90
|
|
|
89
91
|
async def app_init_redis_session(
|
|
90
|
-
app: web.Application,
|
|
92
|
+
app: web.Application,
|
|
93
|
+
max_age: int = 3600 * 24 * 90,
|
|
94
|
+
check_proxy_cb: Callable[[], Awaitable[None]] | None = None,
|
|
95
|
+
encoder: Callable[[object], str] = json.dumps,
|
|
96
|
+
decoder: Callable[[str], Any] = json.loads,
|
|
91
97
|
) -> None:
|
|
92
98
|
"""Init an aiohttp_session with Redis storage helper.
|
|
93
99
|
|
|
@@ -96,7 +102,10 @@ async def app_init_redis_session(
|
|
|
96
102
|
from aiohttp_session import redis_storage
|
|
97
103
|
from redis.asyncio import from_url
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
if check_proxy_cb:
|
|
106
|
+
await check_proxy_cb()
|
|
107
|
+
else:
|
|
108
|
+
await check_proxy()
|
|
100
109
|
|
|
101
110
|
_LOGGER.info("Connect to Redis %s", ENV.REDIS)
|
|
102
111
|
try:
|
|
@@ -114,10 +123,13 @@ async def app_init_redis_session(
|
|
|
114
123
|
secure=True,
|
|
115
124
|
domain=ENV.DOMAIN,
|
|
116
125
|
cookie_name=ENV.COOKIE_NAME,
|
|
126
|
+
encoder=encoder,
|
|
127
|
+
decoder=decoder,
|
|
117
128
|
)
|
|
118
129
|
_setup(app, storage)
|
|
119
130
|
|
|
120
131
|
|
|
132
|
+
@retry
|
|
121
133
|
async def check_proxy() -> None:
|
|
122
134
|
"""Test if we have Internet connectivity through proxies etc."""
|
|
123
135
|
try:
|
aiohttp_msal/helpers.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Graph User Info."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping, Sequence
|
|
4
|
+
from typing import Any, Literal, TypeVar
|
|
5
|
+
|
|
6
|
+
from aiohttp import web
|
|
7
|
+
from aiohttp_session import get_session
|
|
8
|
+
|
|
9
|
+
from aiohttp_msal import ENV
|
|
10
|
+
from aiohttp_msal.msal_async import AsyncMSAL
|
|
11
|
+
from aiohttp_msal.utils import retry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@retry
|
|
15
|
+
async def get_user_info(aiomsal: AsyncMSAL) -> None:
|
|
16
|
+
"""Load user info from MS graph API. Requires User.Read permissions."""
|
|
17
|
+
async with aiomsal.get("https://graph.microsoft.com/v1.0/me") as res:
|
|
18
|
+
body = await res.json()
|
|
19
|
+
try:
|
|
20
|
+
aiomsal.mail = body["mail"]
|
|
21
|
+
aiomsal.name = body["displayName"]
|
|
22
|
+
except KeyError as err:
|
|
23
|
+
raise KeyError(
|
|
24
|
+
f"Unexpected return from Graph endpoint: {body}: {err}"
|
|
25
|
+
) from err
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@retry
|
|
29
|
+
async def get_manager_info(aiomsal: AsyncMSAL) -> None:
|
|
30
|
+
"""Load manager info from MS graph API. Requires User.Read.All permissions."""
|
|
31
|
+
async with aiomsal.get("https://graph.microsoft.com/v1.0/me/manager") as res:
|
|
32
|
+
body = await res.json()
|
|
33
|
+
try:
|
|
34
|
+
aiomsal.manager_mail = body["mail"]
|
|
35
|
+
aiomsal.manager_name = body["displayName"]
|
|
36
|
+
except KeyError as err:
|
|
37
|
+
raise KeyError(
|
|
38
|
+
f"Unexpected return from Graph endpoint: {body}: {err}"
|
|
39
|
+
) from err
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def html_table(items: Mapping[Any, Any]) -> str:
|
|
43
|
+
"""Return a table HTML."""
|
|
44
|
+
res = "<table style='width:80%;border:1px solid black;'>"
|
|
45
|
+
for key, val in items.items():
|
|
46
|
+
res += f"<tr><td>{key}</td><td>{val}</td></tr>"
|
|
47
|
+
res += "</table>"
|
|
48
|
+
return res
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def html_wrap(msgs: Sequence[str]) -> str:
|
|
52
|
+
"""Return proper HTML when login fails."""
|
|
53
|
+
html = "</li><li>".join(msgs)
|
|
54
|
+
return f"""
|
|
55
|
+
<h2>Login failed</h2>
|
|
56
|
+
|
|
57
|
+
<p>Retry at <a href='/user/login'>/user/login</a></p>
|
|
58
|
+
|
|
59
|
+
<p>Try clearing the cookies for <b>.{ENV.DOMAIN}<b> by navigating to the correct
|
|
60
|
+
address for your browser:
|
|
61
|
+
<ul>
|
|
62
|
+
<li>chrome://settings/siteData?searchSubpage={ENV.DOMAIN}</li>
|
|
63
|
+
<li>brave://settings/siteData?searchSubpage={ENV.DOMAIN}</li>
|
|
64
|
+
<li>edge://settings/siteData (you will have to search for {ENV.DOMAIN} cookies)</li>
|
|
65
|
+
</ul></p>
|
|
66
|
+
|
|
67
|
+
<h4>Debug info</h4>
|
|
68
|
+
<ul><li>{html}</li></ul>
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
TA = TypeVar("TA", bound=AsyncMSAL)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def check_auth_response(
|
|
76
|
+
request: web.Request,
|
|
77
|
+
asyncmsal_class: type[TA] = AsyncMSAL, # type:ignore[assignment]
|
|
78
|
+
get_info: Literal["user", "manager", ""] = "manager",
|
|
79
|
+
) -> tuple[TA | None, list[str]]:
|
|
80
|
+
"""Parse the MS auth response."""
|
|
81
|
+
# Expecting response_mode="form_post"
|
|
82
|
+
auth_response = dict(await request.post())
|
|
83
|
+
|
|
84
|
+
msg = list[str]()
|
|
85
|
+
|
|
86
|
+
# Ensure all expected variables were returned...
|
|
87
|
+
if not all(auth_response.get(k) for k in ["code", "session_state", "state"]):
|
|
88
|
+
msg.append("Expected 'code', 'session_state', 'state' in auth_response")
|
|
89
|
+
msg.append(f"Received auth_response: {list(auth_response)}")
|
|
90
|
+
return None, msg
|
|
91
|
+
|
|
92
|
+
if not request.cookies.get(ENV.COOKIE_NAME):
|
|
93
|
+
cookies = dict(request.cookies.items())
|
|
94
|
+
msg.append(f"<b>Expected '{ENV.COOKIE_NAME}' in cookies</b>")
|
|
95
|
+
msg.append(html_table(cookies))
|
|
96
|
+
msg.append("Cookie should be set with Samesite:None")
|
|
97
|
+
|
|
98
|
+
session = await get_session(request)
|
|
99
|
+
if session.new:
|
|
100
|
+
msg.append(
|
|
101
|
+
"Warning: This is a new session and may not have all expected values."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if not session.get(asyncmsal_class.flow_cache_key):
|
|
105
|
+
msg.append(f"<b>Expected '{asyncmsal_class.flow_cache_key}' in session</b>")
|
|
106
|
+
msg.append(html_table(session))
|
|
107
|
+
|
|
108
|
+
aiomsal = asyncmsal_class(session)
|
|
109
|
+
aiomsal.redirect = "/" + aiomsal.redirect.lstrip("/")
|
|
110
|
+
|
|
111
|
+
if msg:
|
|
112
|
+
return aiomsal, msg
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
await aiomsal.async_acquire_token_by_auth_code_flow(auth_response)
|
|
116
|
+
except Exception as err:
|
|
117
|
+
msg.append("<b>Could not get token</b> - async_acquire_token_by_auth_code_flow")
|
|
118
|
+
msg.append(str(err))
|
|
119
|
+
|
|
120
|
+
if not msg:
|
|
121
|
+
try:
|
|
122
|
+
if get_info in ("user", "manager"):
|
|
123
|
+
await get_user_info(aiomsal)
|
|
124
|
+
if get_info == "manager":
|
|
125
|
+
await get_manager_info(aiomsal)
|
|
126
|
+
except Exception as err:
|
|
127
|
+
msg.append("Could not get org info from MS graph")
|
|
128
|
+
msg.append(str(err))
|
|
129
|
+
aiomsal.mail = ""
|
|
130
|
+
aiomsal.name = ""
|
|
131
|
+
|
|
132
|
+
if session.get("mail"):
|
|
133
|
+
for lcb in ENV.login_callback:
|
|
134
|
+
await lcb(aiomsal)
|
|
135
|
+
|
|
136
|
+
return aiomsal, msg
|
aiohttp_msal/msal_async.py
CHANGED
|
@@ -9,7 +9,7 @@ import asyncio
|
|
|
9
9
|
import json
|
|
10
10
|
from collections.abc import Callable
|
|
11
11
|
from functools import cached_property, partialmethod
|
|
12
|
-
from typing import Any, ClassVar, Literal, Unpack
|
|
12
|
+
from typing import Any, ClassVar, Literal, Unpack, cast
|
|
13
13
|
|
|
14
14
|
import attrs
|
|
15
15
|
from aiohttp import web
|
|
@@ -24,6 +24,7 @@ from aiohttp_session import Session
|
|
|
24
24
|
from msal import ConfidentialClientApplication, SerializableTokenCache
|
|
25
25
|
|
|
26
26
|
from aiohttp_msal.settings import ENV
|
|
27
|
+
from aiohttp_msal.utils import dict_property
|
|
27
28
|
|
|
28
29
|
HttpMethods = Literal["get", "post", "put", "patch", "delete"]
|
|
29
30
|
HTTP_GET = "get"
|
|
@@ -33,11 +34,8 @@ HTTP_PATCH = "patch"
|
|
|
33
34
|
HTTP_DELETE = "delete"
|
|
34
35
|
HTTP_ALLOWED = [HTTP_GET, HTTP_POST, HTTP_PUT, HTTP_PATCH, HTTP_DELETE]
|
|
35
36
|
|
|
36
|
-
DEFAULT_SCOPES = ["User.Read", "User.Read.All"]
|
|
37
|
-
FLOW_CACHE = "flow_cache"
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
@attrs.define()
|
|
38
|
+
@attrs.define(slots=False)
|
|
41
39
|
class AsyncMSAL:
|
|
42
40
|
"""AsycMSAL class.
|
|
43
41
|
|
|
@@ -58,8 +56,11 @@ class AsyncMSAL:
|
|
|
58
56
|
"""ConfidentialClientApplication kwargs."""
|
|
59
57
|
client_session: ClassVar[ClientSession | None] = None
|
|
60
58
|
|
|
61
|
-
token_cache_key: str = "token_cache"
|
|
62
|
-
user_email_key: str = "mail"
|
|
59
|
+
token_cache_key: ClassVar[str] = "token_cache"
|
|
60
|
+
user_email_key: ClassVar[str] = "mail"
|
|
61
|
+
flow_cache_key: ClassVar[str] = "flow_cache"
|
|
62
|
+
redirect_key = "redirect"
|
|
63
|
+
default_scopes: ClassVar[list[str]] = ["User.Read", "User.Read.All"]
|
|
63
64
|
|
|
64
65
|
@cached_property
|
|
65
66
|
def app(self) -> ConfidentialClientApplication:
|
|
@@ -100,8 +101,8 @@ class AsyncMSAL:
|
|
|
100
101
|
"""First step - Start the flow."""
|
|
101
102
|
self.session.pop(self.token_cache_key, None)
|
|
102
103
|
self.session.pop(self.user_email_key, None)
|
|
103
|
-
self.session[
|
|
104
|
-
scopes or
|
|
104
|
+
self.session[self.flow_cache_key] = res = self.app.initiate_auth_code_flow(
|
|
105
|
+
scopes or self.default_scopes,
|
|
105
106
|
redirect_uri=redirect_uri,
|
|
106
107
|
response_mode="form_post",
|
|
107
108
|
prompt=prompt,
|
|
@@ -118,7 +119,7 @@ class AsyncMSAL:
|
|
|
118
119
|
"""Second step - Acquire token."""
|
|
119
120
|
# Assume we have it in the cache (added by /login)
|
|
120
121
|
# will raise keryerror if no cache
|
|
121
|
-
auth_code_flow = self.session.pop(
|
|
122
|
+
auth_code_flow = self.session.pop(self.flow_cache_key)
|
|
122
123
|
result = self.app.acquire_token_by_auth_code_flow(
|
|
123
124
|
auth_code_flow, auth_response, scopes=scopes
|
|
124
125
|
)
|
|
@@ -141,7 +142,7 @@ class AsyncMSAL:
|
|
|
141
142
|
accounts = self.app.get_accounts()
|
|
142
143
|
if accounts:
|
|
143
144
|
result = self.app.acquire_token_silent(
|
|
144
|
-
scopes=scopes or
|
|
145
|
+
scopes=scopes or self.default_scopes, account=accounts[0]
|
|
145
146
|
)
|
|
146
147
|
self.save_token_cache()
|
|
147
148
|
return result
|
|
@@ -197,27 +198,13 @@ class AsyncMSAL:
|
|
|
197
198
|
get = partialmethod(request_ctx, HTTP_GET)
|
|
198
199
|
post = partialmethod(request_ctx, HTTP_POST)
|
|
199
200
|
|
|
200
|
-
@property
|
|
201
|
-
def mail(self) -> str:
|
|
202
|
-
"""User email."""
|
|
203
|
-
return self.session.get(self.user_email_key, "")
|
|
204
|
-
|
|
205
|
-
@property
|
|
206
|
-
def manager_mail(self) -> str:
|
|
207
|
-
"""User's manager's email."""
|
|
208
|
-
return self.session.get("m_mail", "")
|
|
209
|
-
|
|
210
|
-
@property
|
|
211
|
-
def manager_name(self) -> str:
|
|
212
|
-
"""User's manager's name."""
|
|
213
|
-
return self.session.get("m_name", "")
|
|
214
|
-
|
|
215
|
-
@property
|
|
216
|
-
def name(self) -> str:
|
|
217
|
-
"""User's display name."""
|
|
218
|
-
return self.session.get("name", "")
|
|
219
|
-
|
|
220
201
|
@property
|
|
221
202
|
def authenticated(self) -> bool:
|
|
222
203
|
"""If the user is logged in."""
|
|
223
204
|
return bool(self.session.get(self.user_email_key))
|
|
205
|
+
|
|
206
|
+
name = cast(str, dict_property("session", "name"))
|
|
207
|
+
mail = dict_property("session", user_email_key)
|
|
208
|
+
manager_name = dict_property("session", "m_name")
|
|
209
|
+
manager_mail = dict_property("session", "m_mail")
|
|
210
|
+
redirect = dict_property("session", redirect_key)
|
aiohttp_msal/routes.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""The user blueprint."""
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
-
from collections.abc import Mapping, Sequence
|
|
5
4
|
from inspect import iscoroutinefunction
|
|
6
5
|
from typing import Any
|
|
7
6
|
from urllib.parse import urljoin
|
|
@@ -9,9 +8,14 @@ from urllib.parse import urljoin
|
|
|
9
8
|
from aiohttp import web
|
|
10
9
|
from aiohttp_session import get_session, new_session
|
|
11
10
|
|
|
12
|
-
from aiohttp_msal import
|
|
13
|
-
from aiohttp_msal.
|
|
14
|
-
|
|
11
|
+
from aiohttp_msal import ENV, auth_ok, msal_session
|
|
12
|
+
from aiohttp_msal.helpers import (
|
|
13
|
+
check_auth_response,
|
|
14
|
+
get_manager_info,
|
|
15
|
+
get_user_info,
|
|
16
|
+
html_wrap,
|
|
17
|
+
)
|
|
18
|
+
from aiohttp_msal.msal_async import AsyncMSAL
|
|
15
19
|
|
|
16
20
|
ROUTES = web.RouteTableDef()
|
|
17
21
|
|
|
@@ -27,7 +31,7 @@ def get_route(request: web.Request, url: str) -> str:
|
|
|
27
31
|
"""
|
|
28
32
|
url = str(request.url.origin() / url)
|
|
29
33
|
if "localhost" not in url:
|
|
30
|
-
url = url.replace("
|
|
34
|
+
url = url.replace("http:", "https:", 1)
|
|
31
35
|
return url
|
|
32
36
|
|
|
33
37
|
|
|
@@ -45,83 +49,35 @@ async def user_login(request: web.Request) -> web.Response:
|
|
|
45
49
|
|
|
46
50
|
msredirect = get_route(request, URI_USER_AUTHORIZED.lstrip("/"))
|
|
47
51
|
redir = AsyncMSAL(session).initiate_auth_code_flow(redirect_uri=msredirect)
|
|
48
|
-
|
|
52
|
+
raise web.HTTPFound(redir)
|
|
49
53
|
|
|
50
54
|
|
|
51
55
|
@ROUTES.post(URI_USER_AUTHORIZED)
|
|
52
56
|
async def user_authorized(request: web.Request) -> web.Response:
|
|
53
57
|
"""Complete the auth code flow."""
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# build a plain dict from the aiohttp server request's url parameters
|
|
57
|
-
# pre-0.1.18. Now we have response_mode="form_post"
|
|
58
|
-
# auth_response = dict(request.rel_url.query.items())
|
|
59
|
-
auth_response = dict(await request.post())
|
|
60
|
-
|
|
61
|
-
msg = []
|
|
62
|
-
response_cookie = 0
|
|
63
|
-
|
|
64
|
-
# Ensure all expected variables were returned...
|
|
65
|
-
if not all(auth_response.get(k) for k in ["code", "session_state", "state"]):
|
|
66
|
-
msg.append(
|
|
67
|
-
f"<b>Expecting code,state,session_state in post body.</b>auth_response: {auth_response}"
|
|
68
|
-
)
|
|
58
|
+
aiomsal, msg = await check_auth_response(request, AsyncMSAL)
|
|
69
59
|
|
|
70
|
-
if not
|
|
71
|
-
cookies = dict(request.cookies.items())
|
|
72
|
-
msg.append(f"<b>Expecting '{ENV.COOKIE_NAME}' in cookies</b>")
|
|
73
|
-
_LOGGER.fatal("Cookie should be set with Samesite:None")
|
|
74
|
-
msg.append(html_table(cookies))
|
|
75
|
-
|
|
76
|
-
elif not session.get(FLOW_CACHE):
|
|
77
|
-
msg.append(f"<b>Expecting '{FLOW_CACHE}' in session</b>")
|
|
78
|
-
msg.append(f"- Session.new: {session.new}")
|
|
79
|
-
msg.append(html_table(session))
|
|
80
|
-
|
|
81
|
-
aiomsal = AsyncMSAL(session)
|
|
82
|
-
|
|
83
|
-
if not msg:
|
|
60
|
+
if aiomsal and not msg:
|
|
84
61
|
try:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
for lcb in ENV.login_callback:
|
|
103
|
-
await lcb(aiomsal)
|
|
104
|
-
|
|
105
|
-
if msg:
|
|
106
|
-
resp = web.Response(
|
|
107
|
-
body=html_wrap(msg),
|
|
108
|
-
content_type="text/html",
|
|
62
|
+
raise web.HTTPFound(aiomsal.redirect)
|
|
63
|
+
finally:
|
|
64
|
+
aiomsal.redirect = ""
|
|
65
|
+
|
|
66
|
+
resp = web.Response(
|
|
67
|
+
body=html_wrap(msg),
|
|
68
|
+
content_type="text/html",
|
|
69
|
+
)
|
|
70
|
+
if aiomsal is None:
|
|
71
|
+
resp.set_cookie(
|
|
72
|
+
ENV.COOKIE_NAME,
|
|
73
|
+
"",
|
|
74
|
+
path="/",
|
|
75
|
+
httponly=True,
|
|
76
|
+
secure=True,
|
|
77
|
+
samesite="Strict",
|
|
78
|
+
domain=ENV.DOMAIN,
|
|
109
79
|
)
|
|
110
|
-
|
|
111
|
-
resp.set_cookie(
|
|
112
|
-
ENV.COOKIE_NAME,
|
|
113
|
-
"",
|
|
114
|
-
path="/",
|
|
115
|
-
httponly=True,
|
|
116
|
-
secure=True,
|
|
117
|
-
samesite="Strict",
|
|
118
|
-
domain=ENV.DOMAIN,
|
|
119
|
-
)
|
|
120
|
-
return resp
|
|
121
|
-
|
|
122
|
-
redirect = session.pop(SESSION_REDIRECT, "") or "/fr/"
|
|
123
|
-
|
|
124
|
-
return web.HTTPFound(redirect)
|
|
80
|
+
return resp
|
|
125
81
|
|
|
126
82
|
|
|
127
83
|
@ROUTES.get("/user/debug")
|
|
@@ -226,33 +182,3 @@ async def user_photo(request: web.Request, ses: AsyncMSAL) -> web.StreamResponse
|
|
|
226
182
|
|
|
227
183
|
# await response.write_eof()
|
|
228
184
|
return response
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def html_table(items: Mapping[Any, Any]) -> str:
|
|
232
|
-
"""Return a table HTML."""
|
|
233
|
-
res = "<table style='width:80%;border:1px solid black;'>"
|
|
234
|
-
for key, val in items.items():
|
|
235
|
-
res += f"<tr><td>{key}</td><td>{val}</td></tr>"
|
|
236
|
-
res += "</table>"
|
|
237
|
-
return res
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def html_wrap(msgs: Sequence[str]) -> str:
|
|
241
|
-
"""Return proper HTML when login fails."""
|
|
242
|
-
html = "</li><li>".join(msgs)
|
|
243
|
-
return f"""
|
|
244
|
-
<h2>Login failed</h2>
|
|
245
|
-
|
|
246
|
-
<p>Retry at <a href='/user/login'>/user/login</a></p>
|
|
247
|
-
|
|
248
|
-
<p>Try clearing the cookies for <b>.{ENV.DOMAIN}<b> by navigating to the correct
|
|
249
|
-
address for your browser:
|
|
250
|
-
<ul>
|
|
251
|
-
<li>chrome://settings/siteData?searchSubpage={ENV.DOMAIN}</li>
|
|
252
|
-
<li>brave://settings/siteData?searchSubpage={ENV.DOMAIN}</li>
|
|
253
|
-
<li>edge://settings/siteData (you will have to search for {ENV.DOMAIN} cookies)</li>
|
|
254
|
-
</ul></p>
|
|
255
|
-
|
|
256
|
-
<h4>Debug info</h4>
|
|
257
|
-
<ul><li>{html}</li></ul>
|
|
258
|
-
"""
|
aiohttp_msal/utils.py
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from collections.abc import Awaitable, Callable
|
|
5
|
-
from functools import wraps
|
|
6
|
-
from typing import ParamSpec, TypeVar
|
|
7
|
-
from functools import partial
|
|
8
|
-
|
|
5
|
+
from functools import partial, wraps
|
|
6
|
+
from typing import Any, ParamSpec, TypeVar
|
|
9
7
|
|
|
10
8
|
T = TypeVar("T")
|
|
11
9
|
P = ParamSpec("P")
|
|
@@ -31,6 +29,36 @@ def async_wrap(
|
|
|
31
29
|
return run
|
|
32
30
|
|
|
33
31
|
|
|
32
|
+
class dict_property(property):
|
|
33
|
+
"""Property."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, dict_name: str, prop_name: str) -> None:
|
|
36
|
+
"""Initialize the property."""
|
|
37
|
+
self.dict_name = dict_name
|
|
38
|
+
self.prop_name = prop_name
|
|
39
|
+
|
|
40
|
+
def __get__(self, instance: Any, owner: type | None = None, /) -> Any:
|
|
41
|
+
"""Getter."""
|
|
42
|
+
return getattr(instance, self.dict_name, {}).get(self.prop_name, "")
|
|
43
|
+
|
|
44
|
+
def __set__(self, instance: Any, value: Any, /) -> None:
|
|
45
|
+
"""Setter."""
|
|
46
|
+
if value == "":
|
|
47
|
+
getattr(instance, self.dict_name, {}).pop(self.prop_name, None)
|
|
48
|
+
else:
|
|
49
|
+
getattr(instance, self.dict_name, {}).__setitem__(self.prop_name, value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# def dict_property(dict_name: str, prop_name: str) -> property:
|
|
53
|
+
# """Create properties for a dictionary."""
|
|
54
|
+
# return property(
|
|
55
|
+
# fget=lambda self: str(getattr(self, dict_name).get(prop_name, "")),
|
|
56
|
+
# fset=lambda self, v: getattr(self, dict_name).set(prop_name, v),
|
|
57
|
+
# fdel=lambda self: getattr(self, dict_name).pop(prop_name, None),
|
|
58
|
+
# doc=f'self.{dict_name}["{prop_name}"]',
|
|
59
|
+
# )
|
|
60
|
+
|
|
61
|
+
|
|
34
62
|
def retry(func: Callable[P, Awaitable[T]]) -> Callable[P, Awaitable[T]]:
|
|
35
63
|
"""Retry if tenacity is installed."""
|
|
36
64
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
aiohttp_msal/__init__.py,sha256=nrjPAIy0kNbBvHZsy9qUSKs5r_-Kiea-KJM17bsBIkw,4375
|
|
2
|
+
aiohttp_msal/helpers.py,sha256=8sW-7FW4t1Ztazql11ng67SqdKBlbOWuOMjOBRfBc_c,4495
|
|
3
|
+
aiohttp_msal/msal_async.py,sha256=RUV_XlhNNCOZslT1sEkEPyaYcHDOmsIyiGmRpZuA3I0,8097
|
|
4
|
+
aiohttp_msal/redis_tools.py,sha256=6kCw0_zDQcvIcsJaPfG-zHUvT3vzkrNySNTV5y1tckE,6539
|
|
5
|
+
aiohttp_msal/routes.py,sha256=3rAejWcHfL5czVA91TY3wZGT5EkRxmfyvc8vr6rhyxU,5438
|
|
6
|
+
aiohttp_msal/settings.py,sha256=sArlq9vBDMsikLf9sTRw-UXE2_QRK_G-kzmtHvZcbwA,1559
|
|
7
|
+
aiohttp_msal/settings_base.py,sha256=WBI7HS780i9zKWUy1ZnztDbRsfoDMVr3K-otHZOhNCc,3026
|
|
8
|
+
aiohttp_msal/utils.py,sha256=ll303J58nwCEB9QCAm13urUOa6cPqsAE7z_iP9OlRJw,2390
|
|
9
|
+
aiohttp_msal-1.0.4.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
10
|
+
aiohttp_msal-1.0.4.dist-info/METADATA,sha256=OBl_U9OTy602OfSL9-d5myNJPUYuaGjtBF7voW5lu2Y,4514
|
|
11
|
+
aiohttp_msal-1.0.4.dist-info/RECORD,,
|
aiohttp_msal/user_info.py
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"""Graph User Info."""
|
|
2
|
-
|
|
3
|
-
from aiohttp_msal.msal_async import AsyncMSAL
|
|
4
|
-
from aiohttp_msal.utils import retry
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@retry
|
|
8
|
-
async def get_user_info(aiomsal: AsyncMSAL) -> None:
|
|
9
|
-
"""Load user info from MS graph API. Requires User.Read permissions."""
|
|
10
|
-
async with aiomsal.get("https://graph.microsoft.com/v1.0/me") as res:
|
|
11
|
-
body = await res.json()
|
|
12
|
-
try:
|
|
13
|
-
aiomsal.session["mail"] = body["mail"]
|
|
14
|
-
aiomsal.session["name"] = body["displayName"]
|
|
15
|
-
except KeyError as err:
|
|
16
|
-
raise KeyError(
|
|
17
|
-
f"Unexpected return from Graph endpoint: {body}: {err}"
|
|
18
|
-
) from err
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@retry
|
|
22
|
-
async def get_manager_info(aiomsal: AsyncMSAL) -> None:
|
|
23
|
-
"""Load manager info from MS graph API. Requires User.Read.All permissions."""
|
|
24
|
-
async with aiomsal.get("https://graph.microsoft.com/v1.0/me/manager") as res:
|
|
25
|
-
body = await res.json()
|
|
26
|
-
try:
|
|
27
|
-
aiomsal.session["m_mail"] = body["mail"]
|
|
28
|
-
aiomsal.session["m_name"] = body["displayName"]
|
|
29
|
-
except KeyError as err:
|
|
30
|
-
raise KeyError(
|
|
31
|
-
f"Unexpected return from Graph endpoint: {body}: {err}"
|
|
32
|
-
) from err
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
aiohttp_msal/__init__.py,sha256=hnyifyJykI7NMvM93KrHIsTlrrfCVUrpKdbRKL6Gubw,4027
|
|
2
|
-
aiohttp_msal/msal_async.py,sha256=urQvaMTi0mJnCboCsj8A9F9VpWcjPOYaechpZ5XrnbY,8153
|
|
3
|
-
aiohttp_msal/redis_tools.py,sha256=6kCw0_zDQcvIcsJaPfG-zHUvT3vzkrNySNTV5y1tckE,6539
|
|
4
|
-
aiohttp_msal/routes.py,sha256=WyLBuoPMkkG6Cx4gFUu_ER71FyJbeXKhOQRQu5ALG2M,8138
|
|
5
|
-
aiohttp_msal/settings.py,sha256=sArlq9vBDMsikLf9sTRw-UXE2_QRK_G-kzmtHvZcbwA,1559
|
|
6
|
-
aiohttp_msal/settings_base.py,sha256=WBI7HS780i9zKWUy1ZnztDbRsfoDMVr3K-otHZOhNCc,3026
|
|
7
|
-
aiohttp_msal/user_info.py,sha256=lxjFxjm16rvC-0LS81y7SG5pCOa5Zl0s62uxi97yu_k,1171
|
|
8
|
-
aiohttp_msal/utils.py,sha256=SgGpE1eFdVh48FaKvtbnQqJKTReXa9OPBKiYGY7SYq8,1303
|
|
9
|
-
aiohttp_msal-1.0.3.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
|
|
10
|
-
aiohttp_msal-1.0.3.dist-info/METADATA,sha256=raujaCawOODO6HYM79crhTECU28f_IjlkzIPmR8ck48,4514
|
|
11
|
-
aiohttp_msal-1.0.3.dist-info/RECORD,,
|
|
File without changes
|