keepup-admin 0.3.0__tar.gz → 0.4.0__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.
- {keepup_admin-0.3.0/keepup_admin.egg-info → keepup_admin-0.4.0}/PKG-INFO +31 -9
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/README.md +5 -4
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/__init__.py +6 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/audit.py +4 -29
- keepup_admin-0.4.0/builtin/audit.py +107 -0
- keepup_admin-0.4.0/builtin/auth.py +100 -0
- keepup_admin-0.4.0/builtin/db.py +172 -0
- keepup_admin-0.4.0/builtin/integration_logs.py +297 -0
- keepup_admin-0.4.0/builtin/metrics.py +226 -0
- keepup_admin-0.4.0/builtin/postgres.py +70 -0
- keepup_admin-0.4.0/builtin/sqlite.py +68 -0
- keepup_admin-0.4.0/builtin/ui.py +146 -0
- keepup_admin-0.4.0/builtin/users.py +137 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/cluster.py +13 -1
- keepup_admin-0.4.0/compat.py +270 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/factory.py +59 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/integrations.py +4 -1
- {keepup_admin-0.3.0 → keepup_admin-0.4.0/keepup_admin.egg-info}/PKG-INFO +31 -9
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/keepup_admin.egg-info/SOURCES.txt +46 -72
- keepup_admin-0.4.0/keepup_admin.egg-info/requires.txt +55 -0
- keepup_admin-0.4.0/kernel/__init__.py +120 -0
- keepup_admin-0.4.0/kernel/call.py +304 -0
- keepup_admin-0.4.0/kernel/catalogue.py +243 -0
- keepup_admin-0.4.0/kernel/contributions.py +317 -0
- keepup_admin-0.4.0/kernel/datasource.py +95 -0
- keepup_admin-0.4.0/kernel/descriptor.py +209 -0
- keepup_admin-0.4.0/kernel/lifecycle.py +814 -0
- keepup_admin-0.4.0/kernel/loader.py +240 -0
- keepup_admin-0.4.0/kernel/observability.py +127 -0
- keepup_admin-0.4.0/kernel/security.py +113 -0
- keepup_admin-0.4.0/kernel/services.py +267 -0
- keepup_admin-0.4.0/kernel/transports.py +119 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/locks.py +8 -1
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/metrics.py +1 -1
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/metrics_api.py +4 -1
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/metrics_retention.py +3 -1
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/modules.py +10 -1
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/notification_bus.py +3 -1
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/admin.py +3 -1
- keepup_admin-0.4.0/plugins/builtin.json +283 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/registry.py +81 -44
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/routes.py +148 -117
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/pyproject.toml +72 -21
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/retention.py +4 -1
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/schema.py +19 -124
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/settings.py +11 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/themes.py +4 -16
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/web.py +8 -1
- keepup_admin-0.3.0/auth/__init__.py +0 -7
- keepup_admin-0.3.0/auth/config.py +0 -128
- keepup_admin-0.3.0/auth/dependencies.py +0 -556
- keepup_admin-0.3.0/auth/dto/__init__.py +0 -0
- keepup_admin-0.3.0/auth/dto/token.py +0 -31
- keepup_admin-0.3.0/auth/external_accounts.py +0 -144
- keepup_admin-0.3.0/auth/factory.py +0 -28
- keepup_admin-0.3.0/auth/identity/__init__.py +0 -33
- keepup_admin-0.3.0/auth/identity/access.py +0 -119
- keepup_admin-0.3.0/auth/identity/accounts.py +0 -86
- keepup_admin-0.3.0/auth/identity/config.py +0 -220
- keepup_admin-0.3.0/auth/identity/contract.py +0 -174
- keepup_admin-0.3.0/auth/identity/loader.py +0 -119
- keepup_admin-0.3.0/auth/identity/runtime.py +0 -265
- keepup_admin-0.3.0/auth/login_throttle.py +0 -181
- keepup_admin-0.3.0/auth/oidc.py +0 -255
- keepup_admin-0.3.0/auth/oidc_policy.py +0 -122
- keepup_admin-0.3.0/auth/oidc_routes.py +0 -265
- keepup_admin-0.3.0/auth/panel_session.py +0 -386
- keepup_admin-0.3.0/auth/permissions.py +0 -59
- keepup_admin-0.3.0/auth/providers/__init__.py +0 -0
- keepup_admin-0.3.0/auth/providers/base.py +0 -216
- keepup_admin-0.3.0/auth/providers/local.py +0 -212
- keepup_admin-0.3.0/auth/routes.py +0 -514
- keepup_admin-0.3.0/auth/seed_accounts.py +0 -352
- keepup_admin-0.3.0/auth/session_lifetime.py +0 -104
- keepup_admin-0.3.0/auth/signing_key.py +0 -138
- keepup_admin-0.3.0/auth/socket_sessions.py +0 -176
- keepup_admin-0.3.0/auth/user_roles.py +0 -376
- keepup_admin-0.3.0/auth/user_routes.py +0 -313
- keepup_admin-0.3.0/auth/usernames.py +0 -36
- keepup_admin-0.3.0/auth/websocket.py +0 -88
- keepup_admin-0.3.0/db.py +0 -603
- keepup_admin-0.3.0/events.py +0 -506
- keepup_admin-0.3.0/keepup_admin.egg-info/requires.txt +0 -25
- keepup_admin-0.3.0/positional_sql.py +0 -120
- keepup_admin-0.3.0/tables.py +0 -304
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/LICENSE +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/NOTICE +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/THIRD-PARTY.md +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/admin_trail.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/api_docs.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/api_versions.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/body_limit.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/cache.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/events_api.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/instance.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/keepup_admin.egg-info/dependency_links.txt +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/keepup_admin.egg-info/top_level.txt +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/log_shipping.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/logging_setup.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/__init__.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/base.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/enablement.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/plugins/route_mask.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/roles.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/scheduler.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/sections.json +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/security.py +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/setup.cfg +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/css/aos.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/css/main_nebula.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/css/main_new.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/css/tailwind.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/index_nebula.html +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/index_new.html +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/js/aos.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/js/feather-icons.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/js/layout_bootstrap.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/js/main_new.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/js/tailwind.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/background_tasks.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/cluster.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/event_manager.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/integration_logs.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/metrics.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/modules.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/themes.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/css/users.css +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/background_tasks.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/chart.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/cluster.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/event_manager.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/integration_logs.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/metrics.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/modules.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/themes.js +0 -0
- {keepup_admin-0.3.0 → keepup_admin-0.4.0}/static/modules/js/users.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: keepup-admin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: An admin framework over FastAPI: plugins, panel, users, replicas
|
|
5
5
|
Author-email: Fedor Sergeev <fksergeev@gmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -22,10 +22,7 @@ Requires-Dist: fastapi<1,>=0.128
|
|
|
22
22
|
Requires-Dist: starlette<2,>=1.3.1
|
|
23
23
|
Requires-Dist: pydantic<3,>=2.11
|
|
24
24
|
Requires-Dist: sqlalchemy<3,>=2.0
|
|
25
|
-
Requires-Dist: psycopg2-binary>=2.9
|
|
26
25
|
Requires-Dist: apscheduler<4,>=3.10
|
|
27
|
-
Requires-Dist: bcrypt<5,>=4.0
|
|
28
|
-
Requires-Dist: pyjwt[crypto]<3,>=2.15
|
|
29
26
|
Requires-Dist: python-multipart>=0.0.31
|
|
30
27
|
Requires-Dist: PyYAML<7,>=6.0
|
|
31
28
|
Requires-Dist: psutil>=7.0
|
|
@@ -39,8 +36,32 @@ Requires-Dist: pytest>=8; extra == "test"
|
|
|
39
36
|
Requires-Dist: pytest-asyncio>=0.24; extra == "test"
|
|
40
37
|
Requires-Dist: pytest-cov>=5; extra == "test"
|
|
41
38
|
Requires-Dist: uvicorn>=0.30; extra == "test"
|
|
39
|
+
Provides-Extra: db
|
|
40
|
+
Requires-Dist: keepup-db>=0.4.0; extra == "db"
|
|
41
|
+
Provides-Extra: auth
|
|
42
|
+
Requires-Dist: keepup-auth>=0.4.0; extra == "auth"
|
|
43
|
+
Provides-Extra: users
|
|
44
|
+
Requires-Dist: keepup-users>=0.4.0; extra == "users"
|
|
45
|
+
Provides-Extra: ui
|
|
46
|
+
Requires-Dist: keepup-ui>=0.4.0; extra == "ui"
|
|
47
|
+
Provides-Extra: audit
|
|
48
|
+
Requires-Dist: keepup-audit>=0.4.0; extra == "audit"
|
|
49
|
+
Provides-Extra: metrics
|
|
50
|
+
Requires-Dist: keepup-metrics>=0.4.0; extra == "metrics"
|
|
51
|
+
Provides-Extra: integration-log
|
|
52
|
+
Requires-Dist: keepup-integration-log>=0.4.0; extra == "integration-log"
|
|
53
|
+
Provides-Extra: panel
|
|
54
|
+
Requires-Dist: keepup-db>=0.4.0; extra == "panel"
|
|
55
|
+
Requires-Dist: keepup-auth>=0.4.0; extra == "panel"
|
|
56
|
+
Requires-Dist: keepup-users>=0.4.0; extra == "panel"
|
|
57
|
+
Requires-Dist: keepup-ui>=0.4.0; extra == "panel"
|
|
58
|
+
Requires-Dist: keepup-audit>=0.4.0; extra == "panel"
|
|
59
|
+
Requires-Dist: keepup-metrics>=0.4.0; extra == "panel"
|
|
60
|
+
Requires-Dist: keepup-integration-log>=0.4.0; extra == "panel"
|
|
42
61
|
Provides-Extra: postgres
|
|
43
|
-
Requires-Dist:
|
|
62
|
+
Requires-Dist: keepup-postgres>=0.4.0; extra == "postgres"
|
|
63
|
+
Provides-Extra: sqlite
|
|
64
|
+
Requires-Dist: keepup-sqlite>=0.4.0; extra == "sqlite"
|
|
44
65
|
Dynamic: license-file
|
|
45
66
|
|
|
46
67
|
# keepup
|
|
@@ -125,16 +146,17 @@ between replicas needs.
|
|
|
125
146
|
## Tests
|
|
126
147
|
|
|
127
148
|
```bash
|
|
128
|
-
pip install -e ".[test]"
|
|
149
|
+
pip install -e ".[test]" -e packages/keepup-*
|
|
129
150
|
pytest tests/
|
|
130
151
|
```
|
|
131
152
|
|
|
132
153
|
**Install first, then test.** Pointing pytest at the checkout does not work on
|
|
133
154
|
its own: `import keepup` has to resolve, and the directory a clone lands in is
|
|
134
155
|
called `keepup-admin`, so nothing is named `keepup` until the package is
|
|
135
|
-
installed. The
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
installed. The capabilities are distributions of their own in `packages/` —
|
|
157
|
+
each carries the libraries it needs, and the suite imports them — so a clone
|
|
158
|
+
installs them from the tree; the extras name the same ones for an installation
|
|
159
|
+
from the index.
|
|
138
160
|
|
|
139
161
|
The suite runs against SQLite with no network, needs no application beside the
|
|
140
162
|
package, and holds a coverage floor declared in `pyproject.toml`. Checks that
|
|
@@ -80,16 +80,17 @@ between replicas needs.
|
|
|
80
80
|
## Tests
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
pip install -e ".[test]"
|
|
83
|
+
pip install -e ".[test]" -e packages/keepup-*
|
|
84
84
|
pytest tests/
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
**Install first, then test.** Pointing pytest at the checkout does not work on
|
|
88
88
|
its own: `import keepup` has to resolve, and the directory a clone lands in is
|
|
89
89
|
called `keepup-admin`, so nothing is named `keepup` until the package is
|
|
90
|
-
installed. The
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
installed. The capabilities are distributions of their own in `packages/` —
|
|
91
|
+
each carries the libraries it needs, and the suite imports them — so a clone
|
|
92
|
+
installs them from the tree; the extras name the same ones for an installation
|
|
93
|
+
from the index.
|
|
93
94
|
|
|
94
95
|
The suite runs against SQLite with no network, needs no application beside the
|
|
95
96
|
package, and holds a coverage floor declared in `pyproject.toml`. Checks that
|
|
@@ -26,6 +26,12 @@ except Exception:
|
|
|
26
26
|
# itself, until the applications switch to installing by version.
|
|
27
27
|
__version__ = "0.0.0+source"
|
|
28
28
|
|
|
29
|
+
# Names that moved into a distribution of their own keep working for one
|
|
30
|
+
# release, and say where they went (keepup-114, keepup-124).
|
|
31
|
+
from keepup import compat as _compat
|
|
32
|
+
|
|
33
|
+
_compat.install()
|
|
34
|
+
|
|
29
35
|
__all__ = ["KeepupSettings", "StaticMount", "create_app", "__version__"]
|
|
30
36
|
|
|
31
37
|
|
|
@@ -22,8 +22,6 @@ from contextlib import asynccontextmanager
|
|
|
22
22
|
from datetime import datetime
|
|
23
23
|
from typing import Any, Dict, Optional
|
|
24
24
|
|
|
25
|
-
from sqlalchemy import CheckConstraint, Column, DateTime, Index, Integer, Text, UniqueConstraint
|
|
26
|
-
from sqlalchemy.dialects import postgresql
|
|
27
25
|
|
|
28
26
|
from keepup import retention, tables
|
|
29
27
|
from keepup.db import DatabaseManagerV2, db_config
|
|
@@ -48,6 +46,10 @@ __all__ = [
|
|
|
48
46
|
"retention_days",
|
|
49
47
|
]
|
|
50
48
|
|
|
49
|
+
# The audit capability declares its own tables (keepup-124); re-exported here so
|
|
50
|
+
# the path that predates the catalogue still creates them.
|
|
51
|
+
from keepup_audit.tables import INCOMING_REQUESTS # noqa: E402
|
|
52
|
+
|
|
51
53
|
logger = logging.getLogger(__name__)
|
|
52
54
|
|
|
53
55
|
incoming_requests_buffer: Dict[str, Dict[str, Any]] = {}
|
|
@@ -389,33 +391,6 @@ async def audit_retention_background():
|
|
|
389
391
|
await asyncio.sleep(AUDIT_ERROR_BACKOFF_SECONDS)
|
|
390
392
|
|
|
391
393
|
|
|
392
|
-
INCOMING_REQUESTS = tables.table(
|
|
393
|
-
"incoming_requests",
|
|
394
|
-
tables.auto_id(),
|
|
395
|
-
Column("instance_id", Text, nullable=False),
|
|
396
|
-
Column("method", Text, nullable=False),
|
|
397
|
-
Column("endpoint", Text, nullable=False),
|
|
398
|
-
Column("host", Text, nullable=False),
|
|
399
|
-
Column("request_data", postgresql.JSONB().with_variant(Text(), "sqlite")),
|
|
400
|
-
Column("request_start_at", DateTime, nullable=False),
|
|
401
|
-
Column("request_end_at", DateTime),
|
|
402
|
-
Column("duration_ms", Integer),
|
|
403
|
-
Column("http_status", Integer),
|
|
404
|
-
Column("response_data", postgresql.JSONB().with_variant(Text(), "sqlite")),
|
|
405
|
-
Column("error_message", Text),
|
|
406
|
-
Column("created_at", DateTime, server_default=tables.NOW),
|
|
407
|
-
UniqueConstraint("instance_id", "method", "endpoint", "request_start_at",
|
|
408
|
-
name="incoming_requests_instance_method_endpoint_unique").ddl_if(dialect="postgresql"),
|
|
409
|
-
CheckConstraint("duration_ms >= 0",
|
|
410
|
-
name="incoming_requests_check_duration").ddl_if(dialect="postgresql"),
|
|
411
|
-
CheckConstraint("(http_status IS NULL) OR (http_status >= 100 AND http_status <= 599)",
|
|
412
|
-
name="incoming_requests_check_http_status").ddl_if(dialect="postgresql"),
|
|
413
|
-
Index("incoming_requests_instance_method_endpoint_unique",
|
|
414
|
-
"instance_id", "method", "endpoint", "request_start_at", unique=True).ddl_if(dialect="sqlite"),
|
|
415
|
-
Index("idx_incoming_requests_instance_id", "instance_id"),
|
|
416
|
-
Index("idx_incoming_requests_endpoint", "endpoint"),
|
|
417
|
-
Index("idx_incoming_requests_created_at", "created_at"),
|
|
418
|
-
)
|
|
419
394
|
|
|
420
395
|
|
|
421
396
|
def init_incoming_requests_table():
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""The audit: incoming calls, the application's events and the administrator's trail.
|
|
2
|
+
|
|
3
|
+
Task keepup-111. These live in three modules of the kernel -- ``audit.py``
|
|
4
|
+
(incoming requests, redaction, retention), ``events.py`` (the application's own
|
|
5
|
+
event log) and ``admin_trail.py`` (what an administrator decided) -- and all
|
|
6
|
+
three import SQLAlchemy, so the base bundle cannot be assembled without a
|
|
7
|
+
database library while they stay. The plugin here owns the two tables and
|
|
8
|
+
publishes the two services; the kernel's own trail and the route wrapper reach
|
|
9
|
+
them through the names (``keepup/kernel/observability.py``), not by importing
|
|
10
|
+
these modules.
|
|
11
|
+
|
|
12
|
+
The routes of the event log and the section that shows it travel with the panel
|
|
13
|
+
(keepup-104); what is here is the recording itself, its retention and the two
|
|
14
|
+
names.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, Dict, List
|
|
19
|
+
|
|
20
|
+
from keepup.kernel.datasource import SERVICE_DATASOURCE
|
|
21
|
+
from keepup.kernel.descriptor import KIND_OPTIONAL, PluginDescriptor
|
|
22
|
+
from keepup.kernel.observability import SERVICE_AUDIT, SERVICE_EVENTS, Recording
|
|
23
|
+
from keepup.audit import INCOMING_REQUESTS
|
|
24
|
+
from keepup.events import APP_EVENTS
|
|
25
|
+
from keepup.plugins.base import BasePlugin
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
__all__ = ["AuditPlugin", "AuditRecording"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuditRecording(Recording):
|
|
33
|
+
"""The framework's own recording, behind the kernel's two names.
|
|
34
|
+
|
|
35
|
+
It delegates to the modules that already do the work (``keepup.audit``,
|
|
36
|
+
``keepup.events``) so that nothing changes for a deployment today; those
|
|
37
|
+
modules travel into this capability's distribution in keepup-124.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, datasource: Any = None):
|
|
41
|
+
from keepup.audit import IncomingRequestLogger, log_api_request
|
|
42
|
+
|
|
43
|
+
super().__init__(start=log_api_request, end=IncomingRequestLogger.end_request,
|
|
44
|
+
name="keepup audit")
|
|
45
|
+
|
|
46
|
+
def watch(self, emitter: Any) -> None:
|
|
47
|
+
"""Add another emitter, for a capability that keeps events of its own."""
|
|
48
|
+
self.emitters.append(emitter)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AuditPlugin(BasePlugin):
|
|
52
|
+
"""The capability: two tables, two services and the retention they need."""
|
|
53
|
+
|
|
54
|
+
descriptor = PluginDescriptor(
|
|
55
|
+
id="audit",
|
|
56
|
+
name="Event audit",
|
|
57
|
+
version="0.4.0",
|
|
58
|
+
distribution="keepup-audit",
|
|
59
|
+
kind=KIND_OPTIONAL,
|
|
60
|
+
priority=60,
|
|
61
|
+
provides=("audit>=1", "events>=1"),
|
|
62
|
+
requires=("datasource>=1",),
|
|
63
|
+
wants=("locks>=1", "scheduler>=1"),
|
|
64
|
+
contributions=("routes", "sections", "tables", "events"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def __init__(self, config=None):
|
|
68
|
+
super().__init__("audit", "Event audit", config)
|
|
69
|
+
|
|
70
|
+
def register(self, services):
|
|
71
|
+
"""Publish the recording where there is a data source to write it to.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
services: the runtime's registry.
|
|
75
|
+
"""
|
|
76
|
+
if services.has(SERVICE_DATASOURCE):
|
|
77
|
+
self.recording = AuditRecording(services.require(SERVICE_DATASOURCE))
|
|
78
|
+
services.provide(SERVICE_AUDIT, self.recording, version=1, plugin_id="audit")
|
|
79
|
+
services.provide(SERVICE_EVENTS, self.recording, version=1, plugin_id="audit")
|
|
80
|
+
|
|
81
|
+
async def initialize(self):
|
|
82
|
+
"""Keep the recording published at registration; build one if there is none."""
|
|
83
|
+
if getattr(self, "recording", None) is None:
|
|
84
|
+
self.recording = AuditRecording(self.services.require(SERVICE_DATASOURCE))
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
def get_declared_tables(self):
|
|
88
|
+
"""The two tables this capability owns, declared once, until keepup-124."""
|
|
89
|
+
return [INCOMING_REQUESTS, APP_EVENTS]
|
|
90
|
+
|
|
91
|
+
def get_event_sinks(self) -> List[Any]:
|
|
92
|
+
"""What writes an event down, for the kernel's own trail."""
|
|
93
|
+
return [self.emit]
|
|
94
|
+
|
|
95
|
+
async def emit(self, event_type: str, payload: Dict[str, Any] = None) -> None:
|
|
96
|
+
"""Write one event through the module that still owns the table."""
|
|
97
|
+
from keepup.events import emit_event
|
|
98
|
+
|
|
99
|
+
await emit_event(event_type, payload or {})
|
|
100
|
+
|
|
101
|
+
def get_api_routes(self):
|
|
102
|
+
"""No routes yet: the event log's own travel with the panel (keepup-104)."""
|
|
103
|
+
return []
|
|
104
|
+
|
|
105
|
+
def get_handlers(self):
|
|
106
|
+
"""What another plugin may reach instead of importing the audit."""
|
|
107
|
+
return {"recording": self.recording, "emit": self.emit}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Sign-in as a capability: who is calling, and what they may do.
|
|
2
|
+
|
|
3
|
+
Task keepup-116, first half. The kernel owns the shapes and the two names
|
|
4
|
+
(``keepup/kernel/security.py``, keepup-119); this plugin is a deployment putting
|
|
5
|
+
an answer behind them, and the tables that answer needs -- the panel's sessions
|
|
6
|
+
and the login attempts that throttle a guesser -- belong to it.
|
|
7
|
+
|
|
8
|
+
The sign-in machinery itself (``auth/routes.py``, ``panel_session.py``,
|
|
9
|
+
``login_throttle.py``, the providers, OIDC and somebody else's identity system)
|
|
10
|
+
travels into this capability's distribution in keepup-124, together with the
|
|
11
|
+
routes, the sockets and the CSRF middleware it contributes. What is here is the
|
|
12
|
+
claim: two services, two tables, and an identity the runtime adopts.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import Any, Dict, List
|
|
17
|
+
|
|
18
|
+
from keepup.kernel.datasource import SERVICE_DATASOURCE
|
|
19
|
+
from keepup.kernel.descriptor import KIND_OPTIONAL, PluginDescriptor
|
|
20
|
+
from keepup.kernel.security import SERVICE_AUTH, SERVICE_PERMISSIONS, Identity
|
|
21
|
+
from keepup.plugins.base import BasePlugin
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
__all__ = ["AuthPlugin", "build_identity"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_identity(name: str = "keepup-auth") -> Identity:
|
|
29
|
+
"""The framework's own sign-in, behind the shape the kernel owns.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name: what to call it in a log line.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The identity: how a caller is found, and what decides a right.
|
|
36
|
+
"""
|
|
37
|
+
from keepup.auth.dependencies import get_panel_user
|
|
38
|
+
from keepup.auth.identity import access
|
|
39
|
+
|
|
40
|
+
return Identity(subject_dependency=get_panel_user, checker=access.check, name=name)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AuthPlugin(BasePlugin):
|
|
44
|
+
"""The capability: sessions, attempts, and the two services that answer."""
|
|
45
|
+
|
|
46
|
+
descriptor = PluginDescriptor(
|
|
47
|
+
id="auth",
|
|
48
|
+
name="Sign-in and sessions",
|
|
49
|
+
version="0.4.0",
|
|
50
|
+
distribution="keepup-auth",
|
|
51
|
+
kind=KIND_OPTIONAL,
|
|
52
|
+
priority=30,
|
|
53
|
+
provides=("auth>=1", "permissions>=1"),
|
|
54
|
+
requires=("datasource>=1",),
|
|
55
|
+
contributions=("routes", "sockets", "middleware", "tables", "permissions"),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def __init__(self, config=None):
|
|
59
|
+
super().__init__("auth", "Sign-in and sessions", config)
|
|
60
|
+
|
|
61
|
+
def register(self, services):
|
|
62
|
+
"""Publish the identity under both names, once there is storage.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
services: the runtime's registry.
|
|
66
|
+
"""
|
|
67
|
+
if services.has(SERVICE_DATASOURCE):
|
|
68
|
+
self.identity = build_identity()
|
|
69
|
+
services.provide(SERVICE_AUTH, self.identity, version=1, plugin_id="auth")
|
|
70
|
+
services.provide(SERVICE_PERMISSIONS, self.identity, version=1, plugin_id="auth")
|
|
71
|
+
|
|
72
|
+
async def initialize(self):
|
|
73
|
+
"""Keep the identity published at registration; build one if there is none."""
|
|
74
|
+
if getattr(self, "identity", None) is None:
|
|
75
|
+
self.identity = build_identity()
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
def get_declared_tables(self) -> List[Any]:
|
|
79
|
+
"""The two tables this capability owns, declared once, until keepup-124."""
|
|
80
|
+
from keepup.auth.login_throttle import LOGIN_ATTEMPTS
|
|
81
|
+
from keepup.auth.panel_session import AUTH_SESSION
|
|
82
|
+
|
|
83
|
+
return [AUTH_SESSION, LOGIN_ATTEMPTS]
|
|
84
|
+
|
|
85
|
+
def get_route_permissions(self) -> Dict[str, Any]:
|
|
86
|
+
"""The rights this capability's routes ask for, by name.
|
|
87
|
+
|
|
88
|
+
Empty here, and declared all the same: the kind of contribution is what
|
|
89
|
+
another plugin reads to know that a right it asks for exists.
|
|
90
|
+
"""
|
|
91
|
+
return {}
|
|
92
|
+
|
|
93
|
+
def get_api_routes(self) -> List[Any]:
|
|
94
|
+
"""No routes yet: the sign-in's own (sign in, out, refresh, WebSocket, CSRF)
|
|
95
|
+
travel into this capability's distribution in keepup-124."""
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
def get_handlers(self) -> Dict[str, Any]:
|
|
99
|
+
"""What another plugin may reach instead of importing the sign-in."""
|
|
100
|
+
return {"identity": self.identity}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""The data source as a plugin: the abstraction that holds the database library.
|
|
2
|
+
|
|
3
|
+
Task keepup-124, first slice. `keepup/kernel/datasource.py` owns the shapes and
|
|
4
|
+
the two names (keepup-106), the drivers answer with `spec()` (keepup-107,
|
|
5
|
+
keepup-108) -- and nobody published `datasource`, so no capability could reach
|
|
6
|
+
storage through it. This is the plugin that does: it requires a driver, publishes
|
|
7
|
+
the service built on the framework's current manager (`keepup.db`), and creates
|
|
8
|
+
nothing itself except what capabilities declare.
|
|
9
|
+
|
|
10
|
+
It answers the abstraction with the manager, so the dialect, the pool and the
|
|
11
|
+
creation of declared tables stay where they are today and move into
|
|
12
|
+
`packages/keepup-db` in the rest of keepup-124. What is fixed here is the part
|
|
13
|
+
that cannot be deferred: `keepup-admin` plus `keepup-db` plus one driver is a
|
|
14
|
+
deployment that talks to a database, and every capability asks it by name.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
from typing import Any, Dict, List, Mapping, Optional
|
|
19
|
+
|
|
20
|
+
from keepup.db import DatabaseManagerV2, db_config
|
|
21
|
+
from keepup.kernel.datasource import (
|
|
22
|
+
SERVICE_DATASOURCE,
|
|
23
|
+
SERVICE_DATASOURCE_DRIVER,
|
|
24
|
+
DriverSpec,
|
|
25
|
+
)
|
|
26
|
+
from keepup.kernel.descriptor import KIND_REQUIRED, PluginDescriptor
|
|
27
|
+
from keepup.plugins.base import BasePlugin
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
__all__ = ["DbPlugin", "DatabaseSource", "SERVICE_DATASOURCE"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DatabaseSource:
|
|
35
|
+
"""The framework's manager, wearing the abstraction's shape.
|
|
36
|
+
|
|
37
|
+
Everything a capability may ask for is here and nothing else: run a
|
|
38
|
+
statement, run one that changes something, create what was declared, close.
|
|
39
|
+
Which database this is comes from the driver, not from here.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, manager: Any, spec: Optional[DriverSpec] = None):
|
|
43
|
+
self.manager = manager
|
|
44
|
+
self.spec = spec
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def dialect(self) -> str:
|
|
48
|
+
"""The dialect the driver named, or the configured one."""
|
|
49
|
+
if self.spec is not None and self.spec.dialect:
|
|
50
|
+
return self.spec.dialect
|
|
51
|
+
return "postgresql" if db_config.is_postgres() else "sqlite"
|
|
52
|
+
|
|
53
|
+
async def execute(self, statement: str, params: Optional[Mapping[str, Any]] = None) -> Any:
|
|
54
|
+
"""Run a statement and answer its rows."""
|
|
55
|
+
return await self.manager.execute_async(statement, dict(params or {}))
|
|
56
|
+
|
|
57
|
+
async def execute_one(self, statement: str,
|
|
58
|
+
params: Optional[Mapping[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
|
59
|
+
"""Run a statement and answer its first row, or None."""
|
|
60
|
+
return await self.manager.execute_one_async(statement, dict(params or {}))
|
|
61
|
+
|
|
62
|
+
async def execute_commit(self, statement: str,
|
|
63
|
+
params: Optional[Mapping[str, Any]] = None) -> Any:
|
|
64
|
+
"""Run a statement that changes something, and commit it."""
|
|
65
|
+
return await self.manager.execute_commit_async(statement, dict(params or {}))
|
|
66
|
+
|
|
67
|
+
async def execute_many(self, statement: str, params: List[Mapping[str, Any]]) -> Any:
|
|
68
|
+
"""Run one statement for every set of parameters."""
|
|
69
|
+
return await self.manager.execute_many_async(statement, [dict(item) for item in params])
|
|
70
|
+
|
|
71
|
+
def ensure_tables(self, *tables: Any) -> None:
|
|
72
|
+
"""Create what the declaration says, and add the columns it gained.
|
|
73
|
+
|
|
74
|
+
The declarations come from the capabilities that own them, which is why
|
|
75
|
+
this takes them as arguments rather than knowing any itself.
|
|
76
|
+
"""
|
|
77
|
+
from keepup import tables as table_language
|
|
78
|
+
|
|
79
|
+
flat = []
|
|
80
|
+
for table in tables:
|
|
81
|
+
flat.extend(table if isinstance(table, (list, tuple)) else [table])
|
|
82
|
+
created = [table for table in flat if table is not None]
|
|
83
|
+
if not created:
|
|
84
|
+
return
|
|
85
|
+
table_language.ensure_tables(*created)
|
|
86
|
+
|
|
87
|
+
def ensure_columns(self, table: Any) -> None:
|
|
88
|
+
"""Add the columns an existing table lacks."""
|
|
89
|
+
from keepup import tables as table_language
|
|
90
|
+
|
|
91
|
+
table_language.ensure_columns(table)
|
|
92
|
+
|
|
93
|
+
def get_session(self) -> Any:
|
|
94
|
+
"""A session, for a caller that needs a transaction."""
|
|
95
|
+
return self.manager.get_session()
|
|
96
|
+
|
|
97
|
+
def raw_connection(self) -> Any:
|
|
98
|
+
"""A driver cursor, for code that has to go below the abstraction."""
|
|
99
|
+
return self.manager.raw_connection()
|
|
100
|
+
|
|
101
|
+
def pool_status(self) -> Any:
|
|
102
|
+
"""What the pool is doing, for the metrics capability."""
|
|
103
|
+
reader = getattr(self.manager, "get_pool_status", None)
|
|
104
|
+
return reader() if callable(reader) else {}
|
|
105
|
+
|
|
106
|
+
def dispose(self) -> None:
|
|
107
|
+
"""Close what this opened."""
|
|
108
|
+
self.manager.dispose()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class DbPlugin(BasePlugin):
|
|
112
|
+
"""The capability every other one needs: storage, by name."""
|
|
113
|
+
|
|
114
|
+
descriptor = PluginDescriptor(
|
|
115
|
+
id="db",
|
|
116
|
+
name="Database abstraction",
|
|
117
|
+
version="0.4.0",
|
|
118
|
+
distribution="keepup-db",
|
|
119
|
+
kind=KIND_REQUIRED,
|
|
120
|
+
priority=10,
|
|
121
|
+
provides=("datasource>=1",),
|
|
122
|
+
requires=("datasource_driver>=1",),
|
|
123
|
+
contributions=("tables",),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def __init__(self, config=None):
|
|
127
|
+
super().__init__("db", "Database abstraction", config)
|
|
128
|
+
|
|
129
|
+
def register(self, services):
|
|
130
|
+
"""Publish the data source the driver describes.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
services: the runtime's registry.
|
|
134
|
+
"""
|
|
135
|
+
spec = None
|
|
136
|
+
if services.has(SERVICE_DATASOURCE_DRIVER):
|
|
137
|
+
driver = services.require(SERVICE_DATASOURCE_DRIVER)
|
|
138
|
+
spec = driver.spec() if callable(getattr(driver, "spec", None)) else None
|
|
139
|
+
self.source = DatabaseSource(DatabaseManagerV2, spec)
|
|
140
|
+
services.provide(SERVICE_DATASOURCE, self.source, version=1, plugin_id="db")
|
|
141
|
+
logger.info("The data source is %s", self.source.dialect)
|
|
142
|
+
|
|
143
|
+
async def initialize(self):
|
|
144
|
+
"""Take the driver's description, and open nothing.
|
|
145
|
+
|
|
146
|
+
The driver is read here rather than at registration: a plugin's
|
|
147
|
+
``register`` runs in the order the catalogue lists, and the abstraction
|
|
148
|
+
may well be listed before the driver it abstracts. By the time a phase
|
|
149
|
+
initialises, every plugin of it has registered.
|
|
150
|
+
"""
|
|
151
|
+
if self.services.has(SERVICE_DATASOURCE_DRIVER):
|
|
152
|
+
driver = self.services.require(SERVICE_DATASOURCE_DRIVER)
|
|
153
|
+
reader = getattr(driver, "spec", None)
|
|
154
|
+
if callable(reader):
|
|
155
|
+
self.source.spec = reader()
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
def get_declared_tables(self) -> List[Any]:
|
|
159
|
+
"""Nothing: the abstraction owns no table of the framework's.
|
|
160
|
+
|
|
161
|
+
Every table belongs to the capability that keeps it there, which is what
|
|
162
|
+
makes `keepup-db` an abstraction rather than a schema (keepup-106).
|
|
163
|
+
"""
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
def get_api_routes(self) -> List[Any]:
|
|
167
|
+
"""No routes: storage is reached by capabilities, not by people."""
|
|
168
|
+
return []
|
|
169
|
+
|
|
170
|
+
def get_handlers(self) -> Dict[str, Any]:
|
|
171
|
+
"""What another plugin may reach instead of importing the manager."""
|
|
172
|
+
return {"service": self.source}
|