bazimya 0.2.0__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.
- bazimya/__init__.py +146 -0
- bazimya/__main__.py +8 -0
- bazimya/auth/__init__.py +12 -0
- bazimya/auth/guard.py +251 -0
- bazimya/auth/middleware.py +76 -0
- bazimya/cache/__init__.py +5 -0
- bazimya/cache/repository.py +206 -0
- bazimya/console/__init__.py +7 -0
- bazimya/console/command.py +164 -0
- bazimya/console/commands/__init__.py +1 -0
- bazimya/console/commands/build.py +60 -0
- bazimya/console/commands/doctor.py +311 -0
- bazimya/console/commands/inspect.py +302 -0
- bazimya/console/commands/make.py +451 -0
- bazimya/console/commands/migrate.py +210 -0
- bazimya/console/commands/new.py +176 -0
- bazimya/console/commands/serve.py +62 -0
- bazimya/console/kernel.py +408 -0
- bazimya/console/output.py +143 -0
- bazimya/console/registry.py +44 -0
- bazimya/database/__init__.py +22 -0
- bazimya/database/connection.py +328 -0
- bazimya/database/migration.py +287 -0
- bazimya/database/model.py +512 -0
- bazimya/database/query.py +497 -0
- bazimya/database/schema.py +650 -0
- bazimya/deployment/__init__.py +5 -0
- bazimya/deployment/bundler.py +751 -0
- bazimya/events/__init__.py +5 -0
- bazimya/events/dispatcher.py +144 -0
- bazimya/extensions/__init__.py +12 -0
- bazimya/extensions/extension.py +198 -0
- bazimya/extensions/manager.py +234 -0
- bazimya/facades.py +267 -0
- bazimya/filesystem/__init__.py +5 -0
- bazimya/filesystem/storage.py +211 -0
- bazimya/foundation/__init__.py +13 -0
- bazimya/foundation/application.py +613 -0
- bazimya/foundation/container.py +121 -0
- bazimya/foundation/provider.py +68 -0
- bazimya/hashing.py +190 -0
- bazimya/http/__init__.py +58 -0
- bazimya/http/controller.py +84 -0
- bazimya/http/exceptions.py +100 -0
- bazimya/http/form_request.py +90 -0
- bazimya/http/middleware.py +94 -0
- bazimya/http/request.py +435 -0
- bazimya/http/response.py +232 -0
- bazimya/http/route.py +204 -0
- bazimya/http/router.py +433 -0
- bazimya/mail/__init__.py +5 -0
- bazimya/mail/mailer.py +291 -0
- bazimya/notifications/__init__.py +5 -0
- bazimya/notifications/notification.py +177 -0
- bazimya/serving.py +251 -0
- bazimya/session/__init__.py +13 -0
- bazimya/session/middleware.py +222 -0
- bazimya/session/store.py +353 -0
- bazimya/stubs/app/.editorconfig.stub +15 -0
- bazimya/stubs/app/.env.example.stub +40 -0
- bazimya/stubs/app/.gitattributes.stub +10 -0
- bazimya/stubs/app/.gitignore.stub +19 -0
- bazimya/stubs/app/app/Console/Commands/GreetCommand.py.stub +25 -0
- bazimya/stubs/app/app/Console/Commands/__init__.py.stub +24 -0
- bazimya/stubs/app/app/Console/Kernel.py.stub +22 -0
- bazimya/stubs/app/app/Console/__init__.py.stub +0 -0
- bazimya/stubs/app/app/Exceptions/Handler.py.stub +28 -0
- bazimya/stubs/app/app/Exceptions/__init__.py.stub +0 -0
- bazimya/stubs/app/app/Http/Controllers/AuthController.py.stub +75 -0
- bazimya/stubs/app/app/Http/Controllers/Controller.py.stub +11 -0
- bazimya/stubs/app/app/Http/Controllers/HomeController.py.stub +22 -0
- bazimya/stubs/app/app/Http/Controllers/__init__.py.stub +36 -0
- bazimya/stubs/app/app/Http/Kernel.py.stub +53 -0
- bazimya/stubs/app/app/Http/Middleware/Authenticate.py.stub +10 -0
- bazimya/stubs/app/app/Http/Middleware/EnsureEmailIsVerified.py.stub +21 -0
- bazimya/stubs/app/app/Http/Middleware/RedirectIfAuthenticated.py.stub +7 -0
- bazimya/stubs/app/app/Http/Middleware/TrustProxies.py.stub +25 -0
- bazimya/stubs/app/app/Http/Middleware/VerifyCsrfToken.py.stub +16 -0
- bazimya/stubs/app/app/Http/Middleware/__init__.py.stub +36 -0
- bazimya/stubs/app/app/Http/Requests/__init__.py.stub +21 -0
- bazimya/stubs/app/app/Http/__init__.py.stub +0 -0
- bazimya/stubs/app/app/Models/User.py.stub +15 -0
- bazimya/stubs/app/app/Models/__init__.py.stub +36 -0
- bazimya/stubs/app/app/Notifications/WelcomeNotification.py.stub +28 -0
- bazimya/stubs/app/app/Notifications/__init__.py.stub +21 -0
- bazimya/stubs/app/app/Providers/AppServiceProvider.py.stub +14 -0
- bazimya/stubs/app/app/Providers/AuthServiceProvider.py.stub +11 -0
- bazimya/stubs/app/app/Providers/EventServiceProvider.py.stub +15 -0
- bazimya/stubs/app/app/Providers/RouteServiceProvider.py.stub +19 -0
- bazimya/stubs/app/app/Providers/__init__.py.stub +0 -0
- bazimya/stubs/app/app/Rules/__init__.py.stub +0 -0
- bazimya/stubs/app/app/Services/__init__.py.stub +0 -0
- bazimya/stubs/app/app/Support/__init__.py.stub +0 -0
- bazimya/stubs/app/app/View/Components/Alert.py.stub +18 -0
- bazimya/stubs/app/app/View/Components/__init__.py.stub +22 -0
- bazimya/stubs/app/app/View/__init__.py.stub +0 -0
- bazimya/stubs/app/app/__init__.py.stub +0 -0
- bazimya/stubs/app/bazimya.stub +41 -0
- bazimya/stubs/app/bootstrap/app.py.stub +30 -0
- bazimya/stubs/app/bootstrap/cache/.gitkeep +0 -0
- bazimya/stubs/app/config/app.py.stub +44 -0
- bazimya/stubs/app/config/auth.py.stub +28 -0
- bazimya/stubs/app/config/cache.py.stub +20 -0
- bazimya/stubs/app/config/cors.py.stub +16 -0
- bazimya/stubs/app/config/database.py.stub +39 -0
- bazimya/stubs/app/config/extensions.py.stub +17 -0
- bazimya/stubs/app/config/filesystems.py.stub +27 -0
- bazimya/stubs/app/config/hashing.py.stub +22 -0
- bazimya/stubs/app/config/logging.py.stub +9 -0
- bazimya/stubs/app/config/mail.py.stub +31 -0
- bazimya/stubs/app/config/queue.py.stub +20 -0
- bazimya/stubs/app/config/services.py.stub +13 -0
- bazimya/stubs/app/config/session.py.stub +31 -0
- bazimya/stubs/app/config/view.py.stub +12 -0
- bazimya/stubs/app/database/.gitkeep +0 -0
- bazimya/stubs/app/database/factories/.gitkeep +0 -0
- bazimya/stubs/app/database/factories/UserFactory.py.stub +56 -0
- bazimya/stubs/app/database/migrations/2026_01_01_000000_create_users_table.py.stub +17 -0
- bazimya/stubs/app/database/seeders/DatabaseSeeder.py.stub +11 -0
- bazimya/stubs/app/extensions/.gitkeep +0 -0
- bazimya/stubs/app/package.json.stub +19 -0
- bazimya/stubs/app/passenger_wsgi.py.stub +18 -0
- bazimya/stubs/app/public/.htaccess.stub +33 -0
- bazimya/stubs/app/public/css/app.css.stub +86 -0
- bazimya/stubs/app/public/index.py.stub +19 -0
- bazimya/stubs/app/pytest.ini.stub +9 -0
- bazimya/stubs/app/requirements.txt.stub +11 -0
- bazimya/stubs/app/resources/css/app.css.stub +5 -0
- bazimya/stubs/app/resources/js/app.js.stub +4 -0
- bazimya/stubs/app/resources/views/auth/login.baz.html.stub +23 -0
- bazimya/stubs/app/resources/views/auth/register.baz.html.stub +30 -0
- bazimya/stubs/app/resources/views/components/alert.baz.html.stub +6 -0
- bazimya/stubs/app/resources/views/components/input-error.baz.html.stub +8 -0
- bazimya/stubs/app/resources/views/errors/404.baz.html.stub +9 -0
- bazimya/stubs/app/resources/views/errors/500.baz.html.stub +9 -0
- bazimya/stubs/app/resources/views/errors/503.baz.html.stub +8 -0
- bazimya/stubs/app/resources/views/home.baz.html.stub +45 -0
- bazimya/stubs/app/resources/views/layouts/app.baz.html.stub +26 -0
- bazimya/stubs/app/routes/api.py.stub +12 -0
- bazimya/stubs/app/routes/auth.py.stub +20 -0
- bazimya/stubs/app/routes/console.py.stub +12 -0
- bazimya/stubs/app/routes/web.py.stub +32 -0
- bazimya/stubs/app/storage/app/public/.gitkeep +0 -0
- bazimya/stubs/app/storage/framework/cache/.gitkeep +0 -0
- bazimya/stubs/app/storage/framework/sessions/.gitkeep +0 -0
- bazimya/stubs/app/storage/framework/testing/.gitkeep +0 -0
- bazimya/stubs/app/storage/framework/views/.gitkeep +0 -0
- bazimya/stubs/app/storage/logs/.gitkeep +0 -0
- bazimya/stubs/app/tests/Feature/.gitkeep +0 -0
- bazimya/stubs/app/tests/Feature/ExampleTest.py.stub +17 -0
- bazimya/stubs/app/tests/Feature/__init__.py.stub +0 -0
- bazimya/stubs/app/tests/TestCase.py.stub +21 -0
- bazimya/stubs/app/tests/Unit/.gitkeep +0 -0
- bazimya/stubs/app/tests/Unit/ExampleTest.py.stub +13 -0
- bazimya/stubs/app/tests/Unit/__init__.py.stub +0 -0
- bazimya/stubs/app/tests/__init__.py.stub +0 -0
- bazimya/stubs/app/wsgi.py.stub +16 -0
- bazimya/stubs/command.stub +18 -0
- bazimya/stubs/component.stub +8 -0
- bazimya/stubs/component.view.stub +3 -0
- bazimya/stubs/controller.api.stub +38 -0
- bazimya/stubs/controller.resource.stub +42 -0
- bazimya/stubs/controller.stub +8 -0
- bazimya/stubs/extension.stub +60 -0
- bazimya/stubs/extension.view.stub +20 -0
- bazimya/stubs/middleware.stub +13 -0
- bazimya/stubs/migration.create.stub +13 -0
- bazimya/stubs/migration.stub +11 -0
- bazimya/stubs/migration.table.stub +13 -0
- bazimya/stubs/model.stub +16 -0
- bazimya/stubs/notification.stub +17 -0
- bazimya/stubs/provider.stub +11 -0
- bazimya/stubs/request.stub +19 -0
- bazimya/stubs/rule.stub +11 -0
- bazimya/stubs/seeder.stub +6 -0
- bazimya/support/__init__.py +19 -0
- bazimya/support/aliases.py +73 -0
- bazimya/support/config.py +103 -0
- bazimya/support/env.py +137 -0
- bazimya/support/strings.py +116 -0
- bazimya/testing.py +381 -0
- bazimya/validation.py +340 -0
- bazimya/validation_rule.py +23 -0
- bazimya/view/__init__.py +14 -0
- bazimya/view/compiler.py +851 -0
- bazimya/view/components.py +156 -0
- bazimya/view/view.py +459 -0
- bazimya-0.2.0.dist-info/METADATA +604 -0
- bazimya-0.2.0.dist-info/RECORD +193 -0
- bazimya-0.2.0.dist-info/WHEEL +5 -0
- bazimya-0.2.0.dist-info/entry_points.txt +2 -0
- bazimya-0.2.0.dist-info/licenses/LICENSE +21 -0
- bazimya-0.2.0.dist-info/top_level.txt +1 -0
bazimya/__init__.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Bazimya — a Python web framework with Laravel's structure.
|
|
2
|
+
|
|
3
|
+
Everything an application needs is importable from here:
|
|
4
|
+
|
|
5
|
+
from bazimya import Route, Controller, Model, Schema, Migration, Response
|
|
6
|
+
|
|
7
|
+
The layout, the vocabulary and the lifecycle follow Laravel 10; the language
|
|
8
|
+
is Python, and templates are .baz.html.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .database.migration import Migration, Migrator
|
|
12
|
+
from .database.model import Model, ModelNotFound
|
|
13
|
+
from .database.query import QueryBuilder
|
|
14
|
+
from .database.schema import Blueprint, Schema
|
|
15
|
+
from .console.command import Command
|
|
16
|
+
from .console.registry import Console
|
|
17
|
+
from .extensions.extension import Extension
|
|
18
|
+
from .facades import (
|
|
19
|
+
App,
|
|
20
|
+
Auth,
|
|
21
|
+
Cache,
|
|
22
|
+
Config,
|
|
23
|
+
DB,
|
|
24
|
+
Event,
|
|
25
|
+
Extensions,
|
|
26
|
+
Hash,
|
|
27
|
+
Mail,
|
|
28
|
+
Notify,
|
|
29
|
+
Route,
|
|
30
|
+
Session,
|
|
31
|
+
Storage,
|
|
32
|
+
View,
|
|
33
|
+
app,
|
|
34
|
+
auth,
|
|
35
|
+
back,
|
|
36
|
+
csrf_token,
|
|
37
|
+
old,
|
|
38
|
+
base_path,
|
|
39
|
+
config,
|
|
40
|
+
database_path,
|
|
41
|
+
public_path,
|
|
42
|
+
redirect,
|
|
43
|
+
resource_path,
|
|
44
|
+
route,
|
|
45
|
+
storage_path,
|
|
46
|
+
view,
|
|
47
|
+
)
|
|
48
|
+
from .foundation.application import VERSION, Application
|
|
49
|
+
from .foundation.container import Container
|
|
50
|
+
from .foundation.provider import ServiceProvider
|
|
51
|
+
from .http.controller import Controller
|
|
52
|
+
from .http.form_request import FormRequest
|
|
53
|
+
from .http.exceptions import (
|
|
54
|
+
BadRequest,
|
|
55
|
+
Forbidden,
|
|
56
|
+
HttpException,
|
|
57
|
+
NotFound,
|
|
58
|
+
Unauthorized,
|
|
59
|
+
ValidationException,
|
|
60
|
+
abort,
|
|
61
|
+
abort_if,
|
|
62
|
+
abort_unless,
|
|
63
|
+
)
|
|
64
|
+
from .auth.middleware import Authenticate, RedirectIfAuthenticated
|
|
65
|
+
from .http.middleware import Middleware
|
|
66
|
+
from .notifications.notification import Notification
|
|
67
|
+
from .session.middleware import ShareErrorsFromSession, StartSession, VerifyCsrfToken
|
|
68
|
+
from .view.components import Component
|
|
69
|
+
from .http.request import Request
|
|
70
|
+
from .http.response import Response
|
|
71
|
+
from .support.env import Env, env
|
|
72
|
+
from .validation import Validator, validate
|
|
73
|
+
from .validation_rule import Rule
|
|
74
|
+
from .view.view import markup
|
|
75
|
+
|
|
76
|
+
__version__ = VERSION
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"App",
|
|
80
|
+
"Application",
|
|
81
|
+
"Auth",
|
|
82
|
+
"Authenticate",
|
|
83
|
+
"BadRequest",
|
|
84
|
+
"Cache",
|
|
85
|
+
"Blueprint",
|
|
86
|
+
"Command",
|
|
87
|
+
"Component",
|
|
88
|
+
"Config",
|
|
89
|
+
"Console",
|
|
90
|
+
"Container",
|
|
91
|
+
"Controller",
|
|
92
|
+
"DB",
|
|
93
|
+
"Env",
|
|
94
|
+
"Event",
|
|
95
|
+
"Extension",
|
|
96
|
+
"Extensions",
|
|
97
|
+
"Forbidden",
|
|
98
|
+
"Hash",
|
|
99
|
+
"FormRequest",
|
|
100
|
+
"HttpException",
|
|
101
|
+
"Middleware",
|
|
102
|
+
"Migration",
|
|
103
|
+
"Migrator",
|
|
104
|
+
"Model",
|
|
105
|
+
"ModelNotFound",
|
|
106
|
+
"NotFound",
|
|
107
|
+
"Notification",
|
|
108
|
+
"Notify",
|
|
109
|
+
"QueryBuilder",
|
|
110
|
+
"Request",
|
|
111
|
+
"RedirectIfAuthenticated",
|
|
112
|
+
"Response",
|
|
113
|
+
"Route",
|
|
114
|
+
"Rule",
|
|
115
|
+
"Schema",
|
|
116
|
+
"Session",
|
|
117
|
+
"ServiceProvider",
|
|
118
|
+
"ShareErrorsFromSession",
|
|
119
|
+
"StartSession",
|
|
120
|
+
"Storage",
|
|
121
|
+
"Unauthorized",
|
|
122
|
+
"ValidationException",
|
|
123
|
+
"VerifyCsrfToken",
|
|
124
|
+
"Validator",
|
|
125
|
+
"View",
|
|
126
|
+
"abort",
|
|
127
|
+
"abort_if",
|
|
128
|
+
"abort_unless",
|
|
129
|
+
"app",
|
|
130
|
+
"auth",
|
|
131
|
+
"back",
|
|
132
|
+
"base_path",
|
|
133
|
+
"config",
|
|
134
|
+
"csrf_token",
|
|
135
|
+
"database_path",
|
|
136
|
+
"env",
|
|
137
|
+
"markup",
|
|
138
|
+
"old",
|
|
139
|
+
"public_path",
|
|
140
|
+
"redirect",
|
|
141
|
+
"resource_path",
|
|
142
|
+
"route",
|
|
143
|
+
"storage_path",
|
|
144
|
+
"validate",
|
|
145
|
+
"view",
|
|
146
|
+
]
|
bazimya/__main__.py
ADDED
bazimya/auth/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Authentication: the session guard and its middleware."""
|
|
2
|
+
|
|
3
|
+
from .guard import AuthenticationError, SessionGuard
|
|
4
|
+
from .middleware import Authenticate, RedirectIfAuthenticated, ResolveUser
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Authenticate",
|
|
8
|
+
"AuthenticationError",
|
|
9
|
+
"RedirectIfAuthenticated",
|
|
10
|
+
"ResolveUser",
|
|
11
|
+
"SessionGuard",
|
|
12
|
+
]
|
bazimya/auth/guard.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""Authentication.
|
|
2
|
+
|
|
3
|
+
Laravel's Auth facade over a session guard:
|
|
4
|
+
|
|
5
|
+
if Auth.attempt({'email': email, 'password': password}):
|
|
6
|
+
request.session().regenerate()
|
|
7
|
+
|
|
8
|
+
return redirect('/dashboard')
|
|
9
|
+
|
|
10
|
+
Auth.user() the logged-in model, or None
|
|
11
|
+
Auth.check() True when someone is logged in
|
|
12
|
+
Auth.logout()
|
|
13
|
+
|
|
14
|
+
The guard reads its user model from config/auth.py, so it works with whatever
|
|
15
|
+
model you point it at rather than assuming `User`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AuthenticationError(RuntimeError):
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SessionGuard:
|
|
24
|
+
"""Keeps the authenticated user's id in the session."""
|
|
25
|
+
|
|
26
|
+
SESSION_KEY = "_auth_id"
|
|
27
|
+
PASSWORD_HASH_KEY = "_auth_password"
|
|
28
|
+
|
|
29
|
+
def __init__(self, application, config=None):
|
|
30
|
+
self.app = application
|
|
31
|
+
self.config = dict(config or {})
|
|
32
|
+
self._user = None
|
|
33
|
+
self._resolved = False
|
|
34
|
+
self._session = None
|
|
35
|
+
|
|
36
|
+
# -- wiring -----------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def set_session(self, session):
|
|
39
|
+
"""Called by StartSession, once per request."""
|
|
40
|
+
self._session = session
|
|
41
|
+
self._user = None
|
|
42
|
+
self._resolved = False
|
|
43
|
+
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def session(self):
|
|
47
|
+
return self._session
|
|
48
|
+
|
|
49
|
+
def provider_model(self):
|
|
50
|
+
"""The model this guard authenticates, named in config/auth.py."""
|
|
51
|
+
reference = self.config.get("model", "app.Models.User.User")
|
|
52
|
+
|
|
53
|
+
if not isinstance(reference, str):
|
|
54
|
+
return reference
|
|
55
|
+
|
|
56
|
+
import importlib
|
|
57
|
+
|
|
58
|
+
module_name, _, class_name = reference.rpartition(".")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
module = importlib.import_module(module_name)
|
|
62
|
+
except ImportError as error:
|
|
63
|
+
raise AuthenticationError(
|
|
64
|
+
"The auth model [{}] could not be imported. Check "
|
|
65
|
+
"auth.providers.users.model in config/auth.py.".format(reference)
|
|
66
|
+
) from error
|
|
67
|
+
|
|
68
|
+
model = getattr(module, class_name, None)
|
|
69
|
+
|
|
70
|
+
if model is None:
|
|
71
|
+
raise AuthenticationError(
|
|
72
|
+
"The auth model [{}] was not found.".format(reference)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return model
|
|
76
|
+
|
|
77
|
+
def hasher(self):
|
|
78
|
+
return self.app.make("hash")
|
|
79
|
+
|
|
80
|
+
# -- reading the current user -----------------------------------------
|
|
81
|
+
|
|
82
|
+
def user(self):
|
|
83
|
+
if self._resolved:
|
|
84
|
+
return self._user
|
|
85
|
+
|
|
86
|
+
self._resolved = True
|
|
87
|
+
|
|
88
|
+
if self._session is None:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
identifier = self._session.get(self.SESSION_KEY)
|
|
92
|
+
|
|
93
|
+
if identifier is None:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
model = self.provider_model()
|
|
97
|
+
user = model.find(identifier)
|
|
98
|
+
|
|
99
|
+
# A password change should end other sessions. The stored fragment is
|
|
100
|
+
# compared, so an old cookie stops working once the hash changes.
|
|
101
|
+
if user is not None and not self._password_still_valid(user):
|
|
102
|
+
self._session.forget(self.SESSION_KEY, self.PASSWORD_HASH_KEY)
|
|
103
|
+
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
self._user = user
|
|
107
|
+
|
|
108
|
+
return user
|
|
109
|
+
|
|
110
|
+
def _password_still_valid(self, user):
|
|
111
|
+
remembered = self._session.get(self.PASSWORD_HASH_KEY)
|
|
112
|
+
|
|
113
|
+
if remembered is None:
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
return remembered == self._password_fingerprint(user)
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _password_fingerprint(user):
|
|
120
|
+
import hashlib
|
|
121
|
+
|
|
122
|
+
password = user.attributes().get("password") or ""
|
|
123
|
+
|
|
124
|
+
return hashlib.sha256(str(password).encode("utf-8")).hexdigest()[:32]
|
|
125
|
+
|
|
126
|
+
def id(self):
|
|
127
|
+
user = self.user()
|
|
128
|
+
|
|
129
|
+
return user.key() if user is not None else None
|
|
130
|
+
|
|
131
|
+
def check(self):
|
|
132
|
+
return self.user() is not None
|
|
133
|
+
|
|
134
|
+
def guest(self):
|
|
135
|
+
return not self.check()
|
|
136
|
+
|
|
137
|
+
# -- logging in and out -----------------------------------------------
|
|
138
|
+
|
|
139
|
+
def attempt(self, credentials, remember=False):
|
|
140
|
+
"""Find a matching user and verify the password.
|
|
141
|
+
|
|
142
|
+
Returns True on success. The password is verified even when no user
|
|
143
|
+
matched, so that a wrong email and a wrong password take the same time
|
|
144
|
+
and the response cannot be used to enumerate accounts.
|
|
145
|
+
"""
|
|
146
|
+
credentials = dict(credentials or {})
|
|
147
|
+
password = credentials.pop("password", None)
|
|
148
|
+
|
|
149
|
+
user = self.retrieve_by_credentials(credentials)
|
|
150
|
+
|
|
151
|
+
if user is None:
|
|
152
|
+
self.hasher().check(str(password or ""), _DUMMY_HASH)
|
|
153
|
+
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
if not self.validate_password(user, password):
|
|
157
|
+
return False
|
|
158
|
+
|
|
159
|
+
self.login(user, remember)
|
|
160
|
+
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
def retrieve_by_credentials(self, credentials):
|
|
164
|
+
if not credentials:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
model = self.provider_model()
|
|
168
|
+
query = model.query()
|
|
169
|
+
|
|
170
|
+
for column, value in credentials.items():
|
|
171
|
+
query = query.where(column, value)
|
|
172
|
+
|
|
173
|
+
return query.first()
|
|
174
|
+
|
|
175
|
+
def validate_password(self, user, password):
|
|
176
|
+
stored = user.attributes().get("password")
|
|
177
|
+
|
|
178
|
+
if not stored:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
if not self.hasher().check(str(password or ""), stored):
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
# Upgrade the stored hash when the configured cost has moved on. This
|
|
185
|
+
# is the only moment the plaintext is available to do it.
|
|
186
|
+
if self.hasher().needs_rehash(stored):
|
|
187
|
+
user.force_fill({"password": self.hasher().make(password)})
|
|
188
|
+
user.save()
|
|
189
|
+
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
def login(self, user, remember=False):
|
|
193
|
+
if self._session is None:
|
|
194
|
+
raise AuthenticationError(
|
|
195
|
+
"There is no session to log in to. Add StartSession to the "
|
|
196
|
+
"middleware group for this route."
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# A new session id on login is what closes off session fixation: any
|
|
200
|
+
# id an attacker planted before login stops being the one in use.
|
|
201
|
+
self._session.regenerate()
|
|
202
|
+
self._session.put(self.SESSION_KEY, user.key())
|
|
203
|
+
self._session.put(self.PASSWORD_HASH_KEY, self._password_fingerprint(user))
|
|
204
|
+
|
|
205
|
+
self._user = user
|
|
206
|
+
self._resolved = True
|
|
207
|
+
|
|
208
|
+
return user
|
|
209
|
+
|
|
210
|
+
def login_using_id(self, identifier):
|
|
211
|
+
user = self.provider_model().find(identifier)
|
|
212
|
+
|
|
213
|
+
return self.login(user) if user is not None else None
|
|
214
|
+
|
|
215
|
+
def once(self, credentials):
|
|
216
|
+
"""Verify credentials without touching the session."""
|
|
217
|
+
user = self.retrieve_by_credentials(
|
|
218
|
+
{k: v for k, v in credentials.items() if k != "password"}
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
if user is None or not self.validate_password(user, credentials.get("password")):
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
self._user = user
|
|
225
|
+
self._resolved = True
|
|
226
|
+
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
def logout(self):
|
|
230
|
+
if self._session is not None:
|
|
231
|
+
self._session.forget(self.SESSION_KEY, self.PASSWORD_HASH_KEY)
|
|
232
|
+
self._session.invalidate()
|
|
233
|
+
|
|
234
|
+
self._user = None
|
|
235
|
+
self._resolved = True
|
|
236
|
+
|
|
237
|
+
return self
|
|
238
|
+
|
|
239
|
+
def set_user(self, user):
|
|
240
|
+
self._user = user
|
|
241
|
+
self._resolved = True
|
|
242
|
+
|
|
243
|
+
return self
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
#: A real hash of a random value, so a failed lookup still costs one
|
|
247
|
+
#: verification and the timing of "no such user" matches "wrong password".
|
|
248
|
+
_DUMMY_HASH = (
|
|
249
|
+
"pbkdf2$480000$AAAAAAAAAAAAAAAAAAAAAA$"
|
|
250
|
+
"cGxhY2Vob2xkZXJwbGFjZWhvbGRlcnBsYWNlaG9sZGVy"
|
|
251
|
+
)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Authentication middleware — Laravel's Authenticate and RedirectIfAuthenticated."""
|
|
2
|
+
|
|
3
|
+
from ..http.exceptions import Unauthorized
|
|
4
|
+
from ..http.middleware import Middleware
|
|
5
|
+
from ..http.response import Response
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Authenticate(Middleware):
|
|
9
|
+
"""Stop guests. Registered as the `auth` alias.
|
|
10
|
+
|
|
11
|
+
Browsers get a redirect to the login page; anything asking for JSON gets a
|
|
12
|
+
401, because redirecting an API client to an HTML form is never useful.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
redirect_to = "/login"
|
|
16
|
+
|
|
17
|
+
def handle(self, request, next):
|
|
18
|
+
from ..facades import Auth
|
|
19
|
+
|
|
20
|
+
session = request.get("session")
|
|
21
|
+
|
|
22
|
+
if session is not None:
|
|
23
|
+
Auth.set_session(session)
|
|
24
|
+
|
|
25
|
+
user = Auth.user()
|
|
26
|
+
|
|
27
|
+
if user is None:
|
|
28
|
+
if request.wants_json():
|
|
29
|
+
raise Unauthorized("Unauthenticated.")
|
|
30
|
+
|
|
31
|
+
if session is not None:
|
|
32
|
+
# Remember where they were headed, so login can send them back.
|
|
33
|
+
session.put("_intended", request.full_url())
|
|
34
|
+
|
|
35
|
+
return Response.redirect(self.redirect_to)
|
|
36
|
+
|
|
37
|
+
request.set("user", user)
|
|
38
|
+
|
|
39
|
+
return next(request)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class RedirectIfAuthenticated(Middleware):
|
|
43
|
+
"""Keep logged-in users off the login and register pages. Alias: `guest`."""
|
|
44
|
+
|
|
45
|
+
redirect_to = "/"
|
|
46
|
+
|
|
47
|
+
def handle(self, request, next):
|
|
48
|
+
from ..facades import Auth
|
|
49
|
+
|
|
50
|
+
session = request.get("session")
|
|
51
|
+
|
|
52
|
+
if session is not None:
|
|
53
|
+
Auth.set_session(session)
|
|
54
|
+
|
|
55
|
+
if Auth.check():
|
|
56
|
+
return Response.redirect(self.redirect_to)
|
|
57
|
+
|
|
58
|
+
return next(request)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ResolveUser(Middleware):
|
|
62
|
+
"""Attach the authenticated user to the request without requiring one.
|
|
63
|
+
|
|
64
|
+
For pages that show a name when signed in and a login link otherwise.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def handle(self, request, next):
|
|
68
|
+
from ..facades import Auth
|
|
69
|
+
|
|
70
|
+
session = request.get("session")
|
|
71
|
+
|
|
72
|
+
if session is not None:
|
|
73
|
+
Auth.set_session(session)
|
|
74
|
+
request.set("user", Auth.user())
|
|
75
|
+
|
|
76
|
+
return next(request)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""The cache.
|
|
2
|
+
|
|
3
|
+
Laravel's Cache facade, with a file store under storage/framework/cache. No
|
|
4
|
+
Redis, no Memcached — the file store needs nothing installed, which is the
|
|
5
|
+
whole point on shared hosting.
|
|
6
|
+
|
|
7
|
+
Cache.put('key', value, seconds=600)
|
|
8
|
+
Cache.get('key', default)
|
|
9
|
+
Cache.remember('key', 600, lambda: expensive())
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FileStore:
|
|
19
|
+
def __init__(self, directory):
|
|
20
|
+
self.directory = directory
|
|
21
|
+
|
|
22
|
+
def _file(self, key):
|
|
23
|
+
safe = hashlib.sha256(str(key).encode("utf-8")).hexdigest()
|
|
24
|
+
|
|
25
|
+
# Two levels of subdirectory, so a large cache does not put tens of
|
|
26
|
+
# thousands of files in one directory.
|
|
27
|
+
return os.path.join(self.directory, safe[:2], safe[2:4], safe)
|
|
28
|
+
|
|
29
|
+
def get(self, key, default=None):
|
|
30
|
+
path = self._file(key)
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
with open(path, "r", encoding="utf-8") as handle:
|
|
34
|
+
payload = json.load(handle)
|
|
35
|
+
except (OSError, ValueError):
|
|
36
|
+
return default
|
|
37
|
+
|
|
38
|
+
expires = payload.get("expires")
|
|
39
|
+
|
|
40
|
+
if expires is not None and expires < time.time():
|
|
41
|
+
self.forget(key)
|
|
42
|
+
|
|
43
|
+
return default
|
|
44
|
+
|
|
45
|
+
return payload.get("value", default)
|
|
46
|
+
|
|
47
|
+
def put(self, key, value, seconds=None):
|
|
48
|
+
path = self._file(key)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
52
|
+
|
|
53
|
+
payload = {
|
|
54
|
+
"value": value,
|
|
55
|
+
"expires": time.time() + seconds if seconds else None,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
temporary = "{}.{}.tmp".format(path, os.getpid())
|
|
59
|
+
|
|
60
|
+
with open(temporary, "w", encoding="utf-8") as handle:
|
|
61
|
+
json.dump(payload, handle, default=str)
|
|
62
|
+
|
|
63
|
+
os.replace(temporary, path)
|
|
64
|
+
|
|
65
|
+
return True
|
|
66
|
+
except (OSError, TypeError, ValueError):
|
|
67
|
+
# A cache that cannot write is slow, not broken.
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
def forget(self, key):
|
|
71
|
+
try:
|
|
72
|
+
os.remove(self._file(key))
|
|
73
|
+
|
|
74
|
+
return True
|
|
75
|
+
except OSError:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
def flush(self):
|
|
79
|
+
import shutil
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
shutil.rmtree(self.directory)
|
|
83
|
+
os.makedirs(self.directory, exist_ok=True)
|
|
84
|
+
|
|
85
|
+
return True
|
|
86
|
+
except OSError:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ArrayStore:
|
|
91
|
+
"""In-memory, per-process. The default while testing."""
|
|
92
|
+
|
|
93
|
+
def __init__(self):
|
|
94
|
+
self._items = {}
|
|
95
|
+
|
|
96
|
+
def get(self, key, default=None):
|
|
97
|
+
entry = self._items.get(key)
|
|
98
|
+
|
|
99
|
+
if entry is None:
|
|
100
|
+
return default
|
|
101
|
+
|
|
102
|
+
value, expires = entry
|
|
103
|
+
|
|
104
|
+
if expires is not None and expires < time.time():
|
|
105
|
+
self._items.pop(key, None)
|
|
106
|
+
|
|
107
|
+
return default
|
|
108
|
+
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
def put(self, key, value, seconds=None):
|
|
112
|
+
self._items[key] = (value, time.time() + seconds if seconds else None)
|
|
113
|
+
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def forget(self, key):
|
|
117
|
+
return self._items.pop(key, None) is not None
|
|
118
|
+
|
|
119
|
+
def flush(self):
|
|
120
|
+
self._items = {}
|
|
121
|
+
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class NullStore:
|
|
126
|
+
"""Stores nothing. Useful for turning caching off without code changes."""
|
|
127
|
+
|
|
128
|
+
def get(self, key, default=None):
|
|
129
|
+
return default
|
|
130
|
+
|
|
131
|
+
def put(self, key, value, seconds=None):
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
def forget(self, key):
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def flush(self):
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CacheRepository:
|
|
142
|
+
def __init__(self, store):
|
|
143
|
+
self.store = store
|
|
144
|
+
|
|
145
|
+
def get(self, key, default=None):
|
|
146
|
+
return self.store.get(key, default)
|
|
147
|
+
|
|
148
|
+
def put(self, key, value, seconds=None):
|
|
149
|
+
return self.store.put(key, value, seconds)
|
|
150
|
+
|
|
151
|
+
def add(self, key, value, seconds=None):
|
|
152
|
+
"""Store it only if it is not there already."""
|
|
153
|
+
sentinel = object()
|
|
154
|
+
|
|
155
|
+
if self.store.get(key, sentinel) is not sentinel:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
return self.store.put(key, value, seconds)
|
|
159
|
+
|
|
160
|
+
def forever(self, key, value):
|
|
161
|
+
return self.store.put(key, value, None)
|
|
162
|
+
|
|
163
|
+
def has(self, key):
|
|
164
|
+
sentinel = object()
|
|
165
|
+
|
|
166
|
+
return self.store.get(key, sentinel) is not sentinel
|
|
167
|
+
|
|
168
|
+
def missing(self, key):
|
|
169
|
+
return not self.has(key)
|
|
170
|
+
|
|
171
|
+
def pull(self, key, default=None):
|
|
172
|
+
value = self.get(key, default)
|
|
173
|
+
self.forget(key)
|
|
174
|
+
|
|
175
|
+
return value
|
|
176
|
+
|
|
177
|
+
def remember(self, key, seconds, callback):
|
|
178
|
+
"""Return the cached value, computing and storing it when absent."""
|
|
179
|
+
sentinel = object()
|
|
180
|
+
value = self.store.get(key, sentinel)
|
|
181
|
+
|
|
182
|
+
if value is not sentinel:
|
|
183
|
+
return value
|
|
184
|
+
|
|
185
|
+
value = callback()
|
|
186
|
+
self.store.put(key, value, seconds)
|
|
187
|
+
|
|
188
|
+
return value
|
|
189
|
+
|
|
190
|
+
def remember_forever(self, key, callback):
|
|
191
|
+
return self.remember(key, None, callback)
|
|
192
|
+
|
|
193
|
+
def increment(self, key, amount=1):
|
|
194
|
+
value = int(self.get(key, 0) or 0) + amount
|
|
195
|
+
self.forever(key, value)
|
|
196
|
+
|
|
197
|
+
return value
|
|
198
|
+
|
|
199
|
+
def decrement(self, key, amount=1):
|
|
200
|
+
return self.increment(key, -amount)
|
|
201
|
+
|
|
202
|
+
def forget(self, key):
|
|
203
|
+
return self.store.forget(key)
|
|
204
|
+
|
|
205
|
+
def flush(self):
|
|
206
|
+
return self.store.flush()
|