qodalis-cli-server-plugin-users 1.0.0b6__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.
- qodalis_cli_server_plugin_users-1.0.0b6.dist-info/METADATA +21 -0
- qodalis_cli_server_plugin_users-1.0.0b6.dist-info/RECORD +32 -0
- qodalis_cli_server_plugin_users-1.0.0b6.dist-info/WHEEL +4 -0
- qodalis_cli_server_plugin_users-1.0.0b6.dist-info/licenses/LICENSE +21 -0
- qodalis_cli_users/__init__.py +134 -0
- qodalis_cli_users/_http.py +162 -0
- qodalis_cli_users/cli_users_builder.py +385 -0
- qodalis_cli_users/controllers/__init__.py +13 -0
- qodalis_cli_users/controllers/_routing.py +25 -0
- qodalis_cli_users/controllers/audit_controller.py +40 -0
- qodalis_cli_users/controllers/groups_controller.py +109 -0
- qodalis_cli_users/controllers/sessions_controller.py +114 -0
- qodalis_cli_users/controllers/users_controller.py +183 -0
- qodalis_cli_users/errors.py +118 -0
- qodalis_cli_users/middleware/__init__.py +5 -0
- qodalis_cli_users/middleware/auth_middleware.py +77 -0
- qodalis_cli_users/models.py +282 -0
- qodalis_cli_users/permissions.py +58 -0
- qodalis_cli_users/server_protection.py +209 -0
- qodalis_cli_users/services/__init__.py +6 -0
- qodalis_cli_users/services/auth_service.py +786 -0
- qodalis_cli_users/services/password_hasher.py +98 -0
- qodalis_cli_users/services/users_service.py +330 -0
- qodalis_cli_users/shell_access.py +125 -0
- qodalis_cli_users/stores/__init__.py +27 -0
- qodalis_cli_users/stores/in_memory_audit_store.py +50 -0
- qodalis_cli_users/stores/in_memory_credential_store.py +25 -0
- qodalis_cli_users/stores/in_memory_group_store.py +65 -0
- qodalis_cli_users/stores/in_memory_session_store.py +71 -0
- qodalis_cli_users/stores/in_memory_user_store.py +102 -0
- qodalis_cli_users/stores/interfaces.py +208 -0
- qodalis_cli_users/validation.py +100 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: qodalis-cli-server-plugin-users
|
|
3
|
+
Version: 1.0.0b6
|
|
4
|
+
Summary: User management plugin for Qodalis CLI servers — authentication, sessions, groups, and audit log
|
|
5
|
+
Project-URL: Homepage, https://github.com/qodalis-solutions/cli-server-python
|
|
6
|
+
Project-URL: Repository, https://github.com/qodalis-solutions/cli-server-python
|
|
7
|
+
Project-URL: Issues, https://github.com/qodalis-solutions/cli-server-python/issues
|
|
8
|
+
Author-email: Nicolae Lupei <nicolae.lupei@qodalis.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: auth,cli,plugin,qodalis,sessions,users
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: fastapi>=0.100.0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
qodalis_cli_users/__init__.py,sha256=F9xQGr9JU9Yr7r677JgWtuot8mxa9fNPXu2WPy7Y5Qw,3986
|
|
2
|
+
qodalis_cli_users/_http.py,sha256=RKzT8kVz6aK8XNHLWdZ8FBDaLL_lmHgNYepPyNysaFY,5675
|
|
3
|
+
qodalis_cli_users/cli_users_builder.py,sha256=otpys4dBh4XukRH-GVjfpVsxVuAaFeDhDtkZpZW29lw,16027
|
|
4
|
+
qodalis_cli_users/errors.py,sha256=pdYuoSwjCA606f0qRFYUjbr8yvcBJaG78Q24QthiG8c,4292
|
|
5
|
+
qodalis_cli_users/models.py,sha256=Q2CTwnBEGxL3jKQhU7FxRULCLwuFcpyjlB5qbxqU4Pc,8598
|
|
6
|
+
qodalis_cli_users/permissions.py,sha256=_yUCrg5IAooNTEyF68CLcYOvuQK15eYN0Cd3p6yVDxM,1844
|
|
7
|
+
qodalis_cli_users/server_protection.py,sha256=ETFw_Wict2zE0931OdHjn2iiQzti6s6TpID4G_SafDs,8547
|
|
8
|
+
qodalis_cli_users/shell_access.py,sha256=8G03Ytmy1wWadgjebt0yYqQDqhtLg8LsuVg_qco2c5I,5277
|
|
9
|
+
qodalis_cli_users/validation.py,sha256=CMSpDlmGT6BDVkOGh42qYe0DvgsMQSAyTsntfoDSoV4,3371
|
|
10
|
+
qodalis_cli_users/controllers/__init__.py,sha256=uc06EpxRVwjBZ8LjiQHYePviyDRasrcJFJGLpb0mJc0,368
|
|
11
|
+
qodalis_cli_users/controllers/_routing.py,sha256=CEGOD7y9lrBvBWWydOnqtTDQG8zXCpmS7SBmhKUASPU,781
|
|
12
|
+
qodalis_cli_users/controllers/audit_controller.py,sha256=HxankpCkMsWgpW8uei_I3jZLENoKYURTwp6sx2uf5ps,1356
|
|
13
|
+
qodalis_cli_users/controllers/groups_controller.py,sha256=4JtqUBl1WDRAKL_yhbzgboFIyOXl_Amsv6v07shqqNM,4012
|
|
14
|
+
qodalis_cli_users/controllers/sessions_controller.py,sha256=7t3cTD-nsHPfC2Ux5DrAslfU0l1yo9bGp_KiFUXMMUc,4745
|
|
15
|
+
qodalis_cli_users/controllers/users_controller.py,sha256=qLChQzpAGdIBeJ3HhOcA1HyMxYOsm3UyMQ5onlJmt2I,6937
|
|
16
|
+
qodalis_cli_users/middleware/__init__.py,sha256=LmdPf8wm9S5yoBPTzls0d4UIxboUo0BqfiCVH4k72Dg,172
|
|
17
|
+
qodalis_cli_users/middleware/auth_middleware.py,sha256=78cu9PstBpTyGlH5HqO_TPLj2e-HAG0lH5u3OuOPLqo,3123
|
|
18
|
+
qodalis_cli_users/services/__init__.py,sha256=Y7SU5E8XOgpVkaQ7E0cxylhgPRtpQuiXak81L9Ua1fA,249
|
|
19
|
+
qodalis_cli_users/services/auth_service.py,sha256=_nzc19v4_GiAjZ4Zrp6l1dVYOTJ25Y_asg5mXzZixmk,30767
|
|
20
|
+
qodalis_cli_users/services/password_hasher.py,sha256=90813Z2Nk2N_PK0Kggpl1e5MMFZGZJIYrs51FbbeToA,3670
|
|
21
|
+
qodalis_cli_users/services/users_service.py,sha256=9C2-FQ3CdjktO9Q04vq3r9PLH7EMzwSLXNpf_8__iFM,13459
|
|
22
|
+
qodalis_cli_users/stores/__init__.py,sha256=T0qCMrETLtvP77B7mMqYLJ7_aOpdloMmTVQJOQAvzwU,696
|
|
23
|
+
qodalis_cli_users/stores/in_memory_audit_store.py,sha256=ku17nYZo0bMlY07UL-P7pXRCRn2Yv03FYl4tkQJGo9U,1824
|
|
24
|
+
qodalis_cli_users/stores/in_memory_credential_store.py,sha256=TT9X5wi5LV9cnANugLH89ov9KMr5x4JbEsOPl7D86kM,797
|
|
25
|
+
qodalis_cli_users/stores/in_memory_group_store.py,sha256=4BwlncJ3M2MHiv-1TchuRk9kQZl3aeOlyGc-LD4mlQ0,1919
|
|
26
|
+
qodalis_cli_users/stores/in_memory_session_store.py,sha256=0pT49BTfdpLIGdUs5un5y4zjKS5CnHsbPL7_drdxJ-k,2384
|
|
27
|
+
qodalis_cli_users/stores/in_memory_user_store.py,sha256=SfBQKhya0K19HiG2ZsvfYtXgahdAaLtvK5POWI4KxEE,3162
|
|
28
|
+
qodalis_cli_users/stores/interfaces.py,sha256=_AHmgMop6LP1Qf97e_Lz4TXSFDbY-LlEsVWJN199U7Q,5312
|
|
29
|
+
qodalis_cli_server_plugin_users-1.0.0b6.dist-info/METADATA,sha256=fbv_6isoXRjhG9Yso72pz8ZgWZK_z8f3NSv5Lvvx6n8,1023
|
|
30
|
+
qodalis_cli_server_plugin_users-1.0.0b6.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
31
|
+
qodalis_cli_server_plugin_users-1.0.0b6.dist-info/licenses/LICENSE,sha256=uagko_O2JboeVOW3cldvxKW4QIDjTHCCk5T32qqg_As,1079
|
|
32
|
+
qodalis_cli_server_plugin_users-1.0.0b6.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Qodalis Solutions
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Qodalis CLI users plugin — user management, authentication, permissions,
|
|
2
|
+
sessions, groups and audit."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import warnings
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Callable, Optional
|
|
9
|
+
|
|
10
|
+
from .cli_users_builder import CliUsersBuilder, CliUsersPlugin
|
|
11
|
+
from .errors import UsersApiError
|
|
12
|
+
from .models import (
|
|
13
|
+
AuditEntry,
|
|
14
|
+
AuditQueryOptions,
|
|
15
|
+
Credentials,
|
|
16
|
+
ExternalAuthAdapter,
|
|
17
|
+
ExternalAuthResult,
|
|
18
|
+
Group,
|
|
19
|
+
Session,
|
|
20
|
+
SessionMetadata,
|
|
21
|
+
User,
|
|
22
|
+
)
|
|
23
|
+
from .permissions import has_permission, merge_permissions, permission_matches
|
|
24
|
+
from .services.auth_service import (
|
|
25
|
+
DEFAULT_SESSION_TIMEOUT_MS,
|
|
26
|
+
AuthContext,
|
|
27
|
+
AuthService,
|
|
28
|
+
AuthServiceOptions,
|
|
29
|
+
)
|
|
30
|
+
from .services.users_service import UsersService
|
|
31
|
+
from .server_protection import ServerProtectionMiddleware
|
|
32
|
+
from .shell_access import UsersShellAccess
|
|
33
|
+
from .stores.in_memory_audit_store import InMemoryAuditStore
|
|
34
|
+
from .stores.in_memory_credential_store import InMemoryCredentialStore
|
|
35
|
+
from .stores.in_memory_group_store import InMemoryGroupStore
|
|
36
|
+
from .stores.in_memory_session_store import InMemorySessionStore
|
|
37
|
+
from .stores.in_memory_user_store import InMemoryUserStore
|
|
38
|
+
from .stores.interfaces import (
|
|
39
|
+
IAuditStore,
|
|
40
|
+
ICredentialStore,
|
|
41
|
+
IGroupStore,
|
|
42
|
+
ISessionStore,
|
|
43
|
+
IUserStore,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"AuditEntry",
|
|
48
|
+
"AuditQueryOptions",
|
|
49
|
+
"AuthContext",
|
|
50
|
+
"AuthService",
|
|
51
|
+
"AuthServiceOptions",
|
|
52
|
+
"CliUsersBuilder",
|
|
53
|
+
"CliUsersPlugin",
|
|
54
|
+
"Credentials",
|
|
55
|
+
"ExternalAuthAdapter",
|
|
56
|
+
"ExternalAuthResult",
|
|
57
|
+
"Group",
|
|
58
|
+
"IAuditStore",
|
|
59
|
+
"ICredentialStore",
|
|
60
|
+
"IGroupStore",
|
|
61
|
+
"ISessionStore",
|
|
62
|
+
"IUserStore",
|
|
63
|
+
"InMemoryAuditStore",
|
|
64
|
+
"InMemoryCredentialStore",
|
|
65
|
+
"InMemoryGroupStore",
|
|
66
|
+
"InMemorySessionStore",
|
|
67
|
+
"InMemoryUserStore",
|
|
68
|
+
"Session",
|
|
69
|
+
"ServerProtectionMiddleware",
|
|
70
|
+
"SessionMetadata",
|
|
71
|
+
"User",
|
|
72
|
+
"UsersApiError",
|
|
73
|
+
"UsersPluginOptions",
|
|
74
|
+
"UsersPluginResult",
|
|
75
|
+
"UsersService",
|
|
76
|
+
"UsersShellAccess",
|
|
77
|
+
"build_users_plugin",
|
|
78
|
+
"has_permission",
|
|
79
|
+
"merge_permissions",
|
|
80
|
+
"permission_matches",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
#: Deprecated alias of :class:`CliUsersPlugin`.
|
|
84
|
+
UsersPluginResult = CliUsersPlugin
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class UsersPluginOptions:
|
|
89
|
+
"""Deprecated options for :func:`build_users_plugin`; use
|
|
90
|
+
:class:`CliUsersBuilder`."""
|
|
91
|
+
|
|
92
|
+
user_store: IUserStore | None = None
|
|
93
|
+
session_store: ISessionStore | None = None
|
|
94
|
+
credential_store: ICredentialStore | None = None
|
|
95
|
+
audit_store: IAuditStore | None = None
|
|
96
|
+
group_store: IGroupStore | None = None
|
|
97
|
+
session_timeout_ms: int = DEFAULT_SESSION_TIMEOUT_MS
|
|
98
|
+
default_admin_password: str | None = None
|
|
99
|
+
auth_adapter: Optional[ExternalAuthAdapter] = None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def build_users_plugin(
|
|
103
|
+
options: UsersPluginOptions | None = None,
|
|
104
|
+
broadcast_event: Callable[[str], Any] | None = None,
|
|
105
|
+
) -> CliUsersPlugin:
|
|
106
|
+
"""Deprecated: use :class:`CliUsersBuilder` and call ``startup()`` from
|
|
107
|
+
the app lifespan. Builds the plugin and runs ``startup()`` on the
|
|
108
|
+
current loop."""
|
|
109
|
+
warnings.warn(
|
|
110
|
+
"build_users_plugin() is deprecated; use CliUsersBuilder().build() and "
|
|
111
|
+
"await plugin.startup() in the app lifespan",
|
|
112
|
+
DeprecationWarning,
|
|
113
|
+
stacklevel=2,
|
|
114
|
+
)
|
|
115
|
+
opts = options or UsersPluginOptions()
|
|
116
|
+
builder = (
|
|
117
|
+
CliUsersBuilder()
|
|
118
|
+
.set_default_admin_password(opts.default_admin_password)
|
|
119
|
+
.set_session_timeout(opts.session_timeout_ms)
|
|
120
|
+
.set_auth_adapter(opts.auth_adapter)
|
|
121
|
+
)
|
|
122
|
+
if opts.user_store:
|
|
123
|
+
builder.set_user_store(opts.user_store)
|
|
124
|
+
if opts.session_store:
|
|
125
|
+
builder.set_session_store(opts.session_store)
|
|
126
|
+
if opts.credential_store:
|
|
127
|
+
builder.set_credential_store(opts.credential_store)
|
|
128
|
+
if opts.audit_store:
|
|
129
|
+
builder.set_audit_store(opts.audit_store)
|
|
130
|
+
if opts.group_store:
|
|
131
|
+
builder.set_group_store(opts.group_store)
|
|
132
|
+
plugin = builder.build(broadcast_event)
|
|
133
|
+
await plugin.startup()
|
|
134
|
+
return plugin
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Small HTTP/ASGI helpers (kept local so the plugin does not depend on the
|
|
2
|
+
core server package)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import fnmatch
|
|
7
|
+
from typing import Any, Iterable
|
|
8
|
+
from urllib.parse import urlsplit
|
|
9
|
+
|
|
10
|
+
SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})
|
|
11
|
+
#: Key under ``scope["state"]`` where the authenticated caller is stored
|
|
12
|
+
#: (shared with ``qodalis_cli.security``).
|
|
13
|
+
AUTH_STATE_KEY = "qcli_auth"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_DEFAULT_PORTS = {"http": 80, "https": 443}
|
|
17
|
+
_WS_SCHEMES = {"ws": "http", "wss": "https"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _scope_header(scope: Any, name: str) -> str | None:
|
|
21
|
+
raw = name.encode("latin-1")
|
|
22
|
+
for key, value in (scope.get("headers") if isinstance(scope, dict) else None) or []:
|
|
23
|
+
if key.lower() == raw:
|
|
24
|
+
return value.decode("latin-1")
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def trusted_hops(trust_proxy: bool | int | None) -> int:
|
|
29
|
+
"""Number of trusted reverse-proxy hops for a ``trust_proxy`` setting.
|
|
30
|
+
|
|
31
|
+
``False``/``0``/``None`` trust no proxy, ``True`` one hop, an int ``n``
|
|
32
|
+
``n`` hops; any other enabled value below 1 (a negative int) counts as
|
|
33
|
+
one hop. ``bool`` is checked first (``True == 1`` in Python).
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(trust_proxy, bool):
|
|
36
|
+
return 1 if trust_proxy else 0
|
|
37
|
+
if not trust_proxy:
|
|
38
|
+
return 0
|
|
39
|
+
return max(int(trust_proxy), 1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def forwarded_client_ip(headers: Any, trust_proxy: bool | int | None) -> str | None:
|
|
43
|
+
"""Client IP from ``X-Forwarded-For`` behind ``trust_proxy`` trusted hops.
|
|
44
|
+
|
|
45
|
+
Every proxy appends the address it saw, so only the rightmost ``n``
|
|
46
|
+
entries were written by trusted proxies; the ones to their left are
|
|
47
|
+
client-controlled. The client IP is the entry at ``len - n`` of the
|
|
48
|
+
comma-separated, trimmed list (the rightmost entry for one hop), or the
|
|
49
|
+
leftmost entry when the list has fewer than ``n`` entries. Returns
|
|
50
|
+
``None`` (use the socket peer) when no proxy is trusted, the header is
|
|
51
|
+
absent/empty, or the selected entry is empty.
|
|
52
|
+
"""
|
|
53
|
+
hops = trusted_hops(trust_proxy)
|
|
54
|
+
if hops == 0:
|
|
55
|
+
return None
|
|
56
|
+
entries = [
|
|
57
|
+
entry.strip()
|
|
58
|
+
for value in headers.getlist("x-forwarded-for")
|
|
59
|
+
for entry in value.split(",")
|
|
60
|
+
]
|
|
61
|
+
if not entries:
|
|
62
|
+
return None
|
|
63
|
+
return entries[max(len(entries) - hops, 0)] or None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _normalize_origin(scheme: str, netloc: str) -> tuple[str, str, int] | None:
|
|
67
|
+
scheme = _WS_SCHEMES.get(scheme.strip().lower(), scheme.strip().lower())
|
|
68
|
+
if scheme not in _DEFAULT_PORTS or not netloc:
|
|
69
|
+
return None
|
|
70
|
+
try:
|
|
71
|
+
parts = urlsplit(f"//{netloc.strip()}")
|
|
72
|
+
host = (parts.hostname or "").lower()
|
|
73
|
+
port = parts.port
|
|
74
|
+
except ValueError:
|
|
75
|
+
return None
|
|
76
|
+
if not host:
|
|
77
|
+
return None
|
|
78
|
+
return scheme, host, port if port is not None else _DEFAULT_PORTS[scheme]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_same_origin(origin: str | None, scope: Any, trust_proxy: bool | int = False) -> bool:
|
|
82
|
+
"""True when ``origin`` equals the request's own origin (scheme + Host).
|
|
83
|
+
|
|
84
|
+
``ws``/``wss`` map to ``http``/``https``; default ports are normalized and
|
|
85
|
+
the comparison is case-insensitive. ``X-Forwarded-Proto`` /
|
|
86
|
+
``X-Forwarded-Host`` are honoured only when ``trust_proxy`` is on. A
|
|
87
|
+
same-origin request is never cross-site, so it always passes Origin checks.
|
|
88
|
+
"""
|
|
89
|
+
if not origin or not isinstance(scope, dict):
|
|
90
|
+
return False
|
|
91
|
+
try:
|
|
92
|
+
o = urlsplit(origin.strip().rstrip("/"))
|
|
93
|
+
except ValueError:
|
|
94
|
+
return False
|
|
95
|
+
if o.path or o.query or o.fragment:
|
|
96
|
+
return False
|
|
97
|
+
actual = _normalize_origin(o.scheme, o.netloc)
|
|
98
|
+
if actual is None:
|
|
99
|
+
return False
|
|
100
|
+
scheme = scope.get("scheme") or "http"
|
|
101
|
+
host = _scope_header(scope, "host")
|
|
102
|
+
if trust_proxy:
|
|
103
|
+
fwd_proto = _scope_header(scope, "x-forwarded-proto")
|
|
104
|
+
fwd_host = _scope_header(scope, "x-forwarded-host")
|
|
105
|
+
if fwd_proto and fwd_proto.split(",")[0].strip():
|
|
106
|
+
scheme = fwd_proto.split(",")[0].strip()
|
|
107
|
+
if fwd_host and fwd_host.split(",")[0].strip():
|
|
108
|
+
host = fwd_host.split(",")[0].strip()
|
|
109
|
+
if not host:
|
|
110
|
+
return False
|
|
111
|
+
return actual == _normalize_origin(scheme, host)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def origin_allowed(
|
|
115
|
+
origin: str | None,
|
|
116
|
+
allowed: Iterable[str] | None,
|
|
117
|
+
scope: Any = None,
|
|
118
|
+
trust_proxy: bool | int = False,
|
|
119
|
+
) -> bool:
|
|
120
|
+
"""True when ``origin`` matches ``allowed`` (``*`` globs) or, when the
|
|
121
|
+
request ``scope`` is given, equals the request's own origin."""
|
|
122
|
+
if not origin:
|
|
123
|
+
return False
|
|
124
|
+
if scope is not None and is_same_origin(origin, scope, trust_proxy):
|
|
125
|
+
return True
|
|
126
|
+
origin = origin.rstrip("/").lower()
|
|
127
|
+
for pattern in allowed or []:
|
|
128
|
+
pattern = pattern.rstrip("/").lower()
|
|
129
|
+
if pattern == "*" or pattern == origin or fnmatch.fnmatchcase(origin, pattern):
|
|
130
|
+
return True
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def has_bearer(headers: Any) -> bool:
|
|
135
|
+
auth = headers.get("authorization", "") or ""
|
|
136
|
+
return auth[:7].lower() == "bearer " and bool(auth[7:].strip())
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def bearer_token(headers: Any) -> str | None:
|
|
140
|
+
auth = headers.get("authorization", "") or ""
|
|
141
|
+
if auth[:7].lower() != "bearer ":
|
|
142
|
+
return None
|
|
143
|
+
token = auth[7:].strip()
|
|
144
|
+
return token or None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_scope_auth(scope: dict) -> Any:
|
|
148
|
+
state = scope.get("state")
|
|
149
|
+
if isinstance(state, dict):
|
|
150
|
+
return state.get(AUTH_STATE_KEY)
|
|
151
|
+
return getattr(state, AUTH_STATE_KEY, None) if state is not None else None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def set_scope_auth(scope: dict, ctx: Any) -> None:
|
|
155
|
+
state = scope.get("state")
|
|
156
|
+
if state is None:
|
|
157
|
+
state = {}
|
|
158
|
+
scope["state"] = state
|
|
159
|
+
if isinstance(state, dict):
|
|
160
|
+
state[AUTH_STATE_KEY] = ctx
|
|
161
|
+
else:
|
|
162
|
+
setattr(state, AUTH_STATE_KEY, ctx)
|