skrift 0.1.0a10__tar.gz → 0.1.0a11__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.
- {skrift-0.1.0a10 → skrift-0.1.0a11}/PKG-INFO +1 -1
- {skrift-0.1.0a10 → skrift-0.1.0a11}/pyproject.toml +1 -1
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/config.py +1 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/controllers/auth.py +68 -4
- {skrift-0.1.0a10 → skrift-0.1.0a11}/.gitignore +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/README.md +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/__init__.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/__main__.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/admin/__init__.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/admin/controller.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/admin/navigation.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/env.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/script.py.mako +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260120_210154_09b0364dbb7b_initial_schema.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260122_152744_0b7c927d2591_add_roles_and_permissions.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260122_172836_cdf734a5b847_add_sa_orm_sentinel_column.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260122_175637_a9c55348eae7_remove_page_type_column.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260122_200000_add_settings_table.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260129_add_oauth_accounts.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260129_add_provider_metadata.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic.ini +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/asgi.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/auth/__init__.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/auth/guards.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/auth/roles.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/auth/services.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/cli.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/controllers/__init__.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/controllers/web.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/__init__.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/base.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/__init__.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/oauth_account.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/page.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/role.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/setting.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/models/user.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/services/__init__.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/services/oauth_service.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/services/page_service.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/db/services/setting_service.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/lib/__init__.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/lib/exceptions.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/lib/template.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/__init__.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/config_writer.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/controller.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/middleware.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/providers.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/setup/state.py +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/static/css/style.css +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/admin.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/base.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/pages/edit.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/pages/list.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/settings/site.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/users/list.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/admin/users/roles.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/auth/dummy_login.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/auth/login.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/base.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/error-404.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/error-500.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/error.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/index.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/page.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/admin.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/auth.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/base.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/complete.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/configuring.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/database.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/restart.html +0 -0
- {skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/templates/setup/site.html +0 -0
|
@@ -5,11 +5,12 @@ Also supports a development-only "dummy" provider for testing.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import base64
|
|
8
|
+
import fnmatch
|
|
8
9
|
import hashlib
|
|
9
10
|
import secrets
|
|
10
11
|
from datetime import UTC, datetime
|
|
11
12
|
from typing import Annotated
|
|
12
|
-
from urllib.parse import urlencode
|
|
13
|
+
from urllib.parse import urlencode, urlparse
|
|
13
14
|
|
|
14
15
|
import httpx
|
|
15
16
|
from litestar import Controller, Request, get, post
|
|
@@ -26,6 +27,56 @@ from skrift.db.models.user import User
|
|
|
26
27
|
from skrift.setup.providers import DUMMY_PROVIDER_KEY, OAUTH_PROVIDERS, get_provider_info
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
def _is_safe_redirect_url(url: str, allowed_domains: list[str]) -> bool:
|
|
31
|
+
"""Check if URL is safe to redirect to.
|
|
32
|
+
|
|
33
|
+
Supports wildcard patterns using fnmatch-style matching:
|
|
34
|
+
- "*.example.com" matches any subdomain of example.com
|
|
35
|
+
- "app-*.example.com" matches app-foo.example.com, app-bar.example.com, etc.
|
|
36
|
+
- "example.com" (no wildcards) matches example.com and all subdomains
|
|
37
|
+
"""
|
|
38
|
+
# Relative paths are always safe (but not protocol-relative //domain.com)
|
|
39
|
+
if url.startswith("/") and not url.startswith("//"):
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
# Parse absolute URL
|
|
43
|
+
try:
|
|
44
|
+
parsed = urlparse(url)
|
|
45
|
+
except Exception:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
# Must have scheme and netloc
|
|
49
|
+
if not parsed.scheme or not parsed.netloc:
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
# Only allow http/https
|
|
53
|
+
if parsed.scheme not in ("http", "https"):
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
# Check if domain matches allowed list
|
|
57
|
+
host = parsed.netloc.lower().split(":")[0] # Remove port
|
|
58
|
+
for pattern in allowed_domains:
|
|
59
|
+
pattern = pattern.lower()
|
|
60
|
+
# If pattern contains wildcards, use fnmatch
|
|
61
|
+
if "*" in pattern or "?" in pattern:
|
|
62
|
+
if fnmatch.fnmatch(host, pattern):
|
|
63
|
+
return True
|
|
64
|
+
else:
|
|
65
|
+
# No wildcards: exact match or subdomain match
|
|
66
|
+
if host == pattern or host.endswith(f".{pattern}"):
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_safe_redirect_url(request: Request, allowed_domains: list[str], default: str = "/") -> str:
|
|
73
|
+
"""Get the next redirect URL from session, validating it's safe."""
|
|
74
|
+
next_url = request.session.pop("auth_next", None)
|
|
75
|
+
if next_url and _is_safe_redirect_url(next_url, allowed_domains):
|
|
76
|
+
return next_url
|
|
77
|
+
return default
|
|
78
|
+
|
|
79
|
+
|
|
29
80
|
def get_auth_url(provider: str, settings, state: str, code_challenge: str | None = None) -> str:
|
|
30
81
|
"""Build the OAuth authorization URL for a provider."""
|
|
31
82
|
provider_info = get_provider_info(provider)
|
|
@@ -230,11 +281,16 @@ class AuthController(Controller):
|
|
|
230
281
|
self,
|
|
231
282
|
request: Request,
|
|
232
283
|
provider: str,
|
|
284
|
+
next_url: Annotated[str | None, Parameter(query="next")] = None,
|
|
233
285
|
) -> Redirect | TemplateResponse:
|
|
234
286
|
"""Redirect to OAuth provider consent screen, or show dummy login form."""
|
|
235
287
|
settings = get_settings()
|
|
236
288
|
provider_info = get_provider_info(provider)
|
|
237
289
|
|
|
290
|
+
# Store next URL in session if provided and valid
|
|
291
|
+
if next_url and _is_safe_redirect_url(next_url, settings.auth.allowed_redirect_domains):
|
|
292
|
+
request.session["auth_next"] = next_url
|
|
293
|
+
|
|
238
294
|
if not provider_info:
|
|
239
295
|
raise NotFoundException(f"Unknown provider: {provider}")
|
|
240
296
|
|
|
@@ -392,14 +448,22 @@ class AuthController(Controller):
|
|
|
392
448
|
request.session["user_picture_url"] = user.picture_url
|
|
393
449
|
request.session["flash"] = "Successfully logged in!"
|
|
394
450
|
|
|
395
|
-
return Redirect(path=
|
|
451
|
+
return Redirect(path=_get_safe_redirect_url(request, settings.auth.allowed_redirect_domains))
|
|
396
452
|
|
|
397
453
|
@get("/login")
|
|
398
|
-
async def login_page(
|
|
454
|
+
async def login_page(
|
|
455
|
+
self,
|
|
456
|
+
request: Request,
|
|
457
|
+
next_url: Annotated[str | None, Parameter(query="next")] = None,
|
|
458
|
+
) -> TemplateResponse:
|
|
399
459
|
"""Show login page with available providers."""
|
|
400
460
|
flash = request.session.pop("flash", None)
|
|
401
461
|
settings = get_settings()
|
|
402
462
|
|
|
463
|
+
# Store next URL in session if provided and valid
|
|
464
|
+
if next_url and _is_safe_redirect_url(next_url, settings.auth.allowed_redirect_domains):
|
|
465
|
+
request.session["auth_next"] = next_url
|
|
466
|
+
|
|
403
467
|
# Get configured providers (excluding dummy from main list)
|
|
404
468
|
configured_providers = list(settings.auth.providers.keys())
|
|
405
469
|
providers = {
|
|
@@ -522,7 +586,7 @@ class AuthController(Controller):
|
|
|
522
586
|
request.session["user_picture_url"] = user.picture_url
|
|
523
587
|
request.session["flash"] = "Successfully logged in!"
|
|
524
588
|
|
|
525
|
-
return Redirect(path=
|
|
589
|
+
return Redirect(path=_get_safe_redirect_url(request, settings.auth.allowed_redirect_domains))
|
|
526
590
|
|
|
527
591
|
@get("/logout")
|
|
528
592
|
async def logout(self, request: Request) -> Redirect:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260122_200000_add_settings_table.py
RENAMED
|
File without changes
|
|
File without changes
|
{skrift-0.1.0a10 → skrift-0.1.0a11}/skrift/alembic/versions/20260129_add_provider_metadata.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|