@ao_zorin/zocket 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/bin/zocket-setup.cjs +12 -0
- package/bin/zocket.cjs +174 -0
- package/docs/AI_AUTODEPLOY.md +52 -0
- package/docs/CLIENTS_MCP.md +59 -0
- package/docs/INSTALL.md +288 -0
- package/docs/LOCAL_MODELS.md +95 -0
- package/package.json +52 -0
- package/pyproject.toml +29 -0
- package/scripts/ai-autodeploy.py +127 -0
- package/scripts/install-zocket.ps1 +116 -0
- package/scripts/install-zocket.sh +228 -0
- package/zocket/__init__.py +2 -0
- package/zocket/__main__.py +5 -0
- package/zocket/audit.py +76 -0
- package/zocket/auth.py +34 -0
- package/zocket/autostart.py +281 -0
- package/zocket/backup.py +33 -0
- package/zocket/cli.py +655 -0
- package/zocket/config_store.py +68 -0
- package/zocket/crypto.py +158 -0
- package/zocket/harden.py +136 -0
- package/zocket/i18n.py +216 -0
- package/zocket/mcp_server.py +249 -0
- package/zocket/paths.py +50 -0
- package/zocket/runner.py +108 -0
- package/zocket/templates/index.html +1062 -0
- package/zocket/templates/login.html +244 -0
- package/zocket/vault.py +331 -0
- package/zocket/web.py +490 -0
package/zocket/web.py
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import secrets
|
|
5
|
+
from functools import wraps
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import quote_plus
|
|
9
|
+
|
|
10
|
+
from flask import Flask, jsonify, redirect, render_template, request, session, url_for
|
|
11
|
+
|
|
12
|
+
from .audit import AuditLogger
|
|
13
|
+
from .auth import hash_password, verify_password
|
|
14
|
+
from .config_store import ConfigStore
|
|
15
|
+
from .i18n import normalize_lang, tr
|
|
16
|
+
from .vault import ProjectNotFoundError, SecretNotFoundError, ValidationError, VaultService
|
|
17
|
+
|
|
18
|
+
DEFAULT_FOLDER_PICKER_ROOTS = (
|
|
19
|
+
"/home",
|
|
20
|
+
"/srv",
|
|
21
|
+
"/opt",
|
|
22
|
+
"/var/www",
|
|
23
|
+
"/var/lib",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
THEME_STANDARD = "standard"
|
|
27
|
+
THEME_ZORIN = "zorin"
|
|
28
|
+
AVAILABLE_THEMES = {THEME_STANDARD, THEME_ZORIN}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _folder_picker_roots(config: dict) -> list[Path]:
|
|
32
|
+
raw = config.get("folder_picker_roots")
|
|
33
|
+
candidates = raw if isinstance(raw, list) and raw else list(DEFAULT_FOLDER_PICKER_ROOTS)
|
|
34
|
+
roots: list[Path] = []
|
|
35
|
+
seen: set[str] = set()
|
|
36
|
+
for item in candidates:
|
|
37
|
+
if not isinstance(item, str) or not item.strip():
|
|
38
|
+
continue
|
|
39
|
+
try:
|
|
40
|
+
resolved = Path(item).expanduser().resolve(strict=False)
|
|
41
|
+
except OSError:
|
|
42
|
+
continue
|
|
43
|
+
as_str = str(resolved)
|
|
44
|
+
if as_str in seen:
|
|
45
|
+
continue
|
|
46
|
+
if resolved.exists() and resolved.is_dir():
|
|
47
|
+
roots.append(resolved)
|
|
48
|
+
seen.add(as_str)
|
|
49
|
+
return roots
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_subpath(child: Path, parent: Path) -> bool:
|
|
53
|
+
return child == parent or child.is_relative_to(parent)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _is_allowed_path(path: Path, roots: list[Path]) -> bool:
|
|
57
|
+
return any(_is_subpath(path, root) for root in roots)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _safe_resolve(path: str) -> Path:
|
|
61
|
+
return Path(path).expanduser().resolve(strict=False)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _current_lang(config: dict) -> str:
|
|
65
|
+
if "lang" in request.args:
|
|
66
|
+
lang = normalize_lang(request.args.get("lang"))
|
|
67
|
+
session["lang"] = lang
|
|
68
|
+
return lang
|
|
69
|
+
if "lang" in session:
|
|
70
|
+
return normalize_lang(str(session.get("lang")))
|
|
71
|
+
return normalize_lang(str(config.get("language", "en")))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def normalize_variant(value: str | None) -> str:
|
|
75
|
+
if not value:
|
|
76
|
+
return "dark"
|
|
77
|
+
normalized = value.strip().lower()
|
|
78
|
+
return normalized if normalized in {"light", "dark"} else "dark"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _current_variant(config: dict[str, Any]) -> str:
|
|
82
|
+
if "variant" in request.args:
|
|
83
|
+
variant = normalize_variant(request.args.get("variant"))
|
|
84
|
+
session["theme_variant"] = variant
|
|
85
|
+
return variant
|
|
86
|
+
stored = session.get("theme_variant")
|
|
87
|
+
if stored:
|
|
88
|
+
return normalize_variant(str(stored))
|
|
89
|
+
return normalize_variant(str(config.get("theme_variant", "dark")))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def normalize_theme(value: str | None) -> str:
|
|
93
|
+
if not value:
|
|
94
|
+
return THEME_STANDARD
|
|
95
|
+
normalized = value.strip().lower()
|
|
96
|
+
return normalized if normalized in AVAILABLE_THEMES else THEME_STANDARD
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _current_theme(config: dict[str, Any]) -> str:
|
|
100
|
+
arg_theme = request.args.get("theme")
|
|
101
|
+
if arg_theme:
|
|
102
|
+
theme = normalize_theme(arg_theme)
|
|
103
|
+
session["theme"] = theme
|
|
104
|
+
return theme
|
|
105
|
+
stored = session.get("theme")
|
|
106
|
+
if stored:
|
|
107
|
+
return normalize_theme(str(stored))
|
|
108
|
+
return normalize_theme(str(config.get("theme", THEME_STANDARD)))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _is_authenticated(config: dict) -> bool:
|
|
112
|
+
if not bool(config.get("web_auth_enabled", True)):
|
|
113
|
+
return True
|
|
114
|
+
return bool(session.get("is_authenticated", False))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _has_password(config: dict) -> bool:
|
|
118
|
+
return bool(config.get("web_password_hash")) and bool(config.get("web_password_salt"))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def create_web_app(
|
|
122
|
+
vault: VaultService,
|
|
123
|
+
cfg_store: ConfigStore,
|
|
124
|
+
audit: AuditLogger,
|
|
125
|
+
) -> Flask:
|
|
126
|
+
template_dir = Path(__file__).with_name("templates").resolve()
|
|
127
|
+
app = Flask(__name__, template_folder=str(template_dir))
|
|
128
|
+
cfg = cfg_store.ensure_exists()
|
|
129
|
+
|
|
130
|
+
app.config["SECRET_KEY"] = cfg["session_secret"]
|
|
131
|
+
app.config["SESSION_COOKIE_HTTPONLY"] = True
|
|
132
|
+
app.config["SESSION_COOKIE_SAMESITE"] = "Lax"
|
|
133
|
+
app.config["SESSION_COOKIE_SECURE"] = False
|
|
134
|
+
|
|
135
|
+
@app.context_processor
|
|
136
|
+
def inject_i18n():
|
|
137
|
+
cfg_local = cfg_store.load()
|
|
138
|
+
lang = _current_lang(cfg_local)
|
|
139
|
+
theme = _current_theme(cfg_local)
|
|
140
|
+
variant = _current_variant(cfg_local)
|
|
141
|
+
return {
|
|
142
|
+
"t": lambda key, **kwargs: tr(lang, key, **kwargs),
|
|
143
|
+
"lang": lang,
|
|
144
|
+
"theme": theme,
|
|
145
|
+
"theme_variant": variant,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def login_required(fn):
|
|
149
|
+
@wraps(fn)
|
|
150
|
+
def wrapper(*args, **kwargs):
|
|
151
|
+
cfg_local = cfg_store.load()
|
|
152
|
+
_current_lang(cfg_local)
|
|
153
|
+
if _is_authenticated(cfg_local):
|
|
154
|
+
return fn(*args, **kwargs)
|
|
155
|
+
return redirect(url_for("login", next=request.path))
|
|
156
|
+
|
|
157
|
+
return wrapper
|
|
158
|
+
|
|
159
|
+
@app.get("/login")
|
|
160
|
+
def login():
|
|
161
|
+
cfg_local = cfg_store.load()
|
|
162
|
+
if not bool(cfg_local.get("web_auth_enabled", True)):
|
|
163
|
+
return redirect("/")
|
|
164
|
+
if session.get("is_authenticated"):
|
|
165
|
+
return redirect("/")
|
|
166
|
+
lang = _current_lang(cfg_local)
|
|
167
|
+
error = request.args.get("error")
|
|
168
|
+
next_path = request.args.get("next") or request.path
|
|
169
|
+
theme = _current_theme(cfg_local)
|
|
170
|
+
return render_template(
|
|
171
|
+
"login.html",
|
|
172
|
+
error=error,
|
|
173
|
+
lang=lang,
|
|
174
|
+
missing_password=not _has_password(cfg_local),
|
|
175
|
+
next_path=next_path,
|
|
176
|
+
theme=theme,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
@app.post("/login")
|
|
180
|
+
def login_post():
|
|
181
|
+
cfg_local = cfg_store.load()
|
|
182
|
+
if not bool(cfg_local.get("web_auth_enabled", True)):
|
|
183
|
+
return redirect("/")
|
|
184
|
+
lang = _current_lang(cfg_local)
|
|
185
|
+
if not _has_password(cfg_local):
|
|
186
|
+
return redirect("/login")
|
|
187
|
+
password = request.form.get("password", "")
|
|
188
|
+
ok = verify_password(
|
|
189
|
+
password=password,
|
|
190
|
+
salt_hex=str(cfg_local["web_password_salt"]),
|
|
191
|
+
expected_hash_hex=str(cfg_local["web_password_hash"]),
|
|
192
|
+
iterations=int(cfg_local.get("web_password_iterations", 390000)),
|
|
193
|
+
)
|
|
194
|
+
if not ok:
|
|
195
|
+
audit.log("web.login", "failed", "web", {"remote_addr": request.remote_addr})
|
|
196
|
+
return redirect(f"/login?error={quote_plus(tr(lang, 'ui.invalid_login'))}")
|
|
197
|
+
session["is_authenticated"] = True
|
|
198
|
+
audit.log("web.login", "ok", "web", {"remote_addr": request.remote_addr})
|
|
199
|
+
return redirect("/")
|
|
200
|
+
|
|
201
|
+
@app.post("/setup/first-run")
|
|
202
|
+
def first_run_setup():
|
|
203
|
+
cfg_local = cfg_store.load()
|
|
204
|
+
lang = _current_lang(cfg_local)
|
|
205
|
+
if _has_password(cfg_local):
|
|
206
|
+
return redirect("/login")
|
|
207
|
+
|
|
208
|
+
mode = (request.form.get("mode") or "").strip()
|
|
209
|
+
if mode == "set_password":
|
|
210
|
+
password = request.form.get("password", "")
|
|
211
|
+
password_repeat = request.form.get("password_repeat", "")
|
|
212
|
+
if not password:
|
|
213
|
+
return redirect(f"/login?error={quote_plus(tr(lang, 'ui.password_required'))}")
|
|
214
|
+
if password != password_repeat:
|
|
215
|
+
return redirect(f"/login?error={quote_plus(tr(lang, 'ui.passwords_do_not_match'))}")
|
|
216
|
+
salt_hex, hash_hex = hash_password(password)
|
|
217
|
+
cfg_local["web_password_salt"] = salt_hex
|
|
218
|
+
cfg_local["web_password_hash"] = hash_hex
|
|
219
|
+
cfg_local["web_auth_enabled"] = True
|
|
220
|
+
cfg_store.save(cfg_local)
|
|
221
|
+
session["is_authenticated"] = True
|
|
222
|
+
audit.log("web.setup.first_run", "ok", "web", {"mode": "set_password"})
|
|
223
|
+
return redirect("/")
|
|
224
|
+
|
|
225
|
+
if mode == "generate_password":
|
|
226
|
+
generated = secrets.token_urlsafe(24)
|
|
227
|
+
salt_hex, hash_hex = hash_password(generated)
|
|
228
|
+
cfg_local["web_password_salt"] = salt_hex
|
|
229
|
+
cfg_local["web_password_hash"] = hash_hex
|
|
230
|
+
cfg_local["web_auth_enabled"] = True
|
|
231
|
+
cfg_store.save(cfg_local)
|
|
232
|
+
session["is_authenticated"] = True
|
|
233
|
+
session["generated_password_once"] = generated
|
|
234
|
+
audit.log("web.setup.first_run", "ok", "web", {"mode": "generate_password"})
|
|
235
|
+
return redirect("/")
|
|
236
|
+
|
|
237
|
+
if mode == "no_password":
|
|
238
|
+
confirmed = request.form.get("confirm_no_password") == "1"
|
|
239
|
+
if not confirmed:
|
|
240
|
+
return redirect(
|
|
241
|
+
f"/login?error={quote_plus(tr(lang, 'ui.confirm_insecure_required'))}"
|
|
242
|
+
)
|
|
243
|
+
cfg_local["web_auth_enabled"] = False
|
|
244
|
+
cfg_local["web_password_salt"] = ""
|
|
245
|
+
cfg_local["web_password_hash"] = ""
|
|
246
|
+
cfg_store.save(cfg_local)
|
|
247
|
+
session["is_authenticated"] = True
|
|
248
|
+
audit.log("web.setup.first_run", "ok", "web", {"mode": "no_password"})
|
|
249
|
+
return redirect("/")
|
|
250
|
+
|
|
251
|
+
return redirect(f"/login?error={quote_plus(tr(lang, 'ui.invalid_setup_option'))}")
|
|
252
|
+
|
|
253
|
+
@app.post("/set-theme")
|
|
254
|
+
def set_theme():
|
|
255
|
+
theme = normalize_theme(request.form.get("theme"))
|
|
256
|
+
session["theme"] = theme
|
|
257
|
+
next_url = request.form.get("next") or request.referrer or "/"
|
|
258
|
+
return redirect(next_url)
|
|
259
|
+
|
|
260
|
+
@app.post("/set-theme-variant")
|
|
261
|
+
def set_theme_variant():
|
|
262
|
+
variant = normalize_variant(request.form.get("variant"))
|
|
263
|
+
session["theme_variant"] = variant
|
|
264
|
+
next_url = request.form.get("next") or request.referrer or "/"
|
|
265
|
+
return redirect(next_url)
|
|
266
|
+
|
|
267
|
+
@app.post("/logout")
|
|
268
|
+
def logout():
|
|
269
|
+
session.pop("is_authenticated", None)
|
|
270
|
+
return redirect("/login")
|
|
271
|
+
|
|
272
|
+
@app.get("/api/folders")
|
|
273
|
+
@login_required
|
|
274
|
+
def list_folders():
|
|
275
|
+
cfg_local = cfg_store.load()
|
|
276
|
+
roots = _folder_picker_roots(cfg_local)
|
|
277
|
+
if not roots:
|
|
278
|
+
return jsonify({"ok": False, "error": "Folder picker is not configured."}), 500
|
|
279
|
+
|
|
280
|
+
requested = (request.args.get("path") or "").strip()
|
|
281
|
+
if not requested:
|
|
282
|
+
root_rows = [{"name": str(path), "path": str(path)} for path in roots]
|
|
283
|
+
return jsonify(
|
|
284
|
+
{
|
|
285
|
+
"ok": True,
|
|
286
|
+
"current": None,
|
|
287
|
+
"parent": None,
|
|
288
|
+
"roots": root_rows,
|
|
289
|
+
"directories": root_rows,
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
current = _safe_resolve(requested)
|
|
295
|
+
except OSError:
|
|
296
|
+
return jsonify({"ok": False, "error": "Invalid folder path."}), 400
|
|
297
|
+
|
|
298
|
+
if not _is_allowed_path(current, roots):
|
|
299
|
+
return jsonify({"ok": False, "error": "Folder is outside allowed roots."}), 403
|
|
300
|
+
if not current.exists():
|
|
301
|
+
return jsonify({"ok": False, "error": "Folder does not exist."}), 404
|
|
302
|
+
if not current.is_dir():
|
|
303
|
+
return jsonify({"ok": False, "error": "Path is not a folder."}), 400
|
|
304
|
+
|
|
305
|
+
directories: list[dict[str, str]] = []
|
|
306
|
+
try:
|
|
307
|
+
with os.scandir(current) as entries:
|
|
308
|
+
for entry in entries:
|
|
309
|
+
if entry.is_dir(follow_symlinks=False):
|
|
310
|
+
if entry.name.startswith("."):
|
|
311
|
+
continue
|
|
312
|
+
try:
|
|
313
|
+
child = _safe_resolve(entry.path)
|
|
314
|
+
except OSError:
|
|
315
|
+
continue
|
|
316
|
+
if _is_allowed_path(child, roots):
|
|
317
|
+
directories.append({"name": entry.name, "path": str(child)})
|
|
318
|
+
except PermissionError:
|
|
319
|
+
return jsonify({"ok": False, "error": "Permission denied for this folder."}), 403
|
|
320
|
+
directories.sort(key=lambda row: row["name"].lower())
|
|
321
|
+
|
|
322
|
+
parent = current.parent
|
|
323
|
+
parent_path = str(parent) if _is_allowed_path(parent, roots) and parent != current else None
|
|
324
|
+
return jsonify(
|
|
325
|
+
{
|
|
326
|
+
"ok": True,
|
|
327
|
+
"current": str(current),
|
|
328
|
+
"parent": parent_path,
|
|
329
|
+
"roots": [{"name": str(path), "path": str(path)} for path in roots],
|
|
330
|
+
"directories": directories,
|
|
331
|
+
}
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@app.get("/")
|
|
335
|
+
@login_required
|
|
336
|
+
def index():
|
|
337
|
+
cfg_local = cfg_store.load()
|
|
338
|
+
project = request.args.get("project")
|
|
339
|
+
show_values = bool(int(request.args.get("show_values", "0")))
|
|
340
|
+
error = request.args.get("error")
|
|
341
|
+
lang = _current_lang(cfg_local)
|
|
342
|
+
generated_password = session.pop("generated_password_once", None)
|
|
343
|
+
|
|
344
|
+
projects = vault.list_projects()
|
|
345
|
+
selected_project = project or (projects[0]["project"] if projects else None)
|
|
346
|
+
selected_project_info = next(
|
|
347
|
+
(item for item in projects if item["project"] == selected_project),
|
|
348
|
+
None,
|
|
349
|
+
)
|
|
350
|
+
secrets = (
|
|
351
|
+
vault.list_project_secrets(
|
|
352
|
+
selected_project, include_values=show_values
|
|
353
|
+
)
|
|
354
|
+
if selected_project
|
|
355
|
+
else []
|
|
356
|
+
)
|
|
357
|
+
return render_template(
|
|
358
|
+
"index.html",
|
|
359
|
+
projects=projects,
|
|
360
|
+
selected_project=selected_project,
|
|
361
|
+
selected_project_info=selected_project_info,
|
|
362
|
+
secrets=secrets,
|
|
363
|
+
show_values=show_values,
|
|
364
|
+
error=error,
|
|
365
|
+
lang=lang,
|
|
366
|
+
generated_password=generated_password,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
@app.post("/projects/create")
|
|
370
|
+
@login_required
|
|
371
|
+
def create_project():
|
|
372
|
+
name = request.form.get("name", "")
|
|
373
|
+
description = request.form.get("description", "")
|
|
374
|
+
folder_path = request.form.get("folder_path", "")
|
|
375
|
+
try:
|
|
376
|
+
vault.create_project(
|
|
377
|
+
name,
|
|
378
|
+
description=description,
|
|
379
|
+
folder_path=folder_path,
|
|
380
|
+
)
|
|
381
|
+
audit.log(
|
|
382
|
+
"web.project.create",
|
|
383
|
+
"ok",
|
|
384
|
+
"web",
|
|
385
|
+
{"project": name, "folder_path": folder_path},
|
|
386
|
+
)
|
|
387
|
+
except ValidationError as exc:
|
|
388
|
+
audit.log("web.project.create", "failed", "web", {"error": str(exc)})
|
|
389
|
+
return redirect(f"/?error={quote_plus(str(exc))}")
|
|
390
|
+
return redirect(f"/?project={name}")
|
|
391
|
+
|
|
392
|
+
@app.post("/projects/<project>/folder")
|
|
393
|
+
@login_required
|
|
394
|
+
def set_project_folder(project: str):
|
|
395
|
+
clear = request.form.get("clear") == "1"
|
|
396
|
+
folder_path = None if clear else request.form.get("folder_path", "")
|
|
397
|
+
try:
|
|
398
|
+
vault.set_project_folder(project, folder_path)
|
|
399
|
+
audit.log(
|
|
400
|
+
"web.project.set_folder",
|
|
401
|
+
"ok",
|
|
402
|
+
"web",
|
|
403
|
+
{"project": project, "cleared": clear},
|
|
404
|
+
)
|
|
405
|
+
except (ValidationError, ProjectNotFoundError) as exc:
|
|
406
|
+
audit.log(
|
|
407
|
+
"web.project.set_folder",
|
|
408
|
+
"failed",
|
|
409
|
+
"web",
|
|
410
|
+
{"project": project, "error": str(exc)},
|
|
411
|
+
)
|
|
412
|
+
return redirect(f"/?project={project}&error={quote_plus(str(exc))}")
|
|
413
|
+
return redirect(f"/?project={project}")
|
|
414
|
+
|
|
415
|
+
@app.post("/projects/<project>/secrets/upsert")
|
|
416
|
+
@login_required
|
|
417
|
+
def upsert_secret(project: str):
|
|
418
|
+
key = request.form.get("key", "")
|
|
419
|
+
value = request.form.get("value", "")
|
|
420
|
+
description = request.form.get("description", "")
|
|
421
|
+
try:
|
|
422
|
+
vault.upsert_secret(project=project, key=key, value=value, description=description)
|
|
423
|
+
audit.log("web.secret.upsert", "ok", "web", {"project": project, "key": key})
|
|
424
|
+
except (ValidationError, ProjectNotFoundError) as exc:
|
|
425
|
+
audit.log(
|
|
426
|
+
"web.secret.upsert",
|
|
427
|
+
"failed",
|
|
428
|
+
"web",
|
|
429
|
+
{"project": project, "key": key, "error": str(exc)},
|
|
430
|
+
)
|
|
431
|
+
return redirect(f"/?project={project}&error={quote_plus(str(exc))}")
|
|
432
|
+
return redirect(f"/?project={project}")
|
|
433
|
+
|
|
434
|
+
@app.post("/projects/<project>/secrets/<key>/delete")
|
|
435
|
+
@login_required
|
|
436
|
+
def delete_secret(project: str, key: str):
|
|
437
|
+
try:
|
|
438
|
+
vault.delete_secret(project=project, key=key)
|
|
439
|
+
audit.log("web.secret.delete", "ok", "web", {"project": project, "key": key})
|
|
440
|
+
except (ProjectNotFoundError, SecretNotFoundError) as exc:
|
|
441
|
+
audit.log(
|
|
442
|
+
"web.secret.delete",
|
|
443
|
+
"failed",
|
|
444
|
+
"web",
|
|
445
|
+
{"project": project, "key": key, "error": str(exc)},
|
|
446
|
+
)
|
|
447
|
+
return redirect(f"/?project={project}&error={quote_plus(str(exc))}")
|
|
448
|
+
return redirect(f"/?project={project}")
|
|
449
|
+
|
|
450
|
+
@app.get("/projects/<project>/secrets/<key>/value")
|
|
451
|
+
@login_required
|
|
452
|
+
def secret_value(project: str, key: str):
|
|
453
|
+
try:
|
|
454
|
+
secret = vault.get_secret(project=project, key=key)
|
|
455
|
+
audit.log(
|
|
456
|
+
"web.secret.view",
|
|
457
|
+
"ok",
|
|
458
|
+
"web",
|
|
459
|
+
{"project": project, "key": key},
|
|
460
|
+
)
|
|
461
|
+
return jsonify(
|
|
462
|
+
{
|
|
463
|
+
"ok": True,
|
|
464
|
+
"key": key,
|
|
465
|
+
"value": secret.get("value", ""),
|
|
466
|
+
"description": secret.get("description", ""),
|
|
467
|
+
"updated_at": secret.get("updated_at"),
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
except (ProjectNotFoundError, SecretNotFoundError) as exc:
|
|
471
|
+
audit.log(
|
|
472
|
+
"web.secret.view",
|
|
473
|
+
"failed",
|
|
474
|
+
"web",
|
|
475
|
+
{"project": project, "key": key, "error": str(exc)},
|
|
476
|
+
)
|
|
477
|
+
return jsonify({"ok": False, "error": str(exc)}), 404
|
|
478
|
+
|
|
479
|
+
@app.post("/projects/<project>/delete")
|
|
480
|
+
@login_required
|
|
481
|
+
def delete_project(project: str):
|
|
482
|
+
try:
|
|
483
|
+
vault.delete_project(project=project)
|
|
484
|
+
audit.log("web.project.delete", "ok", "web", {"project": project})
|
|
485
|
+
except ProjectNotFoundError as exc:
|
|
486
|
+
audit.log("web.project.delete", "failed", "web", {"error": str(exc)})
|
|
487
|
+
return redirect(f"/?error={quote_plus(str(exc))}")
|
|
488
|
+
return redirect("/")
|
|
489
|
+
|
|
490
|
+
return app
|