@@ -0,0 +1,271 @@
|
||||
package master
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/example/sessionguard/internal/auth"
|
||||
"github.com/example/sessionguard/internal/config"
|
||||
"github.com/example/sessionguard/internal/httpx"
|
||||
"github.com/example/sessionguard/internal/model"
|
||||
)
|
||||
|
||||
const Version = "0.1.0"
|
||||
|
||||
type App struct {
|
||||
cfg config.Master
|
||||
store *store
|
||||
auth *auth.Manager
|
||||
}
|
||||
|
||||
func New(ctx context.Context, cfg config.Master) (*App, error) {
|
||||
s, err := newStore(cfg.DataFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a, err := auth.New(ctx, cfg.OIDC)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("OIDC: %w", err)
|
||||
}
|
||||
return &App{cfg: cfg, store: s, auth: a}, nil
|
||||
}
|
||||
|
||||
func (a *App) Run(ctx context.Context) error {
|
||||
mux := http.NewServeMux()
|
||||
a.auth.Register(mux)
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
httpx.JSON(w, 200, map[string]any{"ok": true, "version": Version})
|
||||
})
|
||||
mux.HandleFunc("POST /api/v1/agents/enroll", a.enroll)
|
||||
mux.HandleFunc("POST /api/v1/agents/heartbeat", a.heartbeat)
|
||||
mux.HandleFunc("GET /app.js", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/javascript; charset=utf-8")
|
||||
_, _ = fmt.Fprint(w, masterJS)
|
||||
})
|
||||
mux.Handle("GET /", a.auth.Require(http.HandlerFunc(a.masterPage)))
|
||||
mux.Handle("GET /api/v1/dashboard", a.auth.Require(http.HandlerFunc(a.dashboard)))
|
||||
mux.Handle("GET /api/v1/agents/{id}", a.auth.Require(http.HandlerFunc(a.agentDetail)))
|
||||
mux.Handle("PUT /api/v1/agents/{id}/policy", a.auth.Require(http.HandlerFunc(a.policy)))
|
||||
mux.Handle("PUT /api/v1/policy/all", a.auth.Require(http.HandlerFunc(a.policyAll)))
|
||||
server := &http.Server{Addr: a.cfg.Listen, Handler: securityHeaders(mux), ReadHeaderTimeout: 5 * time.Second}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = server.Shutdown(c)
|
||||
}()
|
||||
log.Printf("master listening on %s", a.cfg.Listen)
|
||||
err := server.ListenAndServe()
|
||||
if err == http.ErrServerClosed {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) enroll(w http.ResponseWriter, r *http.Request) {
|
||||
var req model.EnrollRequest
|
||||
if err := httpx.DecodeJSON(r, &req, 64<<10); err != nil {
|
||||
httpx.Error(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if !constantEqual(req.EnrollmentToken, a.cfg.EnrollmentToken) || req.MachineID == "" {
|
||||
httpx.Error(w, 401, "invalid enrollment")
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
token := randomToken(32)
|
||||
id := randomToken(16)
|
||||
a.store.mu.Lock()
|
||||
defer a.store.mu.Unlock()
|
||||
for oldID, rec := range a.store.data.Agents {
|
||||
if rec.MachineID == req.MachineID {
|
||||
id = oldID
|
||||
break
|
||||
}
|
||||
}
|
||||
rec := a.store.data.Agents[id]
|
||||
rec.ID = id
|
||||
rec.Name = req.Name
|
||||
rec.MachineID = req.MachineID
|
||||
rec.TokenHash = hashToken(token)
|
||||
if rec.EnrolledAt.IsZero() {
|
||||
rec.EnrolledAt = now
|
||||
}
|
||||
a.store.data.Agents[id] = rec
|
||||
if err := a.store.saveLocked(); err != nil {
|
||||
httpx.Error(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.JSON(w, 200, model.EnrollResponse{AgentID: id, Token: token})
|
||||
}
|
||||
|
||||
func (a *App) heartbeat(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.Header.Get("X-Agent-ID")
|
||||
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
|
||||
if id == "" || token == "" {
|
||||
httpx.Error(w, 401, "missing agent credentials")
|
||||
return
|
||||
}
|
||||
var snap model.AgentSnapshot
|
||||
if err := httpx.DecodeJSON(r, &snap, 2<<20); err != nil {
|
||||
httpx.Error(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
a.store.mu.Lock()
|
||||
defer a.store.mu.Unlock()
|
||||
rec, ok := a.store.data.Agents[id]
|
||||
if !ok || !constantEqual(hashToken(token), rec.TokenHash) {
|
||||
httpx.Error(w, 401, "invalid agent credentials")
|
||||
return
|
||||
}
|
||||
if snap.ProtocolVersion != model.ProtocolVersion {
|
||||
httpx.Error(w, 409, "protocol version mismatch")
|
||||
return
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
snap.AgentID = id
|
||||
rec.LastSeen = now
|
||||
rec.Snapshot = snap
|
||||
if snap.Server.Hostname != "" {
|
||||
rec.Name = snap.Server.Hostname
|
||||
}
|
||||
a.store.data.Agents[id] = rec
|
||||
if err := a.store.saveLocked(); err != nil {
|
||||
httpx.Error(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
var desired *model.Policy
|
||||
if rec.DesiredPolicy != nil && rec.DesiredPolicy.Revision != snap.PolicyRevision {
|
||||
p := *rec.DesiredPolicy
|
||||
desired = &p
|
||||
}
|
||||
httpx.JSON(w, 200, model.HeartbeatResponse{DesiredPolicy: desired, ServerTime: now})
|
||||
}
|
||||
|
||||
func (a *App) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
recs := a.store.all()
|
||||
sort.Slice(recs, func(i, j int) bool { return strings.ToLower(recs[i].Name) < strings.ToLower(recs[j].Name) })
|
||||
now := time.Now().UTC()
|
||||
type row struct {
|
||||
model.AgentRecord
|
||||
Online bool `json:"online"`
|
||||
Active int `json:"active_sessions"`
|
||||
Total int `json:"total_sessions"`
|
||||
}
|
||||
out := make([]row, 0, len(recs))
|
||||
for _, rec := range recs {
|
||||
active := 0
|
||||
for _, s := range rec.Snapshot.Sessions {
|
||||
if s.State == "Active" {
|
||||
active++
|
||||
}
|
||||
}
|
||||
out = append(out, row{AgentRecord: rec, Online: now.Sub(rec.LastSeen) < time.Duration(a.cfg.OfflineAfterSeconds)*time.Second, Active: active, Total: len(rec.Snapshot.Sessions)})
|
||||
}
|
||||
httpx.JSON(w, 200, map[string]any{"agents": out, "server_time": now})
|
||||
}
|
||||
func (a *App) agentDetail(w http.ResponseWriter, r *http.Request) {
|
||||
rec, ok := a.store.get(r.PathValue("id"))
|
||||
if !ok {
|
||||
httpx.Error(w, 404, "agent not found")
|
||||
return
|
||||
}
|
||||
httpx.JSON(w, 200, rec)
|
||||
}
|
||||
func (a *App) policy(w http.ResponseWriter, r *http.Request) {
|
||||
if !httpx.SameOrigin(r) {
|
||||
httpx.Error(w, 403, "cross-origin request rejected")
|
||||
return
|
||||
}
|
||||
id := r.PathValue("id")
|
||||
var p model.Policy
|
||||
if err := httpx.DecodeJSON(r, &p, 2<<20); err != nil {
|
||||
httpx.Error(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if p.Cleanup.GraceSeconds < 1 || p.Cleanup.PollSeconds < 2 {
|
||||
httpx.Error(w, 400, "invalid cleanup timing")
|
||||
return
|
||||
}
|
||||
p.Revision = randomToken(12)
|
||||
p.UpdatedAt = time.Now().UTC()
|
||||
a.store.mu.Lock()
|
||||
defer a.store.mu.Unlock()
|
||||
rec, ok := a.store.data.Agents[id]
|
||||
if !ok {
|
||||
httpx.Error(w, 404, "agent not found")
|
||||
return
|
||||
}
|
||||
rec.DesiredPolicy = &p
|
||||
a.store.data.Agents[id] = rec
|
||||
if err := a.store.saveLocked(); err != nil {
|
||||
httpx.Error(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.JSON(w, 200, p)
|
||||
}
|
||||
func (a *App) policyAll(w http.ResponseWriter, r *http.Request) {
|
||||
if !httpx.SameOrigin(r) {
|
||||
httpx.Error(w, 403, "cross-origin request rejected")
|
||||
return
|
||||
}
|
||||
var p model.Policy
|
||||
if err := httpx.DecodeJSON(r, &p, 2<<20); err != nil {
|
||||
httpx.Error(w, 400, err.Error())
|
||||
return
|
||||
}
|
||||
if p.Cleanup.GraceSeconds < 1 || p.Cleanup.PollSeconds < 2 {
|
||||
httpx.Error(w, 400, "invalid cleanup timing")
|
||||
return
|
||||
}
|
||||
p.Revision = randomToken(12)
|
||||
p.UpdatedAt = time.Now().UTC()
|
||||
a.store.mu.Lock()
|
||||
defer a.store.mu.Unlock()
|
||||
for id, rec := range a.store.data.Agents {
|
||||
cp := p
|
||||
rec.DesiredPolicy = &cp
|
||||
a.store.data.Agents[id] = rec
|
||||
}
|
||||
if err := a.store.saveLocked(); err != nil {
|
||||
httpx.Error(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.JSON(w, 200, map[string]any{"updated_agents": len(a.store.data.Agents), "policy": p})
|
||||
}
|
||||
|
||||
func (a *App) masterPage(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = fmt.Fprint(w, masterHTML)
|
||||
}
|
||||
|
||||
func randomToken(n int) string {
|
||||
b := make([]byte, n)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
func hashToken(s string) string { h := sha256.Sum256([]byte(s)); return hex.EncodeToString(h[:]) }
|
||||
func constantEqual(x, y string) bool {
|
||||
if len(x) != len(y) {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare([]byte(x), []byte(y)) == 1
|
||||
}
|
||||
func securityHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Referrer-Policy", "same-origin")
|
||||
w.Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'; img-src 'self' data:")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package master
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/example/sessionguard/internal/config"
|
||||
"github.com/example/sessionguard/internal/model"
|
||||
)
|
||||
|
||||
type data struct {
|
||||
Agents map[string]model.AgentRecord `json:"agents"`
|
||||
}
|
||||
|
||||
type store struct {
|
||||
path string
|
||||
mu sync.RWMutex
|
||||
data data
|
||||
}
|
||||
|
||||
func newStore(path string) (*store, error) {
|
||||
s := &store{path: path, data: data{Agents: map[string]model.AgentRecord{}}}
|
||||
b, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return s, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(b, &s.data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.data.Agents == nil {
|
||||
s.data.Agents = map[string]model.AgentRecord{}
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
func (s *store) saveLocked() error { return config.SaveJSON(s.path, s.data) }
|
||||
func (s *store) all() []model.AgentRecord {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
out := make([]model.AgentRecord, 0, len(s.data.Agents))
|
||||
for _, a := range s.data.Agents {
|
||||
a.TokenHash = ""
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
func (s *store) get(id string) (model.AgentRecord, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
a, ok := s.data.Agents[id]
|
||||
a.TokenHash = ""
|
||||
return a, ok
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package master
|
||||
|
||||
const masterHTML = `<!doctype html>
|
||||
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>SessionGuard Master</title><style>
|
||||
:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif;color-scheme:dark;background:#0b1020;color:#e8edf7}*{box-sizing:border-box}body{margin:0;background:linear-gradient(135deg,#0b1020,#111a31);min-height:100vh}.wrap{max-width:1500px;margin:auto;padding:28px}.top{display:flex;align-items:center;justify-content:space-between;margin-bottom:22px}.brand{display:flex;gap:12px;align-items:center}.logo{width:42px;height:42px;border-radius:12px;background:#5b8cff;display:grid;place-items:center;font-weight:800}.muted{color:#95a3be}.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.card,.panel{background:rgba(18,27,49,.85);border:1px solid #263654;border-radius:16px;box-shadow:0 14px 35px rgba(0,0,0,.18)}.card{padding:18px}.value{font-size:28px;font-weight:750;margin-top:7px}.panel{margin-top:16px;overflow:hidden}.panel h2{font-size:16px;margin:0;padding:16px 18px;border-bottom:1px solid #263654}.split{display:grid;grid-template-columns:1.05fr 1.45fr;gap:16px}.table{width:100%;border-collapse:collapse}.table th,.table td{padding:12px 14px;border-bottom:1px solid #22314e;text-align:left;font-size:13px}.table th{color:#94a5c4;font-weight:600}.row{cursor:pointer}.row:hover{background:#182642}.status{display:inline-flex;gap:6px;align-items:center}.dot{width:8px;height:8px;border-radius:50%;background:#5ee08b}.off{background:#65728a}.pill{padding:3px 8px;border-radius:999px;background:#1b2a4a;color:#bfd0ee;font-size:12px}button{background:#5b8cff;color:white;border:0;border-radius:10px;padding:9px 13px;font-weight:650;cursor:pointer}button.secondary{background:#22314e}input,textarea{width:100%;background:#0d1629;color:#e8edf7;border:1px solid #314463;border-radius:9px;padding:9px 10px}textarea{min-height:128px;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px}.form{padding:16px;display:grid;gap:12px}.cols{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.check{display:flex;align-items:center;gap:8px}.check input{width:auto}.sessions{max-height:270px;overflow:auto}.empty{padding:24px;color:#8190aa}.toast{position:fixed;right:22px;bottom:22px;background:#1f3155;border:1px solid #3d5785;padding:11px 14px;border-radius:10px;display:none}@media(max-width:950px){.grid{grid-template-columns:repeat(2,1fr)}.split{grid-template-columns:1fr}.cols{grid-template-columns:1fr}} </style></head>
|
||||
<body><div class="wrap"><div class="top"><div class="brand"><div class="logo">SG</div><div><strong>SessionGuard</strong><div class="muted">Master Console</div></div></div><form action="/logout" method="post"><button class="secondary">Abmelden</button></form></div>
|
||||
<div class="grid"><div class="card"><div class="muted">Server</div><div class="value" id="mServers">–</div></div><div class="card"><div class="muted">Online</div><div class="value" id="mOnline">–</div></div><div class="card"><div class="muted">Aktive Sitzungen</div><div class="value" id="mActive">–</div></div><div class="card"><div class="muted">Cleanup geplant</div><div class="value" id="mCleanup">–</div></div></div>
|
||||
<div class="split"><section class="panel"><h2>Terminalserver</h2><div id="agents"></div></section><section class="panel"><h2 id="detailTitle">Server auswählen</h2><div id="detail" class="empty">Links einen Agent auswählen.</div></section></div></div><div class="toast" id="toast"></div><script src="/app.js"></script></body></html>`
|
||||
|
||||
const masterJS = `
|
||||
let selected=null, current=null;
|
||||
const $=id=>document.getElementById(id);
|
||||
function esc(s){return String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
|
||||
function bytes(n){if(!n)return '–';let u=['B','KB','MB','GB','TB'],i=0;while(n>=1024&&i<u.length-1){n/=1024;i++}return n.toFixed(i>1?1:0)+' '+u[i]}
|
||||
function age(sec){if(!sec)return '–';let d=Math.floor(sec/86400),h=Math.floor(sec%86400/3600),m=Math.floor(sec%3600/60);return d+'d '+h+'h '+m+'m'}
|
||||
function toast(t){let e=$('toast');e.textContent=t;e.style.display='block';setTimeout(()=>e.style.display='none',2500)}
|
||||
async function api(url,opt){let r=await fetch(url,opt);if(r.status===401){location='/login';return}let j=await r.json().catch(()=>({}));if(!r.ok)throw new Error(j.error||r.statusText);return j}
|
||||
async function refresh(){try{let d=await api('/api/v1/dashboard');let a=d.agents||[];$('mServers').textContent=a.length;$('mOnline').textContent=a.filter(x=>x.online).length;$('mActive').textContent=a.reduce((n,x)=>n+x.active_sessions,0);$('mCleanup').textContent=a.reduce((n,x)=>n+(x.snapshot.pending_cleanup||[]).length,0);$('agents').innerHTML='<table class="table"><thead><tr><th>Status</th><th>Server</th><th>Sitzungen</th><th>Build</th></tr></thead><tbody>'+a.map(x=>'<tr class="row" data-id="'+esc(x.id)+'"><td><span class="status"><i class="dot '+(x.online?'':'off')+'"></i>'+(x.online?'Online':'Offline')+'</span></td><td><strong>'+esc(x.name)+'</strong><br><span class="muted">'+esc(x.snapshot.server.os||'')+'</span></td><td>'+x.active_sessions+' aktiv / '+x.total_sessions+'</td><td>'+esc(x.snapshot.agent_version||'–')+'</td></tr>').join('')+'</tbody></table>';document.querySelectorAll('.row').forEach(r=>r.onclick=()=>selectAgent(r.dataset.id));if(selected)selectAgent(selected,true)}catch(e){toast(e.message)}}
|
||||
async function selectAgent(id,quiet){selected=id;try{current=await api('/api/v1/agents/'+encodeURIComponent(id));renderDetail(current)}catch(e){if(!quiet)toast(e.message)}}
|
||||
function renderDetail(a){let s=a.snapshot.server||{},sessions=a.snapshot.sessions||[],p=a.desired_policy||defaultPolicy(a.snapshot.policy_revision);$('detailTitle').textContent=a.name||a.id;$('detail').className='';$('detail').innerHTML='<div class="grid" style="grid-template-columns:repeat(3,1fr);padding:16px"><div class="card"><div class="muted">Uptime</div><div class="value" style="font-size:20px">'+age(s.uptime_seconds)+'</div></div><div class="card"><div class="muted">RAM frei</div><div class="value" style="font-size:20px">'+bytes(s.memory_available)+' / '+bytes(s.memory_total)+'</div></div><div class="card"><div class="muted">Policy</div><div class="value" style="font-size:15px">'+esc(a.snapshot.policy_revision||'–')+'</div></div></div><div class="sessions"><table class="table"><thead><tr><th>ID</th><th>Benutzer</th><th>Status</th><th>Client</th></tr></thead><tbody>'+sessions.map(x=>'<tr><td>'+x.id+'</td><td>'+esc((x.domain?x.domain+'\\':'')+x.user)+'</td><td><span class="pill">'+esc(x.state)+'</span></td><td>'+esc(x.client_name||'–')+'</td></tr>').join('')+'</tbody></table></div>'+policyForm(p)}
|
||||
function defaultPolicy(rev){return {revision:rev||'',cleanup:{enabled:true,grace_seconds:600,poll_seconds:10,retry_seconds:60,dry_run:true,exclude_users:['Administrator','DefaultAccount','WDAGUtilityAccount'],exclude_sids:['S-1-5-18','S-1-5-19','S-1-5-20'],allowed_profile_roots:['C:\\Users']},templates:[]}}
|
||||
function policyForm(p){let c=p.cleanup||{};return '<div class="form"><strong>Policy bearbeiten</strong><div class="cols"><label>Grace Period (s)<input id="grace" type="number" min="1" value="'+(c.grace_seconds||600)+'"></label><label>Polling (s)<input id="poll" type="number" min="2" value="'+(c.poll_seconds||10)+'"></label><label>Retry (s)<input id="retry" type="number" min="1" value="'+(c.retry_seconds||60)+'"></label></div><div class="check"><input id="enabled" type="checkbox" '+(c.enabled?'checked':'')+'><label for="enabled">Profil-Cleanup aktiv</label></div><div class="check"><input id="dry" type="checkbox" '+(c.dry_run?'checked':'')+'><label for="dry">Dry-Run (empfohlen zum Testen)</label></div><label>Ausgeschlossene Benutzer (eine Zeile je Eintrag)<textarea id="users">'+esc((c.exclude_users||[]).join('\n'))+'</textarea></label><label>Ausgeschlossene SIDs / SID-Präfixe<textarea id="sids">'+esc((c.exclude_sids||[]).join('\n'))+'</textarea></label><label>Erlaubte Profil-Roots<textarea id="roots">'+esc((c.allowed_profile_roots||[]).join('\n'))+'</textarea></label><label>Templates (JSON Array)<textarea id="templates" style="min-height:230px">'+esc(JSON.stringify(p.templates||[],null,2))+'</textarea></label><div style="display:flex;gap:8px"><button onclick="savePolicy(false)">Für diesen Server speichern</button><button class="secondary" onclick="savePolicy(true)">Auf alle Server anwenden</button></div></div>'}
|
||||
function lines(id){return $(id).value.split('\n').map(x=>x.trim()).filter(Boolean)}
|
||||
async function savePolicy(all){if(!selected)return;try{let p={cleanup:{enabled:$('enabled').checked,grace_seconds:+$('grace').value,poll_seconds:+$('poll').value,retry_seconds:+$('retry').value,dry_run:$('dry').checked,exclude_users:lines('users'),exclude_sids:lines('sids'),allowed_profile_roots:lines('roots')},templates:JSON.parse($('templates').value||'[]')};await api(all?'/api/v1/policy/all':'/api/v1/agents/'+encodeURIComponent(selected)+'/policy',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});toast(all?'Policy auf alle Server angewendet':'Policy gespeichert');await selectAgent(selected,true)}catch(e){toast(e.message)}}
|
||||
refresh();setInterval(refresh,5000);`
|
||||
Reference in New Issue
Block a user