mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 01:13:00 +02:00
webui: firewall — zone matrix, drill-down, conditional detection
Add a zone-to-zone traffic matrix matching the CLI's traffic_flow logic: - HOST row/column always present; zone→HOST uses zone action + services (not policies), producing allow/conditional/deny verdicts correctly - Clicking a matrix cell shows a detail panel with the policy or zone reason for the verdict - Conditional (⚠) detection for zones with services or port-forwarding Enrich the policy and zone tables: - Immutable (built-in) policies flagged with ⚷ in column header and row - Policy rows show description as tooltip; sorted by priority - Zones table renamed "Host Services" column; Policies drops Priority column - Wide tables (Zones, Policies) span two grid columns like Dashboard WiFi Fix the info-grid layout: #content lacked width:100% so margin:0 auto was overriding flex stretch, collapsing the grid to a single column on both the firewall and dashboard pages. Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -30,6 +31,7 @@ type firewallJSON struct {
|
||||
type zoneJSON struct {
|
||||
Name string `json:"name"`
|
||||
Action string `json:"action"`
|
||||
Description string `json:"description"`
|
||||
Interface []string `json:"interface"`
|
||||
Network []string `json:"network"`
|
||||
Service []string `json:"service"`
|
||||
@@ -45,30 +47,31 @@ type portForwardJSON struct {
|
||||
}
|
||||
|
||||
type policyJSON struct {
|
||||
Name string `json:"name"`
|
||||
Action string `json:"action"`
|
||||
Priority yangInt64 `json:"priority"`
|
||||
Ingress []string `json:"ingress"`
|
||||
Egress []string `json:"egress"`
|
||||
Service []string `json:"service"`
|
||||
Masquerade bool `json:"masquerade"`
|
||||
Immutable bool `json:"immutable"`
|
||||
Name string `json:"name"`
|
||||
Action string `json:"action"`
|
||||
Description string `json:"description"`
|
||||
Priority yangInt64 `json:"priority"`
|
||||
Ingress []string `json:"ingress"`
|
||||
Egress []string `json:"egress"`
|
||||
Service []string `json:"service"`
|
||||
Masquerade bool `json:"masquerade"`
|
||||
Immutable bool `json:"immutable"`
|
||||
}
|
||||
|
||||
// Template data structures.
|
||||
|
||||
type firewallData struct {
|
||||
PageData
|
||||
Enabled bool
|
||||
EnabledText string
|
||||
DefaultZone string
|
||||
Lockdown bool
|
||||
Logging string
|
||||
ZoneNames []string
|
||||
Matrix []matrixRow
|
||||
Zones []zoneEntry
|
||||
Policies []policyEntry
|
||||
Error string
|
||||
Enabled bool
|
||||
EnabledText string
|
||||
DefaultZone string
|
||||
Lockdown bool
|
||||
Logging string
|
||||
ZoneNames []string
|
||||
Matrix []matrixRow
|
||||
Zones []zoneEntry
|
||||
Policies []policyEntry
|
||||
Error string
|
||||
}
|
||||
|
||||
type matrixRow struct {
|
||||
@@ -77,8 +80,12 @@ type matrixRow struct {
|
||||
}
|
||||
|
||||
type matrixCell struct {
|
||||
Class string
|
||||
Symbol string
|
||||
Class string
|
||||
Symbol string
|
||||
Verdict string // "allow", "deny", "conditional", "self"
|
||||
From string
|
||||
To string
|
||||
Detail string // human-readable reason for the drill-down panel
|
||||
}
|
||||
|
||||
type zoneEntry struct {
|
||||
@@ -86,17 +93,19 @@ type zoneEntry struct {
|
||||
Action string
|
||||
Interfaces string
|
||||
Networks string
|
||||
Services string
|
||||
Services string // services allowed to HOST from this zone
|
||||
}
|
||||
|
||||
type policyEntry struct {
|
||||
Name string
|
||||
Action string
|
||||
Priority int64
|
||||
Ingress string
|
||||
Egress string
|
||||
Services string
|
||||
Masquerade bool
|
||||
Name string
|
||||
Action string
|
||||
Priority int64
|
||||
Ingress string
|
||||
Egress string
|
||||
Services string
|
||||
Masquerade bool
|
||||
Immutable bool
|
||||
Description string
|
||||
}
|
||||
|
||||
// FirewallHandler serves the firewall overview page.
|
||||
@@ -112,10 +121,18 @@ func (h *FirewallHandler) Overview(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var fw firewallWrapper
|
||||
if err := h.RC.Get(r.Context(), "/data/infix-firewall:firewall", &fw); err != nil {
|
||||
log.Printf("restconf firewall: %v", err)
|
||||
data.Error = "Could not fetch firewall configuration"
|
||||
} else {
|
||||
err := h.RC.Get(r.Context(), "/data/infix-firewall:firewall", &fw)
|
||||
if err != nil {
|
||||
var rcErr *restconf.Error
|
||||
if errors.As(err, &rcErr) && rcErr.StatusCode == http.StatusNotFound {
|
||||
// Firewall module not active — show disabled state, not an error.
|
||||
data.EnabledText = "Inactive"
|
||||
} else {
|
||||
log.Printf("restconf firewall: %v", err)
|
||||
data.Error = "Could not fetch firewall configuration"
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
f := fw.Firewall
|
||||
data.Enabled = f.Enabled == nil || bool(*f.Enabled)
|
||||
if data.Enabled {
|
||||
@@ -142,13 +159,15 @@ func (h *FirewallHandler) Overview(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
for _, p := range f.Policy {
|
||||
data.Policies = append(data.Policies, policyEntry{
|
||||
Name: p.Name,
|
||||
Action: p.Action,
|
||||
Priority: int64(p.Priority),
|
||||
Ingress: strings.Join(p.Ingress, ", "),
|
||||
Egress: strings.Join(p.Egress, ", "),
|
||||
Services: strings.Join(p.Service, ", "),
|
||||
Masquerade: p.Masquerade,
|
||||
Name: p.Name,
|
||||
Action: p.Action,
|
||||
Priority: int64(p.Priority),
|
||||
Ingress: strings.Join(p.Ingress, ", "),
|
||||
Egress: strings.Join(p.Egress, ", "),
|
||||
Services: strings.Join(p.Service, ", "),
|
||||
Masquerade: p.Masquerade,
|
||||
Immutable: p.Immutable,
|
||||
Description: p.Description,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -179,10 +198,10 @@ func buildMatrix(zones []zoneJSON, policies []policyJSON) ([]string, []matrixRow
|
||||
}
|
||||
|
||||
names := []string{"HOST"}
|
||||
zoneAction := map[string]string{}
|
||||
zoneByName := map[string]zoneJSON{}
|
||||
for _, z := range zones {
|
||||
names = append(names, z.Name)
|
||||
zoneAction[z.Name] = z.Action
|
||||
zoneByName[z.Name] = z
|
||||
}
|
||||
|
||||
// Sort policies by priority for evaluation.
|
||||
@@ -196,21 +215,24 @@ func buildMatrix(zones []zoneJSON, policies []policyJSON) ([]string, []matrixRow
|
||||
for i, src := range names {
|
||||
rows[i] = matrixRow{Zone: src, Cells: make([]matrixCell, len(names))}
|
||||
for j, dst := range names {
|
||||
var cell matrixCell
|
||||
switch {
|
||||
case src == "HOST" && dst == "HOST":
|
||||
rows[i].Cells[j] = matrixCell{Class: "matrix-self", Symbol: "\u2014"}
|
||||
case src == "HOST":
|
||||
// Traffic originating from the device is always allowed.
|
||||
rows[i].Cells[j] = matrixCell{Class: "matrix-allow", Symbol: "\u2713"}
|
||||
case src == dst:
|
||||
rows[i].Cells[j] = matrixCell{Class: "matrix-self", Symbol: "\u2014"}
|
||||
cell = matrixCell{Class: "matrix-self", Symbol: "—", Verdict: "self"}
|
||||
case src == "HOST":
|
||||
// Traffic from the device to any zone is always allowed.
|
||||
cell = matrixCell{Class: "matrix-allow", Symbol: "✓", Verdict: "allow",
|
||||
Detail: "HOST can reach all zones"}
|
||||
case dst == "HOST":
|
||||
// Input to device: governed by zone action + policies.
|
||||
rows[i].Cells[j] = zoneToHost(src, zoneAction, sorted)
|
||||
// Input to device: governed by zone action + zone services.
|
||||
cell = zoneToHost(zoneByName[src])
|
||||
default:
|
||||
// Forwarding between zones: governed by policies.
|
||||
rows[i].Cells[j] = evalForward(src, dst, sorted)
|
||||
cell = evalForward(src, dst, sorted)
|
||||
}
|
||||
cell.From = src
|
||||
cell.To = dst
|
||||
rows[i].Cells[j] = cell
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,30 +240,38 @@ func buildMatrix(zones []zoneJSON, policies []policyJSON) ([]string, []matrixRow
|
||||
}
|
||||
|
||||
// zoneToHost determines traffic flow from a zone to the device (HOST).
|
||||
// Policies are checked first; if none gives a verdict, the zone's
|
||||
// default action is used.
|
||||
func zoneToHost(zone string, zoneAction map[string]string, policies []policyJSON) matrixCell {
|
||||
if v := evalPolicies(zone, "HOST", policies); v != "" {
|
||||
return makeCell(v)
|
||||
// This mirrors the CLI: it is based solely on the zone's action and services,
|
||||
// not on policies (per cli_pretty.py traffic_flow logic).
|
||||
func zoneToHost(zone zoneJSON) matrixCell {
|
||||
if zone.Action == "accept" {
|
||||
return matrixCell{Class: "matrix-allow", Symbol: "✓", Verdict: "allow",
|
||||
Detail: "Zone default action: accept"}
|
||||
}
|
||||
if zoneAction[zone] == "accept" {
|
||||
return matrixCell{Class: "matrix-allow", Symbol: "\u2713"}
|
||||
if len(zone.Service) > 0 || len(zone.PortForward) > 0 {
|
||||
if len(zone.Service) > 0 {
|
||||
return matrixCell{Class: "matrix-cond", Symbol: "⚠", Verdict: "conditional",
|
||||
Detail: "Services: " + strings.Join(zone.Service, ", ")}
|
||||
}
|
||||
return matrixCell{Class: "matrix-cond", Symbol: "⚠", Verdict: "conditional",
|
||||
Detail: "Port-forwarding rules apply"}
|
||||
}
|
||||
return matrixCell{Class: "matrix-deny", Symbol: "\u2717"}
|
||||
return matrixCell{Class: "matrix-deny", Symbol: "✗", Verdict: "deny",
|
||||
Detail: "Zone default action: " + zone.Action}
|
||||
}
|
||||
|
||||
// evalForward determines traffic flow between two different zones.
|
||||
// evalForward determines traffic flow between two different zones via policies.
|
||||
func evalForward(src, dst string, policies []policyJSON) matrixCell {
|
||||
if v := evalPolicies(src, dst, policies); v != "" {
|
||||
return makeCell(v)
|
||||
if v, name := evalPolicies(src, dst, policies); v != "" {
|
||||
return makeCell(v, name)
|
||||
}
|
||||
return matrixCell{Class: "matrix-deny", Symbol: "\u2717"}
|
||||
return matrixCell{Class: "matrix-deny", Symbol: "✗", Verdict: "deny",
|
||||
Detail: "No policy — default deny"}
|
||||
}
|
||||
|
||||
// evalPolicies walks the sorted policy list and returns the first terminal
|
||||
// verdict (accept/reject/drop) for traffic from src to dst.
|
||||
// verdict (accept/reject/drop) for traffic from src to dst, plus the policy name.
|
||||
// "continue" policies are skipped (they don't produce a final verdict).
|
||||
func evalPolicies(src, dst string, policies []policyJSON) string {
|
||||
func evalPolicies(src, dst string, policies []policyJSON) (verdict, name string) {
|
||||
for _, p := range policies {
|
||||
if !matchesZone(src, p.Ingress) || !matchesZone(dst, p.Egress) {
|
||||
continue
|
||||
@@ -249,9 +279,9 @@ func evalPolicies(src, dst string, policies []policyJSON) string {
|
||||
if p.Action == "continue" {
|
||||
continue
|
||||
}
|
||||
return p.Action
|
||||
return p.Action, p.Name
|
||||
}
|
||||
return ""
|
||||
return "", ""
|
||||
}
|
||||
|
||||
// matchesZone checks whether zone appears in list, treating "ANY" as a wildcard.
|
||||
@@ -264,9 +294,11 @@ func matchesZone(zone string, list []string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func makeCell(verdict string) matrixCell {
|
||||
func makeCell(verdict, policyName string) matrixCell {
|
||||
if verdict == "accept" {
|
||||
return matrixCell{Class: "matrix-allow", Symbol: "\u2713"}
|
||||
return matrixCell{Class: "matrix-allow", Symbol: "✓", Verdict: "allow",
|
||||
Detail: "Policy: " + policyName + " (accept)"}
|
||||
}
|
||||
return matrixCell{Class: "matrix-deny", Symbol: "\u2717"}
|
||||
return matrixCell{Class: "matrix-deny", Symbol: "✗", Verdict: "deny",
|
||||
Detail: "Policy: " + policyName + " (" + verdict + ")"}
|
||||
}
|
||||
|
||||
@@ -198,10 +198,12 @@ a:hover {
|
||||
|
||||
#content {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 2rem;
|
||||
overflow-y: auto;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -953,26 +955,63 @@ details[open].nav-group-top > summary.nav-group-summary-top::after {
|
||||
color: #991b1b;
|
||||
font-weight: 700;
|
||||
}
|
||||
.matrix-cond {
|
||||
background: #fef9c3;
|
||||
color: #854d0e;
|
||||
font-weight: 700;
|
||||
}
|
||||
.matrix-self {
|
||||
background: var(--slate-100);
|
||||
color: var(--slate-400);
|
||||
}
|
||||
/* Clickable cells */
|
||||
.matrix-cell[data-verdict] {
|
||||
cursor: pointer;
|
||||
}
|
||||
.matrix-cell[data-verdict]:hover {
|
||||
filter: brightness(0.93);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.matrix-allow { background: rgba(22, 163, 74, 0.2); color: #86efac; }
|
||||
.matrix-deny { background: rgba(220, 38, 38, 0.2); color: #fca5a5; }
|
||||
.matrix-self { background: var(--slate-800); color: var(--slate-600); }
|
||||
.matrix-deny { background: rgba(220, 38, 38, 0.2); color: #fca5a5; }
|
||||
.matrix-cond { background: rgba(234, 179, 8, 0.2); color: #fde047; }
|
||||
.matrix-self { background: var(--slate-800); color: var(--slate-600); }
|
||||
}
|
||||
.dark .matrix-allow { background: rgba(22, 163, 74, 0.2); color: #86efac; }
|
||||
.dark .matrix-deny { background: rgba(220, 38, 38, 0.2); color: #fca5a5; }
|
||||
.dark .matrix-self { background: var(--slate-800); color: var(--slate-600); }
|
||||
.dark .matrix-deny { background: rgba(220, 38, 38, 0.2); color: #fca5a5; }
|
||||
.dark .matrix-cond { background: rgba(234, 179, 8, 0.2); color: #fde047; }
|
||||
.dark .matrix-self { background: var(--slate-800); color: var(--slate-600); }
|
||||
.light .matrix-cond { background: #fef9c3; color: #854d0e; }
|
||||
|
||||
/* Flow detail panel — shown below matrix when a cell is clicked.
|
||||
display:flex overrides HTML [hidden], so we restore that here. */
|
||||
.fw-flow-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--surface-alt);
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.fw-flow-detail[hidden] { display: none !important; }
|
||||
.fw-detail-flow { font-weight: 600; }
|
||||
.fw-detail-sep { color: var(--fg-muted); }
|
||||
.fw-detail-text { color: var(--fg-muted); }
|
||||
|
||||
/* Policy immutable lock icon */
|
||||
.policy-lock-col { width: 1.5rem; min-width: 1.5rem; padding: 0 0.25rem !important; text-align: center; }
|
||||
.policy-lock { color: var(--fg-muted); font-size: 0.85rem; user-select: none; }
|
||||
.data-table td.policy-lock-col,
|
||||
.data-table th.policy-lock-col { vertical-align: middle; }
|
||||
|
||||
.matrix-legend {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--fg-muted);
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
@@ -982,10 +1021,15 @@ details[open].nav-group-top > summary.nav-group-summary-top::after {
|
||||
}
|
||||
|
||||
.legend-swatch {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
|
||||
@@ -432,6 +432,28 @@
|
||||
});
|
||||
})();
|
||||
|
||||
// Firewall zone matrix cell drill-down
|
||||
(function() {
|
||||
var verdictLabel = { allow: '✓ Allow', deny: '✗ Deny', conditional: '⚠ Conditional' };
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
var td = e.target.closest('.matrix-cell[data-verdict]');
|
||||
var panel = document.getElementById('fw-flow-detail');
|
||||
if (!panel) return;
|
||||
|
||||
if (!td) {
|
||||
panel.hidden = true;
|
||||
return;
|
||||
}
|
||||
panel.querySelector('.fw-detail-flow').textContent =
|
||||
td.getAttribute('data-from') + ' → ' + td.getAttribute('data-to');
|
||||
panel.querySelector('.fw-detail-verdict').textContent =
|
||||
verdictLabel[td.getAttribute('data-verdict')] || td.getAttribute('data-verdict');
|
||||
panel.querySelector('.fw-detail-text').textContent = td.getAttribute('data-detail') || '';
|
||||
panel.hidden = false;
|
||||
});
|
||||
})();
|
||||
|
||||
// Keystore key detail row toggle
|
||||
(function() {
|
||||
document.addEventListener('click', function(e) {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{{end}}
|
||||
|
||||
<section class="info-grid">
|
||||
|
||||
<div class="info-card">
|
||||
<div class="card-header">Firewall Status</div>
|
||||
<table class="info-table">
|
||||
@@ -23,100 +24,116 @@
|
||||
{{end}}
|
||||
<tr>
|
||||
<th>Lockdown Mode</th>
|
||||
<td>{{if .Lockdown}}Active{{else}}Inactive{{end}}</td>
|
||||
<td>{{if .Lockdown}}<span class="badge badge-drop">Active</span>{{else}}Inactive{{end}}</td>
|
||||
</tr>
|
||||
<tr><th>Log Denied Traffic</th><td>{{.Logging}}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{if .Matrix}}
|
||||
<section class="info-card">
|
||||
<div class="card-header">Zone Matrix</div>
|
||||
<div class="matrix-wrap">
|
||||
<table class="zone-matrix">
|
||||
{{if .Matrix}}
|
||||
<div class="info-card">
|
||||
<div class="card-header">Zone Matrix</div>
|
||||
<div class="matrix-wrap">
|
||||
<table class="zone-matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="matrix-corner">→</th>
|
||||
{{range .ZoneNames}}<th>{{.}}</th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Matrix}}
|
||||
<tr>
|
||||
<th>{{.Zone}}</th>
|
||||
{{range .Cells}}
|
||||
<td class="matrix-cell {{.Class}}"
|
||||
{{if ne .Verdict "self"}}
|
||||
data-from="{{.From}}"
|
||||
data-to="{{.To}}"
|
||||
data-verdict="{{.Verdict}}"
|
||||
data-detail="{{.Detail}}"
|
||||
{{end}}>{{.Symbol}}</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="fw-flow-detail" class="fw-flow-detail" hidden>
|
||||
<span class="fw-detail-flow"></span>
|
||||
<span class="fw-detail-verdict"></span>
|
||||
<span class="fw-detail-sep">—</span>
|
||||
<span class="fw-detail-text"></span>
|
||||
</div>
|
||||
<div class="matrix-legend">
|
||||
<span class="legend-item"><span class="legend-swatch matrix-allow">✓</span> Allow</span>
|
||||
<span class="legend-item"><span class="legend-swatch matrix-cond">⚠</span> Conditional</span>
|
||||
<span class="legend-item"><span class="legend-swatch matrix-deny">✗</span> Deny</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Zones}}
|
||||
<div class="info-card info-grid-span-2">
|
||||
<div class="card-header">Zones</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="matrix-corner">→</th>
|
||||
{{range .ZoneNames}}<th><span class="zone-badge">{{.}}</span></th>{{end}}
|
||||
<th>Name</th>
|
||||
<th>Action</th>
|
||||
<th>Interfaces</th>
|
||||
<th>Networks</th>
|
||||
<th>Host Services</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Matrix}}
|
||||
{{range .Zones}}
|
||||
<tr>
|
||||
<th>{{.Zone}}</th>
|
||||
{{range .Cells}}<td class="{{.Class}}">{{.Symbol}}</td>{{end}}
|
||||
<td>{{.Name}}</td>
|
||||
<td><span class="badge badge-{{.Action}}">{{.Action}}</span></td>
|
||||
<td>{{.Interfaces}}</td>
|
||||
<td>{{if .Networks}}{{.Networks}}{{else}}<span class="text-muted">—</span>{{end}}</td>
|
||||
<td>{{if .Services}}{{.Services}}{{else}}<span class="text-muted">—</span>{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="matrix-legend">
|
||||
<span class="legend-item"><span class="legend-swatch matrix-allow"></span> Allow</span>
|
||||
<span class="legend-item"><span class="legend-swatch matrix-deny"></span> Deny</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .Zones}}
|
||||
<section class="info-card">
|
||||
<div class="card-header">Zones</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Action</th>
|
||||
<th>Interfaces</th>
|
||||
<th>Networks</th>
|
||||
<th>Services</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Zones}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td><span class="badge badge-{{.Action}}">{{.Action}}</span></td>
|
||||
<td>{{.Interfaces}}</td>
|
||||
<td>{{.Networks}}</td>
|
||||
<td>{{.Services}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{if .Policies}}
|
||||
<div class="info-card info-grid-span-2">
|
||||
<div class="card-header">Policies</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="policy-lock-col" title="Immutable">⚷</th>
|
||||
<th>Name</th>
|
||||
<th>Action</th>
|
||||
<th>Ingress</th>
|
||||
<th>Egress</th>
|
||||
<th>Services</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Policies}}
|
||||
<tr{{if .Description}} title="{{.Description}}"{{end}}>
|
||||
<td class="policy-lock-col">{{if .Immutable}}<span class="policy-lock" title="Built-in policy">⚷</span>{{end}}</td>
|
||||
<td>{{.Name}}</td>
|
||||
<td><span class="badge badge-{{.Action}}">{{.Action}}</span></td>
|
||||
<td>{{.Ingress}}</td>
|
||||
<td>{{.Egress}}</td>
|
||||
<td>{{if .Services}}{{.Services}} {{end}}{{if .Masquerade}}<span class="badge badge-neutral">masq</span>{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{if .Policies}}
|
||||
<section class="info-card">
|
||||
<div class="card-header">Policies</div>
|
||||
<div class="data-table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Priority</th>
|
||||
<th>Action</th>
|
||||
<th>Ingress</th>
|
||||
<th>Egress</th>
|
||||
<th>Services</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Policies}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td class="num">{{.Priority}}</td>
|
||||
<td><span class="badge badge-{{.Action}}">{{.Action}}</span></td>
|
||||
<td>{{.Ingress}}</td>
|
||||
<td>{{.Egress}}</td>
|
||||
<td>{{.Services}}{{if .Masquerade}} <span class="badge badge-continue">masq</span>{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user