Added ldap and pam authentication
This commit is contained in:
+59
-16
@@ -44,12 +44,13 @@ type Handler struct {
|
||||
audit audit.Recorder // never nil; audit.Nop when disabled
|
||||
channelFinderURL string // empty if not configured
|
||||
archiverURL string // empty if not configured
|
||||
uiDefaultZoom float64 // base UI scale sent to clients; 0 means 1.0
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// New creates an API Handler. synth may be nil if the synthetic DS is disabled.
|
||||
// rec records system-affecting mutations; pass audit.Nop() to disable auditing.
|
||||
func New(b *broker.Broker, synth *synthetic.Synthetic, store *storage.Store, cfg *confmgr.Store, policy *access.Policy, acl *panelacl.Store, ctrlLogic *controllogic.Store, ctrlEngine *controllogic.Engine, rec audit.Recorder, channelFinderURL, archiverURL string, log *slog.Logger) *Handler {
|
||||
func New(b *broker.Broker, synth *synthetic.Synthetic, store *storage.Store, cfg *confmgr.Store, policy *access.Policy, acl *panelacl.Store, ctrlLogic *controllogic.Store, ctrlEngine *controllogic.Engine, rec audit.Recorder, channelFinderURL, archiverURL string, uiDefaultZoom float64, log *slog.Logger) *Handler {
|
||||
if rec == nil {
|
||||
rec = audit.Nop()
|
||||
}
|
||||
@@ -65,6 +66,7 @@ func New(b *broker.Broker, synth *synthetic.Synthetic, store *storage.Store, cfg
|
||||
audit: rec,
|
||||
channelFinderURL: channelFinderURL,
|
||||
archiverURL: archiverURL,
|
||||
uiDefaultZoom: uiDefaultZoom,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
@@ -192,6 +194,10 @@ func (h *Handler) getMe(w http.ResponseWriter, r *http.Request) {
|
||||
if groups == nil {
|
||||
groups = []string{}
|
||||
}
|
||||
defaultZoom := h.uiDefaultZoom
|
||||
if defaultZoom <= 0 {
|
||||
defaultZoom = 1.0
|
||||
}
|
||||
jsonOK(w, map[string]any{
|
||||
"user": user,
|
||||
"level": h.policy.Level(user).String(),
|
||||
@@ -199,6 +205,7 @@ func (h *Handler) getMe(w http.ResponseWriter, r *http.Request) {
|
||||
"canEditLogic": h.policy.CanEditLogic(user),
|
||||
"canViewAudit": h.policy.CanViewAudit(user),
|
||||
"canAdmin": h.policy.CanAdmin(user),
|
||||
"defaultZoom": defaultZoom,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -366,8 +373,9 @@ func (h *Handler) listSignals(w http.ResponseWriter, r *http.Request) {
|
||||
if dsName == "synthetic" && h.synthetic != nil {
|
||||
user := caller(r)
|
||||
panel := r.URL.Query().Get("panel")
|
||||
userGroups := h.policy.GroupsOf(user)
|
||||
metas := h.synthetic.FilteredMetadata(func(d synthetic.SignalDef) bool {
|
||||
return synVisible(d, user, panel)
|
||||
return synVisible(d, user, panel, userGroups)
|
||||
})
|
||||
out := make([]signalInfo, len(metas))
|
||||
for i, m := range metas {
|
||||
@@ -398,12 +406,15 @@ func (h *Handler) listSignals(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// synVisible reports whether a synthetic signal should be listed for the given
|
||||
// caller while editing the given panel. An empty Visibility is treated as
|
||||
// "global" so legacy definitions remain visible everywhere.
|
||||
func synVisible(d synthetic.SignalDef, user, panel string) bool {
|
||||
// caller (a member of userGroups) while editing the given panel. An empty
|
||||
// Visibility is treated as "global" so legacy definitions remain visible
|
||||
// everywhere.
|
||||
func synVisible(d synthetic.SignalDef, user, panel string, userGroups []string) bool {
|
||||
switch d.Visibility {
|
||||
case "user":
|
||||
return user != "" && d.Owner == user
|
||||
case "group":
|
||||
return access.CanSee(user, d.Owner, access.ScopeGroup, d.Groups, userGroups)
|
||||
case "panel":
|
||||
return panel != "" && d.Panel == panel
|
||||
default: // "global" or legacy empty
|
||||
@@ -431,13 +442,14 @@ func (h *Handler) searchSignals(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user := caller(r)
|
||||
panel := r.URL.Query().Get("panel")
|
||||
userGroups := h.policy.GroupsOf(user)
|
||||
|
||||
var out []signalInfo
|
||||
for _, ds := range sources {
|
||||
var metas []datasource.Metadata
|
||||
if ds.Name() == "synthetic" && h.synthetic != nil {
|
||||
metas = h.synthetic.FilteredMetadata(func(d synthetic.SignalDef) bool {
|
||||
return synVisible(d, user, panel)
|
||||
return synVisible(d, user, panel, userGroups)
|
||||
})
|
||||
} else {
|
||||
var err error
|
||||
@@ -593,10 +605,33 @@ func (h *Handler) putGroups(w http.ResponseWriter, r *http.Request) {
|
||||
// render sharing affordances and filter the visible set.
|
||||
type interfaceListItem struct {
|
||||
storage.InterfaceMeta
|
||||
Owner string `json:"owner,omitempty"`
|
||||
Folder string `json:"folder,omitempty"`
|
||||
Order float64 `json:"order,omitempty"`
|
||||
Perm string `json:"perm"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
Folder string `json:"folder,omitempty"`
|
||||
Order float64 `json:"order,omitempty"`
|
||||
Perm string `json:"perm"`
|
||||
Scope string `json:"scope,omitempty"` // derived visibility bucket: private|group|global
|
||||
Groups []string `json:"groups,omitempty"` // groups a group-scoped panel is shared with
|
||||
}
|
||||
|
||||
// panelScope derives a uniform visibility token (and, for group scope, the
|
||||
// shared group names) from a panel's ACL record so the selector tree can bucket
|
||||
// it as Mine/Group/Global like the other scoped subsystems. An unmanaged panel
|
||||
// (nil record) or any panel exposed publicly is global; otherwise a panel shared
|
||||
// with one or more user-groups is group-scoped; everything else is private.
|
||||
func panelScope(acl *panelacl.PanelACL) (string, []string) {
|
||||
if acl == nil || acl.Public != "" {
|
||||
return access.ScopeGlobal, nil
|
||||
}
|
||||
var groups []string
|
||||
for _, g := range acl.Grants {
|
||||
if g.Kind == "group" {
|
||||
groups = append(groups, g.Name)
|
||||
}
|
||||
}
|
||||
if len(groups) > 0 {
|
||||
return access.ScopeGroup, groups
|
||||
}
|
||||
return access.ScopePrivate, nil
|
||||
}
|
||||
|
||||
func (h *Handler) listInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -613,11 +648,13 @@ func (h *Handler) listInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
continue // hide panels the caller cannot see
|
||||
}
|
||||
item := interfaceListItem{InterfaceMeta: m, Perm: perm.String()}
|
||||
if acl := h.acl.GetPanel(m.ID); acl != nil {
|
||||
acl := h.acl.GetPanel(m.ID)
|
||||
if acl != nil {
|
||||
item.Owner = acl.Owner
|
||||
item.Folder = acl.Folder
|
||||
item.Order = acl.Order
|
||||
}
|
||||
item.Scope, item.Groups = panelScope(acl)
|
||||
out = append(out, item)
|
||||
}
|
||||
h.log.Info("list interfaces", "count", len(out))
|
||||
@@ -1548,14 +1585,17 @@ func (h *Handler) forkSyntheticVersion(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// ── Control logic ──────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) listControlLogic(w http.ResponseWriter, _ *http.Request) {
|
||||
func (h *Handler) listControlLogic(w http.ResponseWriter, r *http.Request) {
|
||||
if h.ctrlLogic == nil {
|
||||
jsonOK(w, []any{})
|
||||
return
|
||||
}
|
||||
graphs := h.ctrlLogic.List()
|
||||
if graphs == nil {
|
||||
graphs = []controllogic.Graph{}
|
||||
user := caller(r)
|
||||
graphs := []controllogic.Graph{}
|
||||
for _, g := range h.ctrlLogic.List() {
|
||||
if h.policy.CanSee(user, g.Owner, g.Scope, g.ScopeGroups) {
|
||||
graphs = append(graphs, g)
|
||||
}
|
||||
}
|
||||
jsonOK(w, graphs)
|
||||
}
|
||||
@@ -1588,6 +1628,7 @@ func (h *Handler) createControlLogic(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
g.ID = genID("cl")
|
||||
g.Owner = caller(r) // stamp ownership from the trusted identity
|
||||
if err := h.ctrlLogic.Save(g); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
@@ -1608,7 +1649,8 @@ func (h *Handler) updateControlLogic(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
id := r.PathValue("id")
|
||||
if _, err := h.ctrlLogic.Get(id); err != nil {
|
||||
prev, err := h.ctrlLogic.Get(id)
|
||||
if err != nil {
|
||||
jsonError(w, http.StatusNotFound, "control logic graph not found: "+id)
|
||||
return
|
||||
}
|
||||
@@ -1618,6 +1660,7 @@ func (h *Handler) updateControlLogic(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
g.ID = id
|
||||
g.Owner = prev.Owner // owner is immutable across revisions
|
||||
if err := h.ctrlLogic.Save(g); err != nil {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
|
||||
@@ -61,7 +61,7 @@ func setup(t *testing.T) (*httptest.Server, func()) {
|
||||
clEngine := controllogic.NewEngine(ctx, brk, clStore, cfgStore, audit.Nop(), log)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
api.New(brk, nil, store, cfgStore, access.New("", nil), acl, clStore, clEngine, audit.Nop(), "", "", log).Register(mux, "/api/v1")
|
||||
api.New(brk, nil, store, cfgStore, access.New("", nil), acl, clStore, clEngine, audit.Nop(), "", "", 0, log).Register(mux, "/api/v1")
|
||||
|
||||
srv := httptest.NewServer(mux)
|
||||
return srv, func() {
|
||||
@@ -485,7 +485,7 @@ func adminSetup(t *testing.T) (*httptest.Server, func()) {
|
||||
}
|
||||
|
||||
inner := http.NewServeMux()
|
||||
api.New(brk, nil, store, cfgStore, policy, acl, clStore, clEngine, audit.Nop(), "", "", log).Register(inner, "/api/v1")
|
||||
api.New(brk, nil, store, cfgStore, policy, acl, clStore, clEngine, audit.Nop(), "", "", 0, log).Register(inner, "/api/v1")
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+23
-4
@@ -37,7 +37,7 @@ func configStatus(err error) int {
|
||||
|
||||
// ── config sets ─────────────────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) listConfigSets(w http.ResponseWriter, _ *http.Request) {
|
||||
func (h *Handler) listConfigSets(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.configEnabled(w) {
|
||||
return
|
||||
}
|
||||
@@ -46,7 +46,7 @@ func (h *Handler) listConfigSets(w http.ResponseWriter, _ *http.Request) {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, sets)
|
||||
jsonOK(w, h.filterConfigMetas(r, sets))
|
||||
}
|
||||
|
||||
func (h *Handler) getConfigSet(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -92,6 +92,9 @@ func (h *Handler) updateConfigSet(w http.ResponseWriter, r *http.Request) {
|
||||
jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
if prev, err := h.cfg.GetSet(id); err == nil {
|
||||
set.Owner = prev.Owner // owner is immutable across revisions
|
||||
}
|
||||
out, err := h.cfg.UpdateSet(id, set, r.URL.Query().Get("tag"))
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
@@ -227,7 +230,7 @@ func (h *Handler) resolveSet(id, version string) (confmgr.ConfigSet, error) {
|
||||
|
||||
// ── config instances ────────────────────────────────────────────────────────
|
||||
|
||||
func (h *Handler) listConfigInstances(w http.ResponseWriter, _ *http.Request) {
|
||||
func (h *Handler) listConfigInstances(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.configEnabled(w) {
|
||||
return
|
||||
}
|
||||
@@ -236,7 +239,20 @@ func (h *Handler) listConfigInstances(w http.ResponseWriter, _ *http.Request) {
|
||||
jsonError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
jsonOK(w, insts)
|
||||
jsonOK(w, h.filterConfigMetas(r, insts))
|
||||
}
|
||||
|
||||
// filterConfigMetas drops entries the caller may not see per their scope
|
||||
// (private/group/global). Owner always sees their own; empty scope = global.
|
||||
func (h *Handler) filterConfigMetas(r *http.Request, metas []confmgr.Meta) []confmgr.Meta {
|
||||
user := caller(r)
|
||||
out := make([]confmgr.Meta, 0, len(metas))
|
||||
for _, m := range metas {
|
||||
if h.policy.CanSee(user, m.Owner, m.Scope, m.Groups) {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (h *Handler) getConfigInstance(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -282,6 +298,9 @@ func (h *Handler) updateConfigInstance(w http.ResponseWriter, r *http.Request) {
|
||||
jsonError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
if prev, err := h.cfg.GetInstance(id); err == nil {
|
||||
inst.Owner = prev.Owner // owner is immutable across revisions
|
||||
}
|
||||
out, err := h.cfg.UpdateInstance(id, inst, r.URL.Query().Get("tag"))
|
||||
if err != nil {
|
||||
jsonError(w, configStatus(err), err.Error())
|
||||
|
||||
Reference in New Issue
Block a user