tina4-python 3.13.105__tar.gz → 3.13.107__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.
- {tina4_python-3.13.105 → tina4_python-3.13.107}/PKG-INFO +1 -1
- {tina4_python-3.13.105 → tina4_python-3.13.107}/pyproject.toml +1 -1
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/__init__.py +7 -2
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/router.py +144 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/server.py +16 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/websocket/__init__.py +33 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/.gitignore +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/README.md +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/CLAUDE.md +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/HtmlElement.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/Testing.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/ai/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/ai/client.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/api/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/auth/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/cache/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/cli/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/container/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/context/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/context/chunker.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/cache.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/constants.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/events.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/middleware.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/port_takeover.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/rate_limiter.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/request.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/core/response.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/crud/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/adapter.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/connection.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/database_url.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/firebird.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/mongodb.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/mssql.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/mysql.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/odbc.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/postgres.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/sql_translator.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/database/sqlite.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/debug/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/debug/error_overlay.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/dev_admin/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/dev_admin/metrics.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/dev_admin/plan.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/dev_admin/project_index.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/docs.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/docstore/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/dotenv/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/env.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/frond/FROND.md +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/frond/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/frond/compiler.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/frond/engine.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/frond/parser.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/auth/meta.json +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/database/meta.json +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/error-overlay/meta.json +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/orm/meta.json +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/queue/meta.json +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/rest-api/meta.json +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/templates/meta.json +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/graphql/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/i18n/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/mcp/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/mcp/protocol.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/mcp/tools.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/messenger/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/migration/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/migration/runner.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/mqtt/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/mqtt/message.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/orm/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/orm/fields.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/orm/model.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/orm/point.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/__feedback/widget.js +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/css/tina4.css +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/css/tina4.min.css +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/favicon.ico +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/images/logo.svg +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/js/frond.js +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/js/frond.min.js +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/js/tina4.min.js +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/js/tina4js.min.js +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/swagger/index.html +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/query_builder/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue/amqp_url.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue/job.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue/kafka_backend.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue/lite_backend.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue/mongo_backend.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue_backends/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue_backends/kafka_backend.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue_backends/mongo_backend.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/realtime/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/realtime/models/Attachment.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/realtime/models/Channel.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/realtime/models/ChannelMember.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/realtime/models/Message.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/realtime/models/Workspace.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/realtime/models/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/realtime/storage.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/seeder/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/service/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/session/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/session_handlers/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/session_handlers/memcached_handler.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/session_handlers/mongodb_handler.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/session_handlers/redis_handler.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/session_handlers/valkey_handler.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/sso.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/swagger/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/components/crud.twig +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/docker/python/Dockerfile +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/docker/uv/Dockerfile +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/errors/302.twig +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/errors/401.twig +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/errors/403.twig +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/errors/404.twig +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/errors/500.twig +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/errors/502.twig +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/errors/503.twig +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/errors/base.twig +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/frontend/README.md +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/readme.md +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/test/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/test_client/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/validator/__init__.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/websocket/backplane.py +0 -0
- {tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/wsdl/__init__.py +0 -0
|
@@ -51,7 +51,7 @@ def _resolve_version() -> str:
|
|
|
51
51
|
#
|
|
52
52
|
# test_version_constant.py now asserts this literal equals the pyproject
|
|
53
53
|
# version, so the release bump cannot leave it behind again.
|
|
54
|
-
return "3.13.
|
|
54
|
+
return "3.13.107"
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
__version__ = _resolve_version()
|
|
@@ -71,9 +71,14 @@ __version__ = _resolve_version()
|
|
|
71
71
|
# ── CORE (always eager) — every app needs these to boot + serve ──
|
|
72
72
|
from tina4_python.core.router import ( # noqa: E402, F401
|
|
73
73
|
get, post, put, patch, delete, any_method,
|
|
74
|
-
noauth, secured, cached, middleware, template,
|
|
74
|
+
noauth, secured, role, can, cached, middleware, template,
|
|
75
75
|
Router, RouteGroup,
|
|
76
76
|
)
|
|
77
|
+
# websocket is not in that tuple. The tina4_python.websocket *subpackage* is
|
|
78
|
+
# itself callable (see tina4_python/websocket/__init__.py) so
|
|
79
|
+
# `from tina4_python import websocket` reaches core.router.websocket without
|
|
80
|
+
# turning `import tina4_python.websocket as ws` into a function. A re-export
|
|
81
|
+
# here would do the latter.
|
|
77
82
|
from tina4_python.core.constants import ( # noqa: E402, F401
|
|
78
83
|
HTTP_OK, HTTP_CREATED, HTTP_ACCEPTED, HTTP_NO_CONTENT,
|
|
79
84
|
HTTP_MOVED, HTTP_REDIRECT, HTTP_NOT_MODIFIED,
|
|
@@ -50,6 +50,24 @@ class RouteRef:
|
|
|
50
50
|
self._route["auth_required"] = False
|
|
51
51
|
return self
|
|
52
52
|
|
|
53
|
+
def role(self, *names):
|
|
54
|
+
"""Require the caller to hold ONE of the named roles (OR). Reads the
|
|
55
|
+
verified JWT ``roles`` claim. Implies auth (a guarded GET still needs a
|
|
56
|
+
token). Stack ``.role()``/``.can()`` for AND. Feature 138 / ADR-0058."""
|
|
57
|
+
if names:
|
|
58
|
+
self._route.setdefault("required_roles", []).append(tuple(names))
|
|
59
|
+
self._route["auth_required"] = True
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def can(self, *permissions):
|
|
63
|
+
"""Require the caller to hold ONE of the named permissions (OR). Reads
|
|
64
|
+
the verified JWT ``permissions`` claim; granted-side wildcards (``posts.*``,
|
|
65
|
+
``*``) satisfy a concrete requirement. Implies auth. Feature 138."""
|
|
66
|
+
if permissions:
|
|
67
|
+
self._route.setdefault("required_perms", []).append(tuple(permissions))
|
|
68
|
+
self._route["auth_required"] = True
|
|
69
|
+
return self
|
|
70
|
+
|
|
53
71
|
def cache(self):
|
|
54
72
|
"""Mark this route as cacheable."""
|
|
55
73
|
self._route["cached"] = True
|
|
@@ -371,6 +389,12 @@ class Router:
|
|
|
371
389
|
auth_required = False
|
|
372
390
|
elif hasattr(handler, "_secured"):
|
|
373
391
|
auth_required = True
|
|
392
|
+
elif hasattr(handler, "_required_roles") or hasattr(handler, "_required_perms"):
|
|
393
|
+
# A role/permission guard (Feature 138) implies auth — a guarded GET
|
|
394
|
+
# still requires a token. This branch only fires in the unusual
|
|
395
|
+
# innermost-order case (guard BELOW @get); the documented order puts
|
|
396
|
+
# the guard ABOVE, where it flips auth_required via the RouteRef.
|
|
397
|
+
auth_required = True
|
|
374
398
|
else:
|
|
375
399
|
# GET, HEAD, OPTIONS, and ANY are public by default. HEAD and
|
|
376
400
|
# OPTIONS are safe/idempotent introspection methods (RFC 9110
|
|
@@ -392,6 +416,11 @@ class Router:
|
|
|
392
416
|
"module": getattr(handler, "__module__", ""),
|
|
393
417
|
"middleware": effective_middleware,
|
|
394
418
|
"auth_required": auth_required,
|
|
419
|
+
# RBAC guards (Feature 138): lists of OR-groups; AND across groups.
|
|
420
|
+
# Populated here for the innermost-order case, and appended to by the
|
|
421
|
+
# RouteRef.role()/.can() modifiers for the documented guard-above order.
|
|
422
|
+
"required_roles": list(getattr(handler, "_required_roles", []) or []),
|
|
423
|
+
"required_perms": list(getattr(handler, "_required_perms", []) or []),
|
|
395
424
|
"cached": options.get("cached", False),
|
|
396
425
|
"cache_max_age": options.get("cache_max_age", 60),
|
|
397
426
|
"swagger_meta": swagger_meta or options.get("swagger_meta", {}),
|
|
@@ -862,6 +891,121 @@ def secured():
|
|
|
862
891
|
return decorator
|
|
863
892
|
|
|
864
893
|
|
|
894
|
+
# ── RBAC guards (Feature 138 / ADR-0058) ──────────────────────
|
|
895
|
+
# Claim-first authorization on top of the JWT auth gate. @role reads the verified
|
|
896
|
+
# `roles` claim, @can reads `permissions`. Multiple args are OR; stack guards for
|
|
897
|
+
# AND. A guard implies @secured. Roles and permissions are independent claims;
|
|
898
|
+
# the core never expands a role into permissions.
|
|
899
|
+
|
|
900
|
+
def role(*names):
|
|
901
|
+
"""Require the caller to hold ONE of the named roles (OR). Stack
|
|
902
|
+
``@role``/``@can`` for AND. Reads the verified JWT ``roles`` claim (a legacy
|
|
903
|
+
singular ``role`` string is coerced). Implies ``@secured``."""
|
|
904
|
+
group = tuple(names)
|
|
905
|
+
|
|
906
|
+
def decorator(fn):
|
|
907
|
+
fn._required_roles = getattr(fn, "_required_roles", []) + [group]
|
|
908
|
+
if hasattr(fn, "_route_ref"):
|
|
909
|
+
route = fn._route_ref._route
|
|
910
|
+
route.setdefault("required_roles", []).append(group)
|
|
911
|
+
was_public = not route.get("auth_required", True)
|
|
912
|
+
route["auth_required"] = True
|
|
913
|
+
if was_public:
|
|
914
|
+
Log.debug(
|
|
915
|
+
f"Route auth updated: {route['method']} {route['path']} "
|
|
916
|
+
f"(auth=required via @role)"
|
|
917
|
+
)
|
|
918
|
+
return fn
|
|
919
|
+
return decorator
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def can(*permissions):
|
|
923
|
+
"""Require the caller to hold ONE of the named permissions (OR). Stack
|
|
924
|
+
``@role``/``@can`` for AND. Reads the verified JWT ``permissions`` claim;
|
|
925
|
+
granted-side wildcards (``posts.*``, ``*``) satisfy a concrete requirement.
|
|
926
|
+
Implies ``@secured``."""
|
|
927
|
+
group = tuple(permissions)
|
|
928
|
+
|
|
929
|
+
def decorator(fn):
|
|
930
|
+
fn._required_perms = getattr(fn, "_required_perms", []) + [group]
|
|
931
|
+
if hasattr(fn, "_route_ref"):
|
|
932
|
+
route = fn._route_ref._route
|
|
933
|
+
route.setdefault("required_perms", []).append(group)
|
|
934
|
+
was_public = not route.get("auth_required", True)
|
|
935
|
+
route["auth_required"] = True
|
|
936
|
+
if was_public:
|
|
937
|
+
Log.debug(
|
|
938
|
+
f"Route auth updated: {route['method']} {route['path']} "
|
|
939
|
+
f"(auth=required via @can)"
|
|
940
|
+
)
|
|
941
|
+
return fn
|
|
942
|
+
return decorator
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def _rbac_claim_list(subject, key, legacy=None):
|
|
946
|
+
"""Read a claim as a list of strings from the VERIFIED payload. Coerces a
|
|
947
|
+
legacy singular string (``role`` -> ``["role"]``). Returns ``[]`` for a
|
|
948
|
+
missing/None subject or claim."""
|
|
949
|
+
if not isinstance(subject, dict):
|
|
950
|
+
return []
|
|
951
|
+
val = subject.get(key)
|
|
952
|
+
out = []
|
|
953
|
+
if isinstance(val, str) and val:
|
|
954
|
+
out = [val]
|
|
955
|
+
elif isinstance(val, (list, tuple)):
|
|
956
|
+
out = [str(x) for x in val if x is not None and str(x) != ""]
|
|
957
|
+
if not out and legacy:
|
|
958
|
+
lv = subject.get(legacy)
|
|
959
|
+
if isinstance(lv, str) and lv:
|
|
960
|
+
out = [lv]
|
|
961
|
+
elif isinstance(lv, (list, tuple)):
|
|
962
|
+
out = [str(x) for x in lv if x is not None and str(x) != ""]
|
|
963
|
+
return out
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _rbac_perm_granted(granted, required):
|
|
967
|
+
"""True if any GRANTED permission satisfies the concrete REQUIRED permission.
|
|
968
|
+
Wildcards live only on the granted side: ``*`` grants everything; ``posts.*``
|
|
969
|
+
grants ``posts.<anything...>`` on the dot boundary (never ``users.delete``)."""
|
|
970
|
+
for g in granted:
|
|
971
|
+
if g == "*" or g == required:
|
|
972
|
+
return True
|
|
973
|
+
if g.endswith(".*") and required.startswith(g[:-1]):
|
|
974
|
+
return True
|
|
975
|
+
return False
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def rbac_authorized(subject, required_roles, required_perms):
|
|
979
|
+
"""Return True if the verified ``subject`` satisfies every guard group.
|
|
980
|
+
|
|
981
|
+
AND across groups, OR within a group. ``required_roles`` / ``required_perms``
|
|
982
|
+
are lists of OR-groups (each ``@role``/``@can`` adds one group). Roles read
|
|
983
|
+
the ``roles`` claim (legacy singular ``role`` coerced); permissions read
|
|
984
|
+
``permissions`` with granted-side wildcards. A missing subject satisfies no
|
|
985
|
+
group. Feature 138 / ADR-0058."""
|
|
986
|
+
# NOTE: explicit loops, NOT any(...): this module shadows the builtin `any`
|
|
987
|
+
# (a route helper), so `any(...)` here would resolve to the wrong callable.
|
|
988
|
+
roles = _rbac_claim_list(subject, "roles", legacy="role")
|
|
989
|
+
for group in (required_roles or []):
|
|
990
|
+
matched = False
|
|
991
|
+
for r in group:
|
|
992
|
+
if r in roles:
|
|
993
|
+
matched = True
|
|
994
|
+
break
|
|
995
|
+
if not matched:
|
|
996
|
+
return False
|
|
997
|
+
perms = _rbac_claim_list(subject, "permissions")
|
|
998
|
+
for group in (required_perms or []):
|
|
999
|
+
matched = False
|
|
1000
|
+
for req in group:
|
|
1001
|
+
if _rbac_perm_granted(perms, req):
|
|
1002
|
+
matched = True
|
|
1003
|
+
break
|
|
1004
|
+
if not matched:
|
|
1005
|
+
return False
|
|
1006
|
+
return True
|
|
1007
|
+
|
|
1008
|
+
|
|
865
1009
|
# ── Middleware Decorator ───────────────────────────────────────
|
|
866
1010
|
|
|
867
1011
|
def middleware(*middleware_classes):
|
|
@@ -1804,6 +1804,22 @@ def _check_auth(request: Request, response: Response, route: dict) -> bool:
|
|
|
1804
1804
|
"status": 401,
|
|
1805
1805
|
})
|
|
1806
1806
|
return True
|
|
1807
|
+
# ── RBAC guards (Feature 138): authorization AFTER authentication ──
|
|
1808
|
+
# Auth passed (401 already ruled out above). If the route carries role/
|
|
1809
|
+
# permission guards, the verified payload (request.user) must satisfy them,
|
|
1810
|
+
# else 403 — authenticated but forbidden. An API-key auth leaves request.user
|
|
1811
|
+
# unset, so it carries no roles and is correctly forbidden from a guarded route.
|
|
1812
|
+
_req_roles = route.get("required_roles") or []
|
|
1813
|
+
_req_perms = route.get("required_perms") or []
|
|
1814
|
+
if _req_roles or _req_perms:
|
|
1815
|
+
from tina4_python.core.router import rbac_authorized
|
|
1816
|
+
if not rbac_authorized(getattr(request, "user", None), _req_roles, _req_perms):
|
|
1817
|
+
response.status(403).json({
|
|
1818
|
+
"error": "Forbidden",
|
|
1819
|
+
"message": "You do not have permission to access this resource",
|
|
1820
|
+
"status": 403,
|
|
1821
|
+
})
|
|
1822
|
+
return True
|
|
1807
1823
|
return False
|
|
1808
1824
|
|
|
1809
1825
|
|
|
@@ -22,6 +22,8 @@ import os
|
|
|
22
22
|
import uuid
|
|
23
23
|
import time
|
|
24
24
|
from typing import Callable
|
|
25
|
+
import sys as _sys
|
|
26
|
+
from types import ModuleType as _ModuleType
|
|
25
27
|
|
|
26
28
|
MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
27
29
|
|
|
@@ -961,3 +963,34 @@ __all__ = [
|
|
|
961
963
|
"OP_TEXT", "OP_BINARY", "OP_CLOSE", "OP_PING", "OP_PONG",
|
|
962
964
|
"CLOSE_NORMAL", "CLOSE_GOING_AWAY", "CLOSE_PROTOCOL_ERROR", "CLOSE_TOO_LARGE",
|
|
963
965
|
]
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
# ── `@websocket("/path")` on the package name ───────────────────────────────
|
|
969
|
+
# `tina4_python.websocket` is two things at once: this RFC 6455 subpackage,
|
|
970
|
+
# and — to every reader of the docs — the route decorator beside @get/@post.
|
|
971
|
+
# Python binds a submodule onto its parent as soon as the submodule loads, so
|
|
972
|
+
#
|
|
973
|
+
# from tina4_python import websocket
|
|
974
|
+
# @websocket("/ws") # TypeError: 'module' object is not callable
|
|
975
|
+
#
|
|
976
|
+
# died at decorate time and auto-discovery dropped every route from that line
|
|
977
|
+
# onward.
|
|
978
|
+
#
|
|
979
|
+
# A re-export in tina4_python/__init__.py AFTER core.server has already loaded
|
|
980
|
+
# this subpackage is stable (importlib setattr happens on first load only).
|
|
981
|
+
# That still fails the other public surface: `import tina4_python.websocket as
|
|
982
|
+
# ws` would then return the function, so `ws.WebSocketServer` breaks. Making
|
|
983
|
+
# this module callable keeps both: the package name decorates, and the
|
|
984
|
+
# subpackage stays a module. Forward *args/**kwargs so this spelling cannot
|
|
985
|
+
# drift from core.router.websocket. Guard sys.modules — a loader that execs
|
|
986
|
+
# this file without registering it must not KeyError.
|
|
987
|
+
class _CallableWebSocketModule(_ModuleType):
|
|
988
|
+
def __call__(self, *args, **kwargs):
|
|
989
|
+
from tina4_python.core.router import websocket as _websocket_route
|
|
990
|
+
|
|
991
|
+
return _websocket_route(*args, **kwargs)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
_self = _sys.modules.get(__name__)
|
|
995
|
+
if _self is not None:
|
|
996
|
+
_self.__class__ = _CallableWebSocketModule
|
|
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
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/gallery/error-overlay/meta.json
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
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/images/tina4-logo-icon.webp
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/js/tina4-dev-admin.min.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/public/swagger/oauth2-redirect.html
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
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue_backends/kafka_backend.py
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue_backends/mongo_backend.py
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/queue_backends/rabbitmq_backend.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/realtime/models/ChannelMember.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
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/session_handlers/memcached_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/session_handlers/mongodb_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/session_handlers/redis_handler.py
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/session_handlers/valkey_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/docker/distroless/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/docker/poetry/Dockerfile
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/templates/docker/python/Dockerfile
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
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/af/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/af/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/en/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/en/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/es/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/es/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/fr/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/fr/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/ja/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/ja/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/zh/LC_MESSAGES/messages.mo
RENAMED
|
File without changes
|
{tina4_python-3.13.105 → tina4_python-3.13.107}/tina4_python/translations/zh/LC_MESSAGES/messages.po
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|