WuttaWeb 0.31.1__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.
- wuttaweb/__init__.py +27 -0
- wuttaweb/_version.py +9 -0
- wuttaweb/app.py +295 -0
- wuttaweb/auth.py +252 -0
- wuttaweb/cleanup.py +102 -0
- wuttaweb/cli/__init__.py +28 -0
- wuttaweb/cli/webapp.py +97 -0
- wuttaweb/code-templates/new-master-view.mako +116 -0
- wuttaweb/code-templates/new-table.mako +64 -0
- wuttaweb/conf.py +89 -0
- wuttaweb/db/__init__.py +31 -0
- wuttaweb/db/continuum.py +62 -0
- wuttaweb/db/sess.py +66 -0
- wuttaweb/diffs.py +290 -0
- wuttaweb/email-templates/feedback.html.mako +40 -0
- wuttaweb/email-templates/feedback.txt.mako +23 -0
- wuttaweb/emails.py +49 -0
- wuttaweb/forms/__init__.py +32 -0
- wuttaweb/forms/base.py +1577 -0
- wuttaweb/forms/schema.py +771 -0
- wuttaweb/forms/widgets.py +631 -0
- wuttaweb/grids/__init__.py +33 -0
- wuttaweb/grids/base.py +2757 -0
- wuttaweb/grids/filters.py +891 -0
- wuttaweb/handler.py +218 -0
- wuttaweb/helpers.py +61 -0
- wuttaweb/menus.py +395 -0
- wuttaweb/progress.py +170 -0
- wuttaweb/static/__init__.py +74 -0
- wuttaweb/static/img/favicon.ico +0 -0
- wuttaweb/static/img/logo.png +0 -0
- wuttaweb/static/img/testing.png +0 -0
- wuttaweb/subscribers.py +431 -0
- wuttaweb/templates/alembic/dashboard.mako +238 -0
- wuttaweb/templates/alembic/migrations/configure.mako +31 -0
- wuttaweb/templates/alembic/migrations/create.mako +70 -0
- wuttaweb/templates/alembic/migrations/index.mako +25 -0
- wuttaweb/templates/alembic/migrations/view.mako +17 -0
- wuttaweb/templates/appinfo/configure.mako +412 -0
- wuttaweb/templates/appinfo/index.mako +120 -0
- wuttaweb/templates/auth/change_password.mako +7 -0
- wuttaweb/templates/auth/login.mako +65 -0
- wuttaweb/templates/base.mako +861 -0
- wuttaweb/templates/base_meta.mako +23 -0
- wuttaweb/templates/batch/view.mako +113 -0
- wuttaweb/templates/configure.mako +207 -0
- wuttaweb/templates/deform/checkbox.pt +13 -0
- wuttaweb/templates/deform/checkbox_choice.pt +18 -0
- wuttaweb/templates/deform/checked_password.pt +16 -0
- wuttaweb/templates/deform/dateinput.pt +7 -0
- wuttaweb/templates/deform/datetimeinput.pt +12 -0
- wuttaweb/templates/deform/moneyinput.pt +8 -0
- wuttaweb/templates/deform/password.pt +10 -0
- wuttaweb/templates/deform/permissions.pt +23 -0
- wuttaweb/templates/deform/readonly/checkbox.pt +4 -0
- wuttaweb/templates/deform/readonly/email_recips.pt +5 -0
- wuttaweb/templates/deform/readonly/external_link.pt +10 -0
- wuttaweb/templates/deform/readonly/filedownload.pt +14 -0
- wuttaweb/templates/deform/readonly/notes.pt +7 -0
- wuttaweb/templates/deform/readonly/objectref.pt +9 -0
- wuttaweb/templates/deform/readonly/permissions.pt +18 -0
- wuttaweb/templates/deform/readonly/rolerefs.pt +7 -0
- wuttaweb/templates/deform/select.pt +50 -0
- wuttaweb/templates/deform/textarea.pt +12 -0
- wuttaweb/templates/deform/textinput.pt +9 -0
- wuttaweb/templates/deform/wutta_checked_password.pt +8 -0
- wuttaweb/templates/diff.mako +15 -0
- wuttaweb/templates/email/settings/view.mako +39 -0
- wuttaweb/templates/forbidden.mako +26 -0
- wuttaweb/templates/form.mako +59 -0
- wuttaweb/templates/forms/vue_buttons.mako +43 -0
- wuttaweb/templates/forms/vue_fields.mako +4 -0
- wuttaweb/templates/forms/vue_template.mako +54 -0
- wuttaweb/templates/grids/table_element.mako +75 -0
- wuttaweb/templates/grids/vue_template.mako +779 -0
- wuttaweb/templates/home.mako +35 -0
- wuttaweb/templates/master/configure.mako +9 -0
- wuttaweb/templates/master/create.mako +7 -0
- wuttaweb/templates/master/create_row.mako +4 -0
- wuttaweb/templates/master/delete.mako +18 -0
- wuttaweb/templates/master/edit.mako +9 -0
- wuttaweb/templates/master/form.mako +5 -0
- wuttaweb/templates/master/index.mako +102 -0
- wuttaweb/templates/master/merge.mako +88 -0
- wuttaweb/templates/master/view.mako +90 -0
- wuttaweb/templates/master/view_version.mako +50 -0
- wuttaweb/templates/master/view_versions.mako +20 -0
- wuttaweb/templates/notfound.mako +23 -0
- wuttaweb/templates/page.mako +55 -0
- wuttaweb/templates/people/view_profile.mako +12 -0
- wuttaweb/templates/progress.mako +127 -0
- wuttaweb/templates/reports/view.mako +61 -0
- wuttaweb/templates/setup.mako +20 -0
- wuttaweb/templates/tables/app/create.mako +1035 -0
- wuttaweb/templates/tables/app/index.mako +33 -0
- wuttaweb/templates/themes/butterfly/base.mako +78 -0
- wuttaweb/templates/themes/butterfly/buefy-components.mako +802 -0
- wuttaweb/templates/themes/butterfly/buefy-plugin.mako +32 -0
- wuttaweb/templates/themes/butterfly/http-plugin.mako +100 -0
- wuttaweb/templates/upgrade.mako +64 -0
- wuttaweb/templates/upgrades/configure.mako +20 -0
- wuttaweb/templates/upgrades/index.mako +14 -0
- wuttaweb/templates/upgrades/view.mako +37 -0
- wuttaweb/templates/users/view.mako +126 -0
- wuttaweb/templates/views/master/configure.mako +31 -0
- wuttaweb/templates/views/master/create.mako +846 -0
- wuttaweb/templates/views/master/index.mako +17 -0
- wuttaweb/templates/wutta-components.mako +850 -0
- wuttaweb/testing.py +226 -0
- wuttaweb/util.py +906 -0
- wuttaweb/views/__init__.py +38 -0
- wuttaweb/views/alembic.py +576 -0
- wuttaweb/views/auth.py +340 -0
- wuttaweb/views/base.py +172 -0
- wuttaweb/views/batch.py +501 -0
- wuttaweb/views/common.py +378 -0
- wuttaweb/views/email.py +335 -0
- wuttaweb/views/essential.py +85 -0
- wuttaweb/views/master.py +4602 -0
- wuttaweb/views/people.py +240 -0
- wuttaweb/views/progress.py +77 -0
- wuttaweb/views/reports.py +280 -0
- wuttaweb/views/roles.py +463 -0
- wuttaweb/views/settings.py +441 -0
- wuttaweb/views/tables.py +486 -0
- wuttaweb/views/upgrades.py +388 -0
- wuttaweb/views/users.py +539 -0
- wuttaweb/views/views.py +469 -0
- wuttaweb-0.31.1.dist-info/METADATA +66 -0
- wuttaweb-0.31.1.dist-info/RECORD +133 -0
- wuttaweb-0.31.1.dist-info/WHEEL +4 -0
- wuttaweb-0.31.1.dist-info/entry_points.txt +20 -0
- wuttaweb-0.31.1.dist-info/licenses/COPYING.txt +674 -0
wuttaweb/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# -*- coding: utf-8; -*-
|
|
2
|
+
################################################################################
|
|
3
|
+
#
|
|
4
|
+
# wuttaweb -- Web App for Wutta Framework
|
|
5
|
+
# Copyright © 2024 Lance Edgar
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Wutta Framework.
|
|
8
|
+
#
|
|
9
|
+
# Wutta Framework is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU General Public License as published by the Free
|
|
11
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
12
|
+
# later version.
|
|
13
|
+
#
|
|
14
|
+
# Wutta Framework is distributed in the hope that it will be useful, but
|
|
15
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
17
|
+
# more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License along with
|
|
20
|
+
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
################################################################################
|
|
23
|
+
"""
|
|
24
|
+
wuttaweb - base package for web app
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from ._version import __version__
|
wuttaweb/_version.py
ADDED
wuttaweb/app.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# -*- coding: utf-8; -*-
|
|
2
|
+
################################################################################
|
|
3
|
+
#
|
|
4
|
+
# wuttaweb -- Web App for Wutta Framework
|
|
5
|
+
# Copyright © 2024-2025 Lance Edgar
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Wutta Framework.
|
|
8
|
+
#
|
|
9
|
+
# Wutta Framework is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU General Public License as published by the Free
|
|
11
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
12
|
+
# later version.
|
|
13
|
+
#
|
|
14
|
+
# Wutta Framework is distributed in the hope that it will be useful, but
|
|
15
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
17
|
+
# more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License along with
|
|
20
|
+
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
################################################################################
|
|
23
|
+
"""
|
|
24
|
+
Application
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
import os
|
|
29
|
+
|
|
30
|
+
from wuttjamaican.app import AppProvider
|
|
31
|
+
from wuttjamaican.conf import make_config
|
|
32
|
+
|
|
33
|
+
from asgiref.wsgi import WsgiToAsgi
|
|
34
|
+
from pyramid.config import Configurator
|
|
35
|
+
|
|
36
|
+
import wuttaweb.db
|
|
37
|
+
from wuttaweb.auth import WuttaSecurityPolicy
|
|
38
|
+
from wuttaweb.util import get_effective_theme, get_theme_template_path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
log = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class WebAppProvider(AppProvider):
|
|
45
|
+
"""
|
|
46
|
+
The :term:`app provider` for WuttaWeb. This adds some methods to
|
|
47
|
+
the :term:`app handler`, which are specific to web apps. It also
|
|
48
|
+
registers some :term:`email templates <email template>` for the
|
|
49
|
+
app, etc.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
email_modules = ["wuttaweb.emails"]
|
|
53
|
+
email_templates = ["wuttaweb:email-templates"]
|
|
54
|
+
|
|
55
|
+
def get_web_handler(self):
|
|
56
|
+
"""
|
|
57
|
+
Get the configured "web" handler for the app.
|
|
58
|
+
|
|
59
|
+
Specify a custom handler in your config file like this:
|
|
60
|
+
|
|
61
|
+
.. code-block:: ini
|
|
62
|
+
|
|
63
|
+
[wutta]
|
|
64
|
+
web.handler_spec = poser.web.handler:PoserWebHandler
|
|
65
|
+
|
|
66
|
+
:returns: Instance of :class:`~wuttaweb.handler.WebHandler`.
|
|
67
|
+
"""
|
|
68
|
+
if "web" not in self.app.handlers:
|
|
69
|
+
spec = self.config.get(
|
|
70
|
+
f"{self.appname}.web.handler_spec",
|
|
71
|
+
default="wuttaweb.handler:WebHandler",
|
|
72
|
+
)
|
|
73
|
+
self.app.handlers["web"] = self.app.load_object(spec)(self.config)
|
|
74
|
+
return self.app.handlers["web"]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def make_wutta_config(settings, config_maker=None, **kwargs):
|
|
78
|
+
"""
|
|
79
|
+
Make a WuttaConfig object from the given settings.
|
|
80
|
+
|
|
81
|
+
Note that ``settings`` dict will (typically) correspond to the
|
|
82
|
+
``[app:main]`` section of your config file.
|
|
83
|
+
|
|
84
|
+
Regardless, the ``settings`` must contain a special key/value
|
|
85
|
+
which is needed to identify the location of the config file.
|
|
86
|
+
Assuming the typical scenario then, your config file should have
|
|
87
|
+
an entry like this:
|
|
88
|
+
|
|
89
|
+
.. code-block:: ini
|
|
90
|
+
|
|
91
|
+
[app:main]
|
|
92
|
+
wutta.config = %(__file__)s
|
|
93
|
+
|
|
94
|
+
The ``%(__file__)s`` is auto-replaced with the config file path,
|
|
95
|
+
so ultimately ``settings`` would contain something like (at
|
|
96
|
+
minimum)::
|
|
97
|
+
|
|
98
|
+
{'wutta.config': '/path/to/config/file'}
|
|
99
|
+
|
|
100
|
+
If this config file path cannot be discovered, an error is raised.
|
|
101
|
+
"""
|
|
102
|
+
wutta_config = settings.get("wutta_config")
|
|
103
|
+
if not wutta_config:
|
|
104
|
+
|
|
105
|
+
# validate config file path
|
|
106
|
+
path = settings.get("wutta.config")
|
|
107
|
+
if not path or not os.path.exists(path):
|
|
108
|
+
raise ValueError(
|
|
109
|
+
"Please set 'wutta.config' in [app:main] "
|
|
110
|
+
"section of config to the path of your "
|
|
111
|
+
"config file. Lame, but necessary."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# make config, add to settings
|
|
115
|
+
config_maker = config_maker or make_config
|
|
116
|
+
wutta_config = config_maker(path, **kwargs)
|
|
117
|
+
settings["wutta_config"] = wutta_config
|
|
118
|
+
|
|
119
|
+
# configure database sessions
|
|
120
|
+
if hasattr(wutta_config, "appdb_engine"):
|
|
121
|
+
wuttaweb.db.Session.configure(bind=wutta_config.appdb_engine)
|
|
122
|
+
|
|
123
|
+
return wutta_config
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def make_pyramid_config(settings):
|
|
127
|
+
"""
|
|
128
|
+
Make and return a Pyramid config object from the given settings.
|
|
129
|
+
|
|
130
|
+
The config is initialized with certain features deemed useful for
|
|
131
|
+
all apps.
|
|
132
|
+
|
|
133
|
+
:returns: Instance of
|
|
134
|
+
:class:`pyramid:pyramid.config.Configurator`.
|
|
135
|
+
"""
|
|
136
|
+
settings.setdefault("fanstatic.versioning", "true")
|
|
137
|
+
settings.setdefault("mako.directories", ["wuttaweb:templates"])
|
|
138
|
+
settings.setdefault(
|
|
139
|
+
"pyramid_deform.template_search_path", "wuttaweb:templates/deform"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# update settings per current theme
|
|
143
|
+
establish_theme(settings)
|
|
144
|
+
|
|
145
|
+
pyramid_config = Configurator(settings=settings)
|
|
146
|
+
|
|
147
|
+
# configure user authorization / authentication
|
|
148
|
+
pyramid_config.set_security_policy(WuttaSecurityPolicy())
|
|
149
|
+
|
|
150
|
+
# require CSRF token for POST
|
|
151
|
+
pyramid_config.set_default_csrf_options(
|
|
152
|
+
require_csrf=True, token="_csrf", header="X-CSRF-TOKEN"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
pyramid_config.include("pyramid_beaker")
|
|
156
|
+
pyramid_config.include("pyramid_deform")
|
|
157
|
+
pyramid_config.include("pyramid_fanstatic")
|
|
158
|
+
pyramid_config.include("pyramid_mako")
|
|
159
|
+
pyramid_config.include("pyramid_tm")
|
|
160
|
+
|
|
161
|
+
# add some permissions magic
|
|
162
|
+
pyramid_config.add_directive(
|
|
163
|
+
"add_wutta_permission_group", "wuttaweb.auth.add_permission_group"
|
|
164
|
+
)
|
|
165
|
+
pyramid_config.add_directive("add_wutta_permission", "wuttaweb.auth.add_permission")
|
|
166
|
+
|
|
167
|
+
# add some more config magic
|
|
168
|
+
pyramid_config.add_directive(
|
|
169
|
+
"add_wutta_master_view", "wuttaweb.conf.add_master_view"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return pyramid_config
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def main(global_config, **settings): # pylint: disable=unused-argument
|
|
176
|
+
"""
|
|
177
|
+
Make and return the WSGI application, per given settings.
|
|
178
|
+
|
|
179
|
+
This function is designed to be called via Paste, hence it does
|
|
180
|
+
require params and therefore can't be used directly as app factory
|
|
181
|
+
for general WSGI servers. For the latter see
|
|
182
|
+
:func:`make_wsgi_app()` instead.
|
|
183
|
+
|
|
184
|
+
And this *particular* function is not even that useful, it only
|
|
185
|
+
constructs an app with minimal views built-in to WuttaWeb. Most
|
|
186
|
+
apps will define their own ``main()`` function (e.g. as
|
|
187
|
+
``poser.web.app:main``), similar to this one but with additional
|
|
188
|
+
views and other config.
|
|
189
|
+
"""
|
|
190
|
+
wutta_config = make_wutta_config(settings) # pylint: disable=unused-variable
|
|
191
|
+
pyramid_config = make_pyramid_config(settings)
|
|
192
|
+
|
|
193
|
+
pyramid_config.include("wuttaweb.static")
|
|
194
|
+
pyramid_config.include("wuttaweb.subscribers")
|
|
195
|
+
pyramid_config.include("wuttaweb.views")
|
|
196
|
+
|
|
197
|
+
return pyramid_config.make_wsgi_app()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def make_wsgi_app(main_app=None, config=None):
|
|
201
|
+
"""
|
|
202
|
+
Make and return a WSGI app, using the given Paste app factory.
|
|
203
|
+
|
|
204
|
+
See also :func:`make_asgi_app()` for the ASGI equivalent.
|
|
205
|
+
|
|
206
|
+
This function could be used directly for general WSGI servers
|
|
207
|
+
(e.g. uvicorn), ***if*** you just want the built-in :func:`main()`
|
|
208
|
+
app factory.
|
|
209
|
+
|
|
210
|
+
But most likely you do not, in which case you must define your own
|
|
211
|
+
function and call this one with your preferred app factory::
|
|
212
|
+
|
|
213
|
+
from wuttaweb.app import make_wsgi_app
|
|
214
|
+
|
|
215
|
+
def my_main(global_config, **settings):
|
|
216
|
+
# TODO: build your app
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
def make_my_wsgi_app():
|
|
220
|
+
return make_wsgi_app(my_main)
|
|
221
|
+
|
|
222
|
+
So ``make_my_wsgi_app()`` could then be used as-is for general
|
|
223
|
+
WSGI servers. However, note that this approach will require
|
|
224
|
+
setting the ``WUTTA_CONFIG_FILES`` environment variable, unless
|
|
225
|
+
running via :ref:`wutta-webapp`.
|
|
226
|
+
|
|
227
|
+
:param main_app: Either a Paste-compatible app factory, or
|
|
228
|
+
:term:`spec` for one. If not specified, the built-in
|
|
229
|
+
:func:`main()` is assumed.
|
|
230
|
+
|
|
231
|
+
:param config: Optional :term:`config object`. If not specified,
|
|
232
|
+
one is created based on ``WUTTA_CONFIG_FILES`` environment
|
|
233
|
+
variable.
|
|
234
|
+
"""
|
|
235
|
+
if not config:
|
|
236
|
+
config = make_config()
|
|
237
|
+
app = config.get_app()
|
|
238
|
+
|
|
239
|
+
# extract pyramid settings
|
|
240
|
+
settings = config.get_dict("app:main")
|
|
241
|
+
|
|
242
|
+
# keep same config object
|
|
243
|
+
settings["wutta_config"] = config
|
|
244
|
+
|
|
245
|
+
# determine the app factory
|
|
246
|
+
if isinstance(main_app, str):
|
|
247
|
+
factory = app.load_object(main_app)
|
|
248
|
+
elif callable(main_app):
|
|
249
|
+
factory = main_app
|
|
250
|
+
else:
|
|
251
|
+
raise ValueError("main_app must be spec or callable")
|
|
252
|
+
|
|
253
|
+
# construct a pyramid app "per usual"
|
|
254
|
+
return factory({}, **settings)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def make_asgi_app(main_app=None, config=None):
|
|
258
|
+
"""
|
|
259
|
+
Make and return a ASGI app, using the given Paste app factory.
|
|
260
|
+
|
|
261
|
+
This works the same as :func:`make_wsgi_app()` and should be
|
|
262
|
+
called in the same way etc.
|
|
263
|
+
"""
|
|
264
|
+
wsgi_app = make_wsgi_app(main_app, config=config)
|
|
265
|
+
return WsgiToAsgi(wsgi_app)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def establish_theme(settings):
|
|
269
|
+
"""
|
|
270
|
+
Establishes initial theme on app startup. This mostly involves
|
|
271
|
+
updating the given ``settings`` dict.
|
|
272
|
+
|
|
273
|
+
This function is called automatically from within
|
|
274
|
+
:func:`make_pyramid_config()`.
|
|
275
|
+
|
|
276
|
+
It will first call :func:`~wuttaweb.util.get_effective_theme()` to
|
|
277
|
+
read the current theme from the :term:`settings table`, and store
|
|
278
|
+
this within ``settings['wuttaweb.theme']``.
|
|
279
|
+
|
|
280
|
+
It then calls :func:`~wuttaweb.util.get_theme_template_path()` and
|
|
281
|
+
will update ``settings['mako.directories']`` such that the theme's
|
|
282
|
+
template path is listed first.
|
|
283
|
+
"""
|
|
284
|
+
config = settings["wutta_config"]
|
|
285
|
+
|
|
286
|
+
theme = get_effective_theme(config)
|
|
287
|
+
settings["wuttaweb.theme"] = theme
|
|
288
|
+
|
|
289
|
+
directories = settings["mako.directories"]
|
|
290
|
+
if isinstance(directories, str):
|
|
291
|
+
directories = config.parse_list(directories)
|
|
292
|
+
|
|
293
|
+
path = get_theme_template_path(config)
|
|
294
|
+
directories.insert(0, path)
|
|
295
|
+
settings["mako.directories"] = directories
|
wuttaweb/auth.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# -*- coding: utf-8; -*-
|
|
2
|
+
################################################################################
|
|
3
|
+
#
|
|
4
|
+
# wuttaweb -- Web App for Wutta Framework
|
|
5
|
+
# Copyright © 2024-2025 Lance Edgar
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Wutta Framework.
|
|
8
|
+
#
|
|
9
|
+
# Wutta Framework is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU General Public License as published by the Free
|
|
11
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
12
|
+
# later version.
|
|
13
|
+
#
|
|
14
|
+
# Wutta Framework is distributed in the hope that it will be useful, but
|
|
15
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
17
|
+
# more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License along with
|
|
20
|
+
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
################################################################################
|
|
23
|
+
"""
|
|
24
|
+
Auth Utility Logic
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from pyramid.authentication import SessionAuthenticationHelper
|
|
28
|
+
from pyramid.request import RequestLocalCache
|
|
29
|
+
from pyramid.security import remember, forget
|
|
30
|
+
|
|
31
|
+
from wuttaweb.db import Session
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def login_user(request, user):
|
|
35
|
+
"""
|
|
36
|
+
Perform the steps necessary to "login" the given user. This
|
|
37
|
+
returns a ``headers`` dict which you should pass to the final
|
|
38
|
+
redirect, like so::
|
|
39
|
+
|
|
40
|
+
from pyramid.httpexceptions import HTTPFound
|
|
41
|
+
|
|
42
|
+
headers = login_user(request, user)
|
|
43
|
+
return HTTPFound(location='/', headers=headers)
|
|
44
|
+
|
|
45
|
+
.. warning::
|
|
46
|
+
|
|
47
|
+
This logic does not "authenticate" the user! It assumes caller
|
|
48
|
+
has already authenticated the user and they are safe to login.
|
|
49
|
+
|
|
50
|
+
See also :func:`logout_user()`.
|
|
51
|
+
"""
|
|
52
|
+
headers = remember(request, user.uuid)
|
|
53
|
+
return headers
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def logout_user(request):
|
|
57
|
+
"""
|
|
58
|
+
Perform the logout action for the given request. This returns a
|
|
59
|
+
``headers`` dict which you should pass to the final redirect, like
|
|
60
|
+
so::
|
|
61
|
+
|
|
62
|
+
from pyramid.httpexceptions import HTTPFound
|
|
63
|
+
|
|
64
|
+
headers = logout_user(request)
|
|
65
|
+
return HTTPFound(location='/', headers=headers)
|
|
66
|
+
|
|
67
|
+
See also :func:`login_user()`.
|
|
68
|
+
"""
|
|
69
|
+
request.session.delete()
|
|
70
|
+
request.session.invalidate()
|
|
71
|
+
headers = forget(request)
|
|
72
|
+
return headers
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class WuttaSecurityPolicy:
|
|
76
|
+
"""
|
|
77
|
+
Pyramid :term:`security policy` for WuttaWeb.
|
|
78
|
+
|
|
79
|
+
For more on the Pyramid details, see :doc:`pyramid:narr/security`.
|
|
80
|
+
|
|
81
|
+
But the idea here is that you should be able to just use this,
|
|
82
|
+
without thinking too hard::
|
|
83
|
+
|
|
84
|
+
from pyramid.config import Configurator
|
|
85
|
+
from wuttaweb.auth import WuttaSecurityPolicy
|
|
86
|
+
|
|
87
|
+
pyramid_config = Configurator()
|
|
88
|
+
pyramid_config.set_security_policy(WuttaSecurityPolicy())
|
|
89
|
+
|
|
90
|
+
This security policy will then do the following:
|
|
91
|
+
|
|
92
|
+
* use the request "web session" for auth storage (e.g. current
|
|
93
|
+
``user.uuid``)
|
|
94
|
+
* check permissions as needed, by calling
|
|
95
|
+
:meth:`~wuttjamaican:wuttjamaican.auth.AuthHandler.has_permission()`
|
|
96
|
+
for current user
|
|
97
|
+
|
|
98
|
+
:param db_session: Optional :term:`db session` to use, instead of
|
|
99
|
+
:class:`wuttaweb.db.sess.Session`. Probably only useful for
|
|
100
|
+
tests.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, db_session=None):
|
|
104
|
+
self.session_helper = SessionAuthenticationHelper()
|
|
105
|
+
self.identity_cache = RequestLocalCache(self.load_identity)
|
|
106
|
+
self.db_session = db_session or Session()
|
|
107
|
+
|
|
108
|
+
def load_identity(self, request): # pylint: disable=empty-docstring
|
|
109
|
+
""" """
|
|
110
|
+
config = request.registry.settings["wutta_config"]
|
|
111
|
+
app = config.get_app()
|
|
112
|
+
model = app.model
|
|
113
|
+
|
|
114
|
+
# fetch user uuid from current session
|
|
115
|
+
uuid = self.session_helper.authenticated_userid(request)
|
|
116
|
+
if not uuid:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
# fetch user object from db
|
|
120
|
+
user = self.db_session.get(model.User, uuid)
|
|
121
|
+
if not user:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
return user
|
|
125
|
+
|
|
126
|
+
def identity(self, request): # pylint: disable=empty-docstring
|
|
127
|
+
""" """
|
|
128
|
+
return self.identity_cache.get_or_create(request)
|
|
129
|
+
|
|
130
|
+
def authenticated_userid(self, request): # pylint: disable=empty-docstring
|
|
131
|
+
""" """
|
|
132
|
+
user = self.identity(request)
|
|
133
|
+
if user is not None:
|
|
134
|
+
return user.uuid
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def remember(self, request, userid, **kw): # pylint: disable=empty-docstring
|
|
138
|
+
""" """
|
|
139
|
+
return self.session_helper.remember(request, userid, **kw)
|
|
140
|
+
|
|
141
|
+
def forget(self, request, **kw): # pylint: disable=empty-docstring
|
|
142
|
+
""" """
|
|
143
|
+
return self.session_helper.forget(request, **kw)
|
|
144
|
+
|
|
145
|
+
def permits( # pylint: disable=unused-argument,empty-docstring
|
|
146
|
+
self, request, context, permission
|
|
147
|
+
):
|
|
148
|
+
""" """
|
|
149
|
+
|
|
150
|
+
# nb. root user can do anything
|
|
151
|
+
if getattr(request, "is_root", False):
|
|
152
|
+
return True
|
|
153
|
+
|
|
154
|
+
config = request.registry.settings["wutta_config"]
|
|
155
|
+
app = config.get_app()
|
|
156
|
+
auth = app.get_auth_handler()
|
|
157
|
+
user = self.identity(request)
|
|
158
|
+
return auth.has_permission(self.db_session, user, permission)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def add_permission_group(pyramid_config, groupkey, label=None, overwrite=True):
|
|
162
|
+
"""
|
|
163
|
+
Pyramid directive to add a "permission group" to the app's
|
|
164
|
+
awareness.
|
|
165
|
+
|
|
166
|
+
The app must be made aware of all permissions, so they are exposed
|
|
167
|
+
when editing a
|
|
168
|
+
:class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic
|
|
169
|
+
for discovering permissions is in
|
|
170
|
+
:meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
|
|
171
|
+
|
|
172
|
+
This is usually called from within a master view's
|
|
173
|
+
:meth:`~wuttaweb.views.master.MasterView.defaults()` to establish
|
|
174
|
+
the permission group which applies to the view model.
|
|
175
|
+
|
|
176
|
+
A simple example of usage::
|
|
177
|
+
|
|
178
|
+
pyramid_config.add_permission_group('widgets', label="Widgets")
|
|
179
|
+
|
|
180
|
+
:param groupkey: Unique key for the permission group. In the
|
|
181
|
+
context of a master view, this will be the same as
|
|
182
|
+
:attr:`~wuttaweb.views.master.MasterView.permission_prefix`.
|
|
183
|
+
|
|
184
|
+
:param label: Optional label for the permission group. If not
|
|
185
|
+
specified, it is derived from ``groupkey``.
|
|
186
|
+
|
|
187
|
+
:param overwrite: If the permission group was already established,
|
|
188
|
+
this flag controls whether the group's label should be
|
|
189
|
+
overwritten (with ``label``).
|
|
190
|
+
|
|
191
|
+
See also :func:`add_permission()`.
|
|
192
|
+
"""
|
|
193
|
+
config = pyramid_config.get_settings()["wutta_config"]
|
|
194
|
+
app = config.get_app()
|
|
195
|
+
|
|
196
|
+
def action():
|
|
197
|
+
perms = pyramid_config.get_settings().get("wutta_permissions", {})
|
|
198
|
+
if overwrite or groupkey not in perms:
|
|
199
|
+
group = perms.setdefault(groupkey, {"key": groupkey})
|
|
200
|
+
group["label"] = label or app.make_title(groupkey)
|
|
201
|
+
pyramid_config.add_settings({"wutta_permissions": perms})
|
|
202
|
+
|
|
203
|
+
pyramid_config.action(None, action)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def add_permission(pyramid_config, groupkey, key, label=None):
|
|
207
|
+
"""
|
|
208
|
+
Pyramid directive to add a single "permission" to the app's
|
|
209
|
+
awareness.
|
|
210
|
+
|
|
211
|
+
The app must be made aware of all permissions, so they are exposed
|
|
212
|
+
when editing a
|
|
213
|
+
:class:`~wuttjamaican:wuttjamaican.db.model.auth.Role`. The logic
|
|
214
|
+
for discovering permissions is in
|
|
215
|
+
:meth:`~wuttaweb.views.roles.RoleView.get_available_permissions()`.
|
|
216
|
+
|
|
217
|
+
This is usually called from within a master view's
|
|
218
|
+
:meth:`~wuttaweb.views.master.MasterView.defaults()` to establish
|
|
219
|
+
"known" permissions based on master view feature flags
|
|
220
|
+
(:attr:`~wuttaweb.views.master.MasterView.viewable`,
|
|
221
|
+
:attr:`~wuttaweb.views.master.MasterView.editable`, etc.).
|
|
222
|
+
|
|
223
|
+
A simple example of usage::
|
|
224
|
+
|
|
225
|
+
pyramid_config.add_permission('widgets', 'widgets.polish',
|
|
226
|
+
label="Polish all the widgets")
|
|
227
|
+
|
|
228
|
+
:param groupkey: Unique key for the permission group. In the
|
|
229
|
+
context of a master view, this will be the same as
|
|
230
|
+
:attr:`~wuttaweb.views.master.MasterView.permission_prefix`.
|
|
231
|
+
|
|
232
|
+
:param key: Unique key for the permission. This should be the
|
|
233
|
+
"complete" permission name which includes the permission
|
|
234
|
+
prefix.
|
|
235
|
+
|
|
236
|
+
:param label: Optional label for the permission. If not
|
|
237
|
+
specified, it is derived from ``key``.
|
|
238
|
+
|
|
239
|
+
See also :func:`add_permission_group()`.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
def action():
|
|
243
|
+
config = pyramid_config.get_settings()["wutta_config"]
|
|
244
|
+
app = config.get_app()
|
|
245
|
+
perms = pyramid_config.get_settings().get("wutta_permissions", {})
|
|
246
|
+
group = perms.setdefault(groupkey, {"key": groupkey})
|
|
247
|
+
group.setdefault("label", app.make_title(groupkey))
|
|
248
|
+
perm = group.setdefault("perms", {}).setdefault(key, {"key": key})
|
|
249
|
+
perm["label"] = label or app.make_title(key)
|
|
250
|
+
pyramid_config.add_settings({"wutta_permissions": perms})
|
|
251
|
+
|
|
252
|
+
pyramid_config.action(None, action)
|
wuttaweb/cleanup.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# -*- coding: utf-8; -*-
|
|
2
|
+
################################################################################
|
|
3
|
+
#
|
|
4
|
+
# wuttaweb -- Web App for Wutta Framework
|
|
5
|
+
# Copyright © 2024-2026 Lance Edgar
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Wutta Framework.
|
|
8
|
+
#
|
|
9
|
+
# Wutta Framework is free software: you can redistribute it and/or modify it
|
|
10
|
+
# under the terms of the GNU General Public License as published by the Free
|
|
11
|
+
# Software Foundation, either version 3 of the License, or (at your option) any
|
|
12
|
+
# later version.
|
|
13
|
+
#
|
|
14
|
+
# Wutta Framework is distributed in the hope that it will be useful, but
|
|
15
|
+
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
17
|
+
# more details.
|
|
18
|
+
#
|
|
19
|
+
# You should have received a copy of the GNU General Public License along with
|
|
20
|
+
# Wutta Framework. If not, see <http://www.gnu.org/licenses/>.
|
|
21
|
+
#
|
|
22
|
+
################################################################################
|
|
23
|
+
"""
|
|
24
|
+
Cleanup for WuttaWeb
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import logging
|
|
29
|
+
import time
|
|
30
|
+
|
|
31
|
+
from wuttjamaican.cleanup import Cleaner
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
log = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class WuttaWebCleaner(Cleaner):
|
|
38
|
+
"""
|
|
39
|
+
This :term:`cleaner` removes old Beaker session files.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def cleanup(self, dry_run=False):
|
|
43
|
+
"""
|
|
44
|
+
This first calls :meth:`get_beaker_session_dir()` and then
|
|
45
|
+
iterates over the contents, looking for files older than the
|
|
46
|
+
cutoff (default is 30 days). Such files will be deleted
|
|
47
|
+
unless ``dry_mode`` is specified.
|
|
48
|
+
"""
|
|
49
|
+
session_dir = self.get_beaker_session_dir()
|
|
50
|
+
if not session_dir:
|
|
51
|
+
log.debug("Beaker session folder could not be located")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
data_dir = os.path.join(session_dir, "data")
|
|
55
|
+
lock_dir = os.path.join(session_dir, "lock")
|
|
56
|
+
|
|
57
|
+
# looking for files older than X days
|
|
58
|
+
days = self.config.get_int(
|
|
59
|
+
f"{self.config.appname}.cleanup.wuttaweb.beaker_session_cutoff_days",
|
|
60
|
+
default=30,
|
|
61
|
+
)
|
|
62
|
+
cutoff = time.time() - 3600 * 24 * days
|
|
63
|
+
|
|
64
|
+
for topdir in (data_dir, lock_dir):
|
|
65
|
+
if not os.path.isdir(topdir):
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
for ( # pylint: disable=unused-variable
|
|
69
|
+
dirpath,
|
|
70
|
+
dirnames,
|
|
71
|
+
filenames,
|
|
72
|
+
) in os.walk(topdir):
|
|
73
|
+
for fname in filenames:
|
|
74
|
+
path = os.path.join(dirpath, fname)
|
|
75
|
+
ts = os.path.getmtime(path)
|
|
76
|
+
if ts <= cutoff:
|
|
77
|
+
if dry_run:
|
|
78
|
+
log.debug("would delete file: %s", path)
|
|
79
|
+
else:
|
|
80
|
+
os.remove(path)
|
|
81
|
+
log.debug("deleted file: %s", path)
|
|
82
|
+
|
|
83
|
+
def get_beaker_session_dir(self):
|
|
84
|
+
"""
|
|
85
|
+
Retrieve the path to Beaker session storage folder.
|
|
86
|
+
|
|
87
|
+
By default this will be at ``venv/app/cache/sessions`` but it
|
|
88
|
+
can be overridden via config.
|
|
89
|
+
|
|
90
|
+
:returns: Path as string, or ``None`` if it could not be
|
|
91
|
+
determined.
|
|
92
|
+
"""
|
|
93
|
+
if path := self.config.get(
|
|
94
|
+
f"{self.config.appname}.cleanup.wuttaweb.beaker_session_dir"
|
|
95
|
+
):
|
|
96
|
+
return path
|
|
97
|
+
|
|
98
|
+
path = self.app.get_appdir("cache", "sessions")
|
|
99
|
+
if os.path.isdir(path):
|
|
100
|
+
return path
|
|
101
|
+
|
|
102
|
+
return None
|