dev fix, ip controls
This commit is contained in:
@@ -16,14 +16,19 @@ from ..config import (
|
||||
read_env_value,
|
||||
)
|
||||
from ..db.apps import (
|
||||
create_app_ip_access_rule,
|
||||
create_app_variable,
|
||||
delete_app_ip_access_rule,
|
||||
delete_app_variable,
|
||||
get_app,
|
||||
get_app_ip_access_rule,
|
||||
get_app_ip_access_rules,
|
||||
get_app_template,
|
||||
get_app_deployments,
|
||||
get_app_templates,
|
||||
get_app_variables,
|
||||
get_apps,
|
||||
update_app_ip_access_rule,
|
||||
update_app_metadata,
|
||||
update_app_resources,
|
||||
upsert_created_app,
|
||||
@@ -41,6 +46,20 @@ from ..templates.layout import page, render_result
|
||||
|
||||
router = APIRouter()
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
# Selectable method presets for IP access rules. WRITE = POST,PUT,PATCH,DELETE; ALL = every
|
||||
# common HTTP method including GET. The Caddy generator will later interpret these values.
|
||||
IP_ACCESS_METHOD_OPTIONS = (
|
||||
"WRITE",
|
||||
"ALL",
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
"GET,POST",
|
||||
"POST,PATCH,DELETE",
|
||||
)
|
||||
METADATA_FIELDS = (
|
||||
"name",
|
||||
"description",
|
||||
@@ -605,6 +624,7 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
template_value = render_template_label(templates, app.get("template", "") or "")
|
||||
environment_usage_example = render_environment_usage_example(language_raw, runtime_raw, template_raw)
|
||||
variables = get_app_variables(app.get("id", ""))
|
||||
ip_access_rules = get_app_ip_access_rules(app.get("id", ""))
|
||||
incidents = get_service_incidents(app.get("id", ""), limit=20)
|
||||
current_health = get_service_health(app.get("id", ""))
|
||||
health_history = get_service_health_history(app.get("id", ""), limit=50)
|
||||
@@ -701,6 +721,48 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
if not variable_rows:
|
||||
variable_rows = '<tr><td colspan="4">Zatím nejsou evidované žádné proměnné.</td></tr>'
|
||||
|
||||
def render_method_options(selected: str) -> str:
|
||||
selected_upper = (selected or "").upper()
|
||||
options = list(IP_ACCESS_METHOD_OPTIONS)
|
||||
if selected_upper and selected_upper not in options:
|
||||
options.append(selected_upper)
|
||||
out = ""
|
||||
for opt in options:
|
||||
is_sel = " selected" if opt == selected_upper else ""
|
||||
out += f'<option value="{html.escape(opt, quote=True)}"{is_sel}>{html.escape(opt)}</option>'
|
||||
return out
|
||||
|
||||
# IP access rules — inputs in each column are associated with the per-row update form via the
|
||||
# HTML5 form="..." attribute, so the whole row is editable inline while staying valid markup.
|
||||
ip_access_rule_rows = ""
|
||||
for rule in ip_access_rules:
|
||||
rid = html.escape(str(rule.get("id", "")))
|
||||
form_id = f"iprule-{rid}"
|
||||
rule_ip = html.escape(rule.get("ip_cidr", "") or "", quote=True)
|
||||
rule_desc = html.escape(rule.get("description", "") or "", quote=True)
|
||||
rule_enabled = bool(rule.get("is_enabled", 1))
|
||||
row_class = "" if rule_enabled else " rule-disabled"
|
||||
enabled_checked = bool_checked(rule_enabled)
|
||||
ip_label = html.escape(rule.get("ip_cidr", "") or "")
|
||||
ip_access_rule_rows += f"""
|
||||
<tr class="ip-rule-row{row_class}">
|
||||
<td><input name="ip_cidr" value="{rule_ip}" form="{form_id}" required></td>
|
||||
<td><select name="methods" form="{form_id}">{render_method_options(rule.get("methods", ""))}</select></td>
|
||||
<td><input name="description" value="{rule_desc}" form="{form_id}"></td>
|
||||
<td><label class="checkbox-label"><input type="checkbox" name="is_enabled" value="1" form="{form_id}"{enabled_checked}> Aktivní</label></td>
|
||||
<td>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/ip-access-rules/{rid}/update" id="{form_id}" class="inline-form"></form>
|
||||
<button type="submit" form="{form_id}" class="btn btn-compact"><i class="fa-solid fa-floppy-disk" aria-hidden="true"></i> Uložit</button>
|
||||
<form method="post" action="/portal/apps/{app_url_id}/ip-access-rules/{rid}/delete" class="inline-form" onsubmit="return confirm('Smazat pravidlo {ip_label}?');">
|
||||
<button type="submit" class="btn btn-secondary btn-compact"><i class="fa-solid fa-trash" aria-hidden="true"></i> Smazat</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not ip_access_rule_rows:
|
||||
ip_access_rule_rows = '<tr><td colspan="5">Zatím nejsou evidovaná žádná IP access pravidla.</td></tr>'
|
||||
|
||||
notice = ""
|
||||
if message:
|
||||
notice = f'<p class="alert">{html.escape(message)}</p>'
|
||||
@@ -724,6 +786,7 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
<div class="detail-tabs" aria-label="Sekce detailu služby">
|
||||
<a class="btn btn-secondary" href="#metadata">Metadata</a>
|
||||
<a class="btn btn-secondary" href="#promenne">Proměnné</a>
|
||||
<a class="btn btn-secondary" href="#ip-access">Security / IP Access</a>
|
||||
<a class="btn btn-secondary" href="#historie">Historie</a>
|
||||
</div>
|
||||
"""
|
||||
@@ -821,6 +884,45 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
else ""
|
||||
)
|
||||
|
||||
ip_access_card = (
|
||||
f"""
|
||||
<div class="card" id="ip-access">
|
||||
<h2><i class="fa-solid fa-shield-halved" aria-hidden="true"></i> Security / IP Access</h2>
|
||||
<p class="muted">
|
||||
Pravidla určují, z jakých IP/CIDR adres lze volat dané HTTP metody.
|
||||
<strong>WRITE</strong> = POST, PUT, PATCH, DELETE; <strong>ALL</strong> = všechny běžné metody včetně GET.
|
||||
Pravidla se zatím pouze ukládají (Caddy je začne vynucovat později).
|
||||
</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>IP/CIDR</th>
|
||||
<th>Methods</th>
|
||||
<th>Description</th>
|
||||
<th>Enabled</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
{ip_access_rule_rows}
|
||||
</table>
|
||||
|
||||
<form method="post" action="/portal/apps/{app_url_id}/ip-access-rules" class="metadata-form add-variable-form">
|
||||
<h3>Přidat pravidlo</h3>
|
||||
<label>IP/CIDR</label>
|
||||
<input name="ip_cidr" placeholder="např. 185.10.20.30 nebo 10.0.0.0/24" required>
|
||||
<label>Methods</label>
|
||||
<select name="methods">{render_method_options("WRITE")}</select>
|
||||
<label>Description</label>
|
||||
<input name="description" placeholder="např. Office IP">
|
||||
<label class="checkbox-label"><input type="checkbox" name="is_enabled" value="1" checked> Aktivní</label>
|
||||
<div class="form-actions">
|
||||
<button type="submit"><i class="fa-solid fa-plus" aria-hidden="true"></i> Přidat pravidlo</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
"""
|
||||
if can_manage
|
||||
else ""
|
||||
)
|
||||
|
||||
return page(
|
||||
"Detail slu\u017eby",
|
||||
f"""
|
||||
@@ -842,6 +944,8 @@ def app_detail(app_id: str, request: Request, message: str = "", error: str = ""
|
||||
|
||||
{variables_card}
|
||||
|
||||
{ip_access_card}
|
||||
|
||||
<div class="card">
|
||||
<h2>Souhrn</h2>
|
||||
<table>
|
||||
@@ -1111,6 +1215,94 @@ def apply_app_environment_action(app_id: str, user=Depends(require_developer)):
|
||||
return redirect_app_detail(app_id, anchor="promenne", message=message)
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/ip-access-rules")
|
||||
def add_app_ip_access_rule(
|
||||
app_id: str,
|
||||
ip_cidr: str = Form(...),
|
||||
methods: str = Form("WRITE"),
|
||||
description: str = Form(""),
|
||||
is_enabled: str | None = Form(None),
|
||||
user=Depends(require_developer),
|
||||
):
|
||||
app = get_app(app_id)
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="App not found")
|
||||
|
||||
rule_ip = clean_optional(ip_cidr)
|
||||
rule_methods = clean_optional(methods).upper()
|
||||
if not rule_ip:
|
||||
return redirect_app_detail(app_id, anchor="ip-access", error="IP/CIDR nesmí být prázdné.")
|
||||
if not rule_methods:
|
||||
return redirect_app_detail(app_id, anchor="ip-access", error="Methods nesmí být prázdné.")
|
||||
|
||||
create_app_ip_access_rule(app_id, rule_ip, rule_methods, description.strip(), bool(is_enabled))
|
||||
log_audit_event(
|
||||
user,
|
||||
action="app.ip_access_rule.created",
|
||||
target_type="app",
|
||||
target_id=app_id,
|
||||
metadata={"ip_cidr": rule_ip, "methods": rule_methods, "is_enabled": bool(is_enabled)},
|
||||
)
|
||||
return redirect_app_detail(app_id, anchor="ip-access", message="IP access pravidlo přidáno.")
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/ip-access-rules/{rule_id}/update")
|
||||
def save_app_ip_access_rule(
|
||||
app_id: str,
|
||||
rule_id: int,
|
||||
ip_cidr: str = Form(...),
|
||||
methods: str = Form("WRITE"),
|
||||
description: str = Form(""),
|
||||
is_enabled: str | None = Form(None),
|
||||
user=Depends(require_developer),
|
||||
):
|
||||
app = get_app(app_id)
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="App not found")
|
||||
|
||||
existing = get_app_ip_access_rule(rule_id, app_id)
|
||||
if not existing:
|
||||
return redirect_app_detail(app_id, anchor="ip-access", error="IP access pravidlo nebylo nalezeno.")
|
||||
|
||||
rule_ip = clean_optional(ip_cidr)
|
||||
rule_methods = clean_optional(methods).upper()
|
||||
if not rule_ip:
|
||||
return redirect_app_detail(app_id, anchor="ip-access", error="IP/CIDR nesmí být prázdné.")
|
||||
if not rule_methods:
|
||||
return redirect_app_detail(app_id, anchor="ip-access", error="Methods nesmí být prázdné.")
|
||||
|
||||
update_app_ip_access_rule(rule_id, app_id, rule_ip, rule_methods, description.strip(), bool(is_enabled))
|
||||
log_audit_event(
|
||||
user,
|
||||
action="app.ip_access_rule.updated",
|
||||
target_type="app",
|
||||
target_id=app_id,
|
||||
metadata={"rule_id": rule_id, "ip_cidr": rule_ip, "methods": rule_methods, "is_enabled": bool(is_enabled)},
|
||||
)
|
||||
return redirect_app_detail(app_id, anchor="ip-access", message="IP access pravidlo upraveno.")
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/ip-access-rules/{rule_id}/delete")
|
||||
def remove_app_ip_access_rule(app_id: str, rule_id: int, user=Depends(require_developer)):
|
||||
app = get_app(app_id)
|
||||
if not app:
|
||||
raise HTTPException(status_code=404, detail="App not found")
|
||||
|
||||
existing = get_app_ip_access_rule(rule_id, app_id)
|
||||
if not existing:
|
||||
return redirect_app_detail(app_id, anchor="ip-access", error="IP access pravidlo nebylo nalezeno.")
|
||||
|
||||
delete_app_ip_access_rule(rule_id, app_id)
|
||||
log_audit_event(
|
||||
user,
|
||||
action="app.ip_access_rule.deleted",
|
||||
target_type="app",
|
||||
target_id=app_id,
|
||||
metadata={"rule_id": rule_id, "ip_cidr": existing.get("ip_cidr"), "methods": existing.get("methods")},
|
||||
)
|
||||
return redirect_app_detail(app_id, anchor="ip-access", message="IP access pravidlo smazáno.")
|
||||
|
||||
|
||||
@router.post("/apps/{app_id}/redeploy")
|
||||
def redeploy_app(app_id: str, user=Depends(require_developer)):
|
||||
app = get_app(app_id)
|
||||
|
||||
Reference in New Issue
Block a user