adapt-server 0.1.4__tar.gz → 0.2.2__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.
- adapt_server-0.2.2/PKG-INFO +176 -0
- adapt_server-0.2.2/README.md +131 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/__init__.py +1 -1
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/groups.py +13 -4
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/models.py +23 -5
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/resources.py +5 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/ui.py +2 -1
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/users.py +20 -5
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/app.py +219 -10
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/check.py +2 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/list_endpoints.py +5 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/serve.py +3 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/config.py +2 -1
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/plugins/dataset_plugin.py +65 -2
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/plugins/media_plugin.py +5 -6
- adapt_server-0.2.2/adapt/security.py +97 -0
- adapt_server-0.2.2/adapt/security_urls.py +32 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/static/admin/app.js +25 -17
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/static/admin/style.css +1 -1
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/templates/admin/index.html +0 -1
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/templates/admin_base.html +23 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/templates/base.html +31 -2
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/templates/datatable.html +11 -3
- adapt_server-0.2.2/adapt/templates/landing.html +93 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/templates/login.html +30 -3
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/templates/profile.html +12 -3
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/utils/__init__.py +4 -10
- adapt_server-0.2.2/adapt_server.egg-info/PKG-INFO +176 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt_server.egg-info/SOURCES.txt +5 -1
- {adapt_server-0.1.4 → adapt_server-0.2.2}/pyproject.toml +1 -1
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_admin.py +19 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_auth.py +111 -1
- adapt_server-0.2.2/tests/test_cli_integration.py +43 -0
- adapt_server-0.2.2/tests/test_integration.py +527 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_phase3.py +8 -3
- adapt_server-0.2.2/tests/test_readonly.py +120 -0
- adapt_server-0.2.2/tests/test_security_urls.py +23 -0
- adapt_server-0.1.4/PKG-INFO +0 -582
- adapt_server-0.1.4/README.md +0 -537
- adapt_server-0.1.4/adapt/templates/landing.html +0 -56
- adapt_server-0.1.4/adapt_server.egg-info/PKG-INFO +0 -582
- adapt_server-0.1.4/tests/test_integration.py +0 -270
- adapt_server-0.1.4/tests/test_readonly.py +0 -70
- {adapt_server-0.1.4 → adapt_server-0.2.2}/LICENSE +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/__main__.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/__init__.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/api_keys.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/audit_logs.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/cache.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/locks.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/admin/permissions.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/api_keys.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/audit.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/auth/__init__.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/auth/dependencies.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/auth/password.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/auth/routes.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/auth/session.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/cache.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/cli.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/__init__.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/addsuperuser.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/__init__.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/add_to_group.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/create_group.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/create_permissions.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/create_user.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/delete_group.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/delete_user.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/list_groups.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/list_resources.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/list_users.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/commands/admin/remove_from_group.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/discovery.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/locks.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/models.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/permissions.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/plugins/__init__.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/plugins/base.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/plugins/csv_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/plugins/excel_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/plugins/html_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/plugins/markdown_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/plugins/parquet_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/plugins/python_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/routes.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/storage.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/templates/media_gallery.html +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/templates/media_player.html +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt/utils/query.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt_server.egg-info/dependency_links.txt +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt_server.egg-info/entry_points.txt +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt_server.egg-info/requires.txt +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/adapt_server.egg-info/top_level.txt +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/setup.cfg +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_config.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_constraints.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_csv_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_dataset_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_discovery.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_html_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_locks.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_markdown_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_on_delete.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_parquet_plugin.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_plugin_interface.py +0 -0
- {adapt_server-0.1.4 → adapt_server-0.2.2}/tests/test_python_plugin.py +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adapt-server
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Adaptive file-backed FastAPI server that turns datasets into CRUD APIs and UIs.
|
|
5
|
+
Author-email: notesofcliff <notesofcliff@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/McInci/adapt
|
|
8
|
+
Project-URL: Repository, https://github.com/McIndi/adapt
|
|
9
|
+
Project-URL: Issues, https://github.com/McIndi/adapt/issues
|
|
10
|
+
Keywords: fastapi,api,csv,excel,parquet,markdown,media
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: fastapi
|
|
25
|
+
Requires-Dist: uvicorn
|
|
26
|
+
Requires-Dist: sqlmodel
|
|
27
|
+
Requires-Dist: pendulum
|
|
28
|
+
Requires-Dist: watchfiles
|
|
29
|
+
Requires-Dist: jinja2
|
|
30
|
+
Requires-Dist: openpyxl
|
|
31
|
+
Requires-Dist: markdown
|
|
32
|
+
Requires-Dist: python-multipart
|
|
33
|
+
Requires-Dist: mutagen
|
|
34
|
+
Requires-Dist: moviepy
|
|
35
|
+
Requires-Dist: pillow
|
|
36
|
+
Requires-Dist: python-json-logger
|
|
37
|
+
Requires-Dist: pandas
|
|
38
|
+
Requires-Dist: fastparquet
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: pytest; extra == "dev"
|
|
41
|
+
Requires-Dist: httpx; extra == "dev"
|
|
42
|
+
Requires-Dist: build; extra == "dev"
|
|
43
|
+
Requires-Dist: twine; extra == "dev"
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
# Adapt
|
|
47
|
+
|
|
48
|
+
Adapt is a FastAPI server that turns files in a directory into APIs and UIs.
|
|
49
|
+
|
|
50
|
+
- Datasets (`.csv`, `.xlsx`, `.parquet`) become CRUD endpoints and DataTables UIs
|
|
51
|
+
- Markdown/HTML become browsable pages
|
|
52
|
+
- Media files become streaming endpoints and player/gallery UIs
|
|
53
|
+
- Python files can register custom routers
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install adapt-server
|
|
59
|
+
adapt addsuperuser --username admin /path/to/docroot
|
|
60
|
+
adapt serve /path/to/docroot
|
|
61
|
+
|
|
62
|
+
# Generate permissions for all discovered resources
|
|
63
|
+
adapt admin create-permissions /path/to/docroot __all__
|
|
64
|
+
|
|
65
|
+
# Everything below here can be done in the admin UI at
|
|
66
|
+
# http://localhost:8000/admin/ after logging in with the superuser account.
|
|
67
|
+
#
|
|
68
|
+
# Create a regular user
|
|
69
|
+
adapt admin create-user --username editor --password secret /path/to/docroot
|
|
70
|
+
|
|
71
|
+
# By default, the editor user has no permissions.
|
|
72
|
+
# See available groups (created by `adapt admin create-permissions`) and assign user to desired group
|
|
73
|
+
adapt admin list-groups /path/to/docroot
|
|
74
|
+
adapt admin add-to-group --username editor --group <group_name> /path/to/docroot
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Useful URLs:
|
|
78
|
+
|
|
79
|
+
- `/` landing page
|
|
80
|
+
- `/admin/` admin UI
|
|
81
|
+
- `/api/<resource>` resource API
|
|
82
|
+
- `/ui/<resource>` resource UI
|
|
83
|
+
- `/schema/<resource>` resource schema
|
|
84
|
+
|
|
85
|
+
## What Adapt Generates
|
|
86
|
+
|
|
87
|
+
From files in your docroot, Adapt auto-discovers resources and mounts routes with extensionless URLs where possible.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
data/
|
|
93
|
+
employees.csv
|
|
94
|
+
sales.xlsx
|
|
95
|
+
video.mp4
|
|
96
|
+
readme.md
|
|
97
|
+
stats.py
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Rough output:
|
|
101
|
+
|
|
102
|
+
- `/api/employees`, `/ui/employees`, `/schema/employees`
|
|
103
|
+
- `/api/sales/<sheet>`, `/ui/sales/<sheet>`
|
|
104
|
+
- `/media/video.mp4`, `/ui/video.mp4`, `/ui/media`
|
|
105
|
+
- `/readme`
|
|
106
|
+
- `/api/stats/*`
|
|
107
|
+
|
|
108
|
+
## Current Security Posture
|
|
109
|
+
|
|
110
|
+
This reflects the current implementation in the codebase.
|
|
111
|
+
|
|
112
|
+
### In Place
|
|
113
|
+
|
|
114
|
+
- **Authentication:** session cookies and API keys (`X-API-Key`)
|
|
115
|
+
- **Authorization:** RBAC (users, groups, permissions), plus superuser bypass
|
|
116
|
+
- **Password security:** PBKDF2 hashing with per-user salts
|
|
117
|
+
- **Session security:** expiration enforcement, sliding renewal, cleanup task
|
|
118
|
+
- **CSRF protection:** enforced for cookie-authenticated unsafe methods (`POST/PUT/PATCH/DELETE`), including mixed session + API-key requests
|
|
119
|
+
- **Redirect hardening:** login `next` paths are validated as local relative paths
|
|
120
|
+
- **Response hardening:** CSP, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, HSTS (when TLS is enabled)
|
|
121
|
+
- **Host header hardening:** Trusted Host middleware
|
|
122
|
+
- **Data integrity:** lock-based, atomic writes for mutable dataset plugins
|
|
123
|
+
- **Auditability:** admin/audit logging for security-relevant actions
|
|
124
|
+
- **Sensitive response cleanup:** admin user APIs no longer expose `password_hash`
|
|
125
|
+
|
|
126
|
+
### Important Deployment Notes
|
|
127
|
+
|
|
128
|
+
- Use TLS in non-local environments (`--tls-cert` + `--tls-key`) so secure cookies and HSTS protections are effective.
|
|
129
|
+
- API-key-only clients are exempt from CSRF checks by design; cookie-auth browser flows require CSRF tokens.
|
|
130
|
+
|
|
131
|
+
## Core Features
|
|
132
|
+
|
|
133
|
+
- Adaptive discovery and route generation
|
|
134
|
+
- Dataset CRUD with schema exposure
|
|
135
|
+
- Caching with invalidation on mutations
|
|
136
|
+
- Built-in admin UI for users/groups/permissions/locks/cache/api keys/audit logs
|
|
137
|
+
- Plugin architecture with companion overrides in `.adapt/`
|
|
138
|
+
|
|
139
|
+
## Dataset Mutation Envelope
|
|
140
|
+
|
|
141
|
+
For dataset endpoints, write operations use this payload structure:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"action": "create|update|delete",
|
|
146
|
+
"data": []
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Use object data for `update`/`delete` as needed (for example, with `_row_id`).
|
|
151
|
+
|
|
152
|
+
## CLI (Common Commands)
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
adapt serve <root> [--host ... --port ... --tls-cert ... --tls-key ... --reload --readonly --debug]
|
|
156
|
+
adapt check <root>
|
|
157
|
+
adapt addsuperuser <root> --username <name>
|
|
158
|
+
adapt list-endpoints <root>
|
|
159
|
+
adapt admin list-resources <root>
|
|
160
|
+
adapt admin create-permissions <root> __all__
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Documentation
|
|
164
|
+
|
|
165
|
+
Detailed docs live under `docs/manual/`.
|
|
166
|
+
|
|
167
|
+
- Manual index: [docs/manual/index.md](docs/manual/index.md)
|
|
168
|
+
- Security: [docs/manual/security.md](docs/manual/security.md)
|
|
169
|
+
- Quick start: [docs/manual/quick_start.md](docs/manual/quick_start.md)
|
|
170
|
+
- Configuration: [docs/manual/configuration.md](docs/manual/configuration.md)
|
|
171
|
+
- API reference: [docs/manual/api_reference.md](docs/manual/api_reference.md)
|
|
172
|
+
- Plugin development: [docs/manual/plugin_development.md](docs/manual/plugin_development.md)
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Adapt
|
|
2
|
+
|
|
3
|
+
Adapt is a FastAPI server that turns files in a directory into APIs and UIs.
|
|
4
|
+
|
|
5
|
+
- Datasets (`.csv`, `.xlsx`, `.parquet`) become CRUD endpoints and DataTables UIs
|
|
6
|
+
- Markdown/HTML become browsable pages
|
|
7
|
+
- Media files become streaming endpoints and player/gallery UIs
|
|
8
|
+
- Python files can register custom routers
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install adapt-server
|
|
14
|
+
adapt addsuperuser --username admin /path/to/docroot
|
|
15
|
+
adapt serve /path/to/docroot
|
|
16
|
+
|
|
17
|
+
# Generate permissions for all discovered resources
|
|
18
|
+
adapt admin create-permissions /path/to/docroot __all__
|
|
19
|
+
|
|
20
|
+
# Everything below here can be done in the admin UI at
|
|
21
|
+
# http://localhost:8000/admin/ after logging in with the superuser account.
|
|
22
|
+
#
|
|
23
|
+
# Create a regular user
|
|
24
|
+
adapt admin create-user --username editor --password secret /path/to/docroot
|
|
25
|
+
|
|
26
|
+
# By default, the editor user has no permissions.
|
|
27
|
+
# See available groups (created by `adapt admin create-permissions`) and assign user to desired group
|
|
28
|
+
adapt admin list-groups /path/to/docroot
|
|
29
|
+
adapt admin add-to-group --username editor --group <group_name> /path/to/docroot
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Useful URLs:
|
|
33
|
+
|
|
34
|
+
- `/` landing page
|
|
35
|
+
- `/admin/` admin UI
|
|
36
|
+
- `/api/<resource>` resource API
|
|
37
|
+
- `/ui/<resource>` resource UI
|
|
38
|
+
- `/schema/<resource>` resource schema
|
|
39
|
+
|
|
40
|
+
## What Adapt Generates
|
|
41
|
+
|
|
42
|
+
From files in your docroot, Adapt auto-discovers resources and mounts routes with extensionless URLs where possible.
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
data/
|
|
48
|
+
employees.csv
|
|
49
|
+
sales.xlsx
|
|
50
|
+
video.mp4
|
|
51
|
+
readme.md
|
|
52
|
+
stats.py
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Rough output:
|
|
56
|
+
|
|
57
|
+
- `/api/employees`, `/ui/employees`, `/schema/employees`
|
|
58
|
+
- `/api/sales/<sheet>`, `/ui/sales/<sheet>`
|
|
59
|
+
- `/media/video.mp4`, `/ui/video.mp4`, `/ui/media`
|
|
60
|
+
- `/readme`
|
|
61
|
+
- `/api/stats/*`
|
|
62
|
+
|
|
63
|
+
## Current Security Posture
|
|
64
|
+
|
|
65
|
+
This reflects the current implementation in the codebase.
|
|
66
|
+
|
|
67
|
+
### In Place
|
|
68
|
+
|
|
69
|
+
- **Authentication:** session cookies and API keys (`X-API-Key`)
|
|
70
|
+
- **Authorization:** RBAC (users, groups, permissions), plus superuser bypass
|
|
71
|
+
- **Password security:** PBKDF2 hashing with per-user salts
|
|
72
|
+
- **Session security:** expiration enforcement, sliding renewal, cleanup task
|
|
73
|
+
- **CSRF protection:** enforced for cookie-authenticated unsafe methods (`POST/PUT/PATCH/DELETE`), including mixed session + API-key requests
|
|
74
|
+
- **Redirect hardening:** login `next` paths are validated as local relative paths
|
|
75
|
+
- **Response hardening:** CSP, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy`, HSTS (when TLS is enabled)
|
|
76
|
+
- **Host header hardening:** Trusted Host middleware
|
|
77
|
+
- **Data integrity:** lock-based, atomic writes for mutable dataset plugins
|
|
78
|
+
- **Auditability:** admin/audit logging for security-relevant actions
|
|
79
|
+
- **Sensitive response cleanup:** admin user APIs no longer expose `password_hash`
|
|
80
|
+
|
|
81
|
+
### Important Deployment Notes
|
|
82
|
+
|
|
83
|
+
- Use TLS in non-local environments (`--tls-cert` + `--tls-key`) so secure cookies and HSTS protections are effective.
|
|
84
|
+
- API-key-only clients are exempt from CSRF checks by design; cookie-auth browser flows require CSRF tokens.
|
|
85
|
+
|
|
86
|
+
## Core Features
|
|
87
|
+
|
|
88
|
+
- Adaptive discovery and route generation
|
|
89
|
+
- Dataset CRUD with schema exposure
|
|
90
|
+
- Caching with invalidation on mutations
|
|
91
|
+
- Built-in admin UI for users/groups/permissions/locks/cache/api keys/audit logs
|
|
92
|
+
- Plugin architecture with companion overrides in `.adapt/`
|
|
93
|
+
|
|
94
|
+
## Dataset Mutation Envelope
|
|
95
|
+
|
|
96
|
+
For dataset endpoints, write operations use this payload structure:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"action": "create|update|delete",
|
|
101
|
+
"data": []
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Use object data for `update`/`delete` as needed (for example, with `_row_id`).
|
|
106
|
+
|
|
107
|
+
## CLI (Common Commands)
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
adapt serve <root> [--host ... --port ... --tls-cert ... --tls-key ... --reload --readonly --debug]
|
|
111
|
+
adapt check <root>
|
|
112
|
+
adapt addsuperuser <root> --username <name>
|
|
113
|
+
adapt list-endpoints <root>
|
|
114
|
+
adapt admin list-resources <root>
|
|
115
|
+
adapt admin create-permissions <root> __all__
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Documentation
|
|
119
|
+
|
|
120
|
+
Detailed docs live under `docs/manual/`.
|
|
121
|
+
|
|
122
|
+
- Manual index: [docs/manual/index.md](docs/manual/index.md)
|
|
123
|
+
- Security: [docs/manual/security.md](docs/manual/security.md)
|
|
124
|
+
- Quick start: [docs/manual/quick_start.md](docs/manual/quick_start.md)
|
|
125
|
+
- Configuration: [docs/manual/configuration.md](docs/manual/configuration.md)
|
|
126
|
+
- API reference: [docs/manual/api_reference.md](docs/manual/api_reference.md)
|
|
127
|
+
- Plugin development: [docs/manual/plugin_development.md](docs/manual/plugin_development.md)
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -7,7 +7,7 @@ from ..auth import require_superuser
|
|
|
7
7
|
from ..storage import User, Group, UserGroup, get_db_session
|
|
8
8
|
from ..audit import log_action
|
|
9
9
|
from . import router
|
|
10
|
-
from .models import GroupCreate,
|
|
10
|
+
from .models import GroupCreate, GroupReadSafe, GroupUserRead
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
@@ -58,7 +58,7 @@ def list_groups(
|
|
|
58
58
|
logger.debug("Listed %d groups with query params", len(result))
|
|
59
59
|
return result
|
|
60
60
|
|
|
61
|
-
@router.get("/groups/{group_id}", response_model=
|
|
61
|
+
@router.get("/groups/{group_id}", response_model=GroupReadSafe)
|
|
62
62
|
def get_group(group_id: int, db: Session = Depends(get_db_session), user = Depends(require_superuser)):
|
|
63
63
|
"""Get a group by ID, including its users."""
|
|
64
64
|
group = db.get(Group, group_id)
|
|
@@ -72,11 +72,20 @@ def get_group(group_id: int, db: Session = Depends(get_db_session), user = Depen
|
|
|
72
72
|
users = db.exec(stmt).all()
|
|
73
73
|
|
|
74
74
|
logger.debug("Retrieved group %s with %d users", group.name, len(users))
|
|
75
|
-
return
|
|
75
|
+
return GroupReadSafe(
|
|
76
76
|
id=group.id,
|
|
77
77
|
name=group.name,
|
|
78
78
|
description=group.description,
|
|
79
|
-
users=
|
|
79
|
+
users=[
|
|
80
|
+
GroupUserRead(
|
|
81
|
+
id=u.id,
|
|
82
|
+
username=u.username,
|
|
83
|
+
is_active=u.is_active,
|
|
84
|
+
is_superuser=u.is_superuser,
|
|
85
|
+
created_at=u.created_at,
|
|
86
|
+
)
|
|
87
|
+
for u in users
|
|
88
|
+
]
|
|
80
89
|
)
|
|
81
90
|
|
|
82
91
|
@router.post("/groups", response_model=Group)
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
from datetime import datetime
|
|
1
2
|
from pydantic import BaseModel
|
|
2
3
|
from typing import List, Optional
|
|
3
4
|
import logging
|
|
4
5
|
|
|
5
|
-
from ..storage import User
|
|
6
|
-
|
|
7
6
|
logger = logging.getLogger(__name__)
|
|
8
7
|
|
|
9
8
|
class UserCreate(BaseModel):
|
|
@@ -17,12 +16,31 @@ class GroupCreate(BaseModel):
|
|
|
17
16
|
name: str
|
|
18
17
|
description: Optional[str] = None
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
|
|
20
|
+
class UserPublic(BaseModel):
|
|
21
|
+
"""Safe user response model without sensitive fields."""
|
|
22
|
+
id: int
|
|
23
|
+
username: str
|
|
24
|
+
is_active: bool
|
|
25
|
+
is_superuser: bool
|
|
26
|
+
created_at: datetime
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GroupUserRead(BaseModel):
|
|
30
|
+
"""Safe group user representation."""
|
|
31
|
+
id: int
|
|
32
|
+
username: str
|
|
33
|
+
is_active: bool
|
|
34
|
+
is_superuser: bool
|
|
35
|
+
created_at: datetime
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GroupReadSafe(BaseModel):
|
|
39
|
+
"""Safe model for reading group data with users."""
|
|
22
40
|
id: int
|
|
23
41
|
name: str
|
|
24
42
|
description: Optional[str] = None
|
|
25
|
-
users: List[
|
|
43
|
+
users: List[GroupUserRead] = []
|
|
26
44
|
|
|
27
45
|
class PermissionCreate(BaseModel):
|
|
28
46
|
"""Model for creating a new permission."""
|
|
@@ -2,8 +2,10 @@ from pathlib import Path
|
|
|
2
2
|
from typing import List
|
|
3
3
|
import logging
|
|
4
4
|
|
|
5
|
+
from .. import cache
|
|
5
6
|
from ..config import AdaptConfig
|
|
6
7
|
from ..discovery import discover_resources
|
|
8
|
+
from ..storage import init_database
|
|
7
9
|
|
|
8
10
|
logger = logging.getLogger(__name__)
|
|
9
11
|
|
|
@@ -14,6 +16,9 @@ def list_resources(root: Path) -> List[str]:
|
|
|
14
16
|
Returns a list of resource identifiers including sub-namespaces for multi-resource files.
|
|
15
17
|
"""
|
|
16
18
|
config = AdaptConfig(root=root)
|
|
19
|
+
config.load_from_file()
|
|
20
|
+
init_database(config.db_path)
|
|
21
|
+
cache.configure(str(config.db_path))
|
|
17
22
|
resources = discover_resources(root, config)
|
|
18
23
|
resource_names = []
|
|
19
24
|
for r in resources:
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
|
|
5
5
|
from ..auth import get_current_user
|
|
6
6
|
from ..utils import build_ui_links
|
|
7
|
+
from ..security_urls import login_redirect_url
|
|
7
8
|
from . import router
|
|
8
9
|
|
|
9
10
|
logger = logging.getLogger(__name__)
|
|
@@ -14,7 +15,7 @@ def admin_ui(request: Request):
|
|
|
14
15
|
user = get_current_user(request)
|
|
15
16
|
if not user or not getattr(user, "is_superuser", False):
|
|
16
17
|
logger.debug("Non-superuser access to admin UI, redirecting to login")
|
|
17
|
-
return RedirectResponse(url="/
|
|
18
|
+
return RedirectResponse(url=login_redirect_url("/admin/"), status_code=302)
|
|
18
19
|
|
|
19
20
|
# Build context for template
|
|
20
21
|
is_superuser = getattr(user, 'is_superuser', False)
|
|
@@ -7,11 +7,11 @@ from ..auth import require_superuser, hash_password
|
|
|
7
7
|
from ..storage import User, get_db_session
|
|
8
8
|
from ..audit import log_action
|
|
9
9
|
from . import router
|
|
10
|
-
from .models import UserCreate
|
|
10
|
+
from .models import UserCreate, UserPublic
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
|
-
@router.get("/users", response_model=List[
|
|
14
|
+
@router.get("/users", response_model=List[UserPublic])
|
|
15
15
|
def list_users(
|
|
16
16
|
db: Session = Depends(get_db_session),
|
|
17
17
|
limit: int = Query(None, ge=1, le=1000),
|
|
@@ -59,9 +59,18 @@ def list_users(
|
|
|
59
59
|
result.append(u)
|
|
60
60
|
|
|
61
61
|
logger.debug("Listed %d users with query params", len(result))
|
|
62
|
-
return
|
|
62
|
+
return [
|
|
63
|
+
UserPublic(
|
|
64
|
+
id=u.id,
|
|
65
|
+
username=u.username,
|
|
66
|
+
is_active=u.is_active,
|
|
67
|
+
is_superuser=u.is_superuser,
|
|
68
|
+
created_at=u.created_at,
|
|
69
|
+
)
|
|
70
|
+
for u in result
|
|
71
|
+
]
|
|
63
72
|
|
|
64
|
-
@router.post("/users", response_model=
|
|
73
|
+
@router.post("/users", response_model=UserPublic)
|
|
65
74
|
def create_user(user_data: UserCreate, request: Request, db: Session = Depends(get_db_session), user: User = Depends(require_superuser)):
|
|
66
75
|
"""Create a new user."""
|
|
67
76
|
# Check if server is in read-only mode
|
|
@@ -87,7 +96,13 @@ def create_user(user_data: UserCreate, request: Request, db: Session = Depends(g
|
|
|
87
96
|
log_action(request, "create_user", "user", f"Created user {new_user.username}", user.id)
|
|
88
97
|
logger.info("Created user %s", new_user.username)
|
|
89
98
|
|
|
90
|
-
return
|
|
99
|
+
return UserPublic(
|
|
100
|
+
id=new_user.id,
|
|
101
|
+
username=new_user.username,
|
|
102
|
+
is_active=new_user.is_active,
|
|
103
|
+
is_superuser=new_user.is_superuser,
|
|
104
|
+
created_at=new_user.created_at,
|
|
105
|
+
)
|
|
91
106
|
|
|
92
107
|
@router.delete("/users/{user_id}")
|
|
93
108
|
def delete_user(user_id: int, request: Request, db: Session = Depends(get_db_session), user: User = Depends(require_superuser)):
|