refactor: simplify bypass - toolsrv is the broker, olliesrv is just a client
- Remove olliesrv's bypass broker machinery (pendingCh, EvaluateRequest, etc) - Session bypass loop now just reads from toolsrv's bypass/pending and notifies - Notification handler writes directly to toolsrv's bypass/resolve (fire and forget) - Remove bypass/ directory from olliesrv's 9P namespace - Remove session/*/bypass file (policy can be added back to toolsrv later if needed) The flow is now: 1. toolsrv blocks tool execution, exposes request via bypass/pending 2. olliesrv reads from toolsrv, shows D-Bus notification 3. User clicks approve/deny, olliesrv writes to toolsrv's bypass/resolve 4. toolsrv unblocks and executes (or denies)
This commit is contained in:
parent
46bd0dd999
commit
edd5ade471
|
|
@ -5,30 +5,37 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
"ollie/cmd/olliesrv/internal/bypass"
|
"ollie/toolsrv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// bypassNotifier handles desktop notifications for bypass requests
|
// bypassNotifier handles desktop notifications for bypass requests.
|
||||||
// and maps notification action callbacks back to the broker.
|
// When a notification action is clicked, it writes the resolution directly to toolsrv.
|
||||||
type bypassNotifier struct {
|
type bypassNotifier struct {
|
||||||
conn *dbus.Conn
|
conn *dbus.Conn
|
||||||
broker *bypass.Broker
|
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
notifID map[uint32]string // notification ID -> request ID
|
notifID map[uint32]*pendingRequest // notification ID -> request info
|
||||||
|
}
|
||||||
|
|
||||||
|
type pendingRequest struct {
|
||||||
|
id string
|
||||||
|
session *sessionRef
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionRef struct {
|
||||||
|
dialFn func() *toolsrv.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
var notifier *bypassNotifier
|
var notifier *bypassNotifier
|
||||||
|
|
||||||
// initBypassNotifier sets up the notification action listener.
|
// initBypassNotifier sets up the notification action listener.
|
||||||
func initBypassNotifier(conn *dbus.Conn, broker *bypass.Broker) {
|
func initBypassNotifier(conn *dbus.Conn) {
|
||||||
if conn == nil || broker == nil {
|
if conn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
notifier = &bypassNotifier{
|
notifier = &bypassNotifier{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
broker: broker,
|
notifID: make(map[uint32]*pendingRequest),
|
||||||
notifID: make(map[uint32]string),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for ActionInvoked signals from the notification daemon
|
// Listen for ActionInvoked signals from the notification daemon
|
||||||
|
|
@ -43,26 +50,26 @@ func initBypassNotifier(conn *dbus.Conn, broker *bypass.Broker) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// notifyBypass sends a desktop notification for a bypass request.
|
// notifyBypass sends a desktop notification for a bypass request.
|
||||||
func notifyBypass(req *bypass.Request) {
|
// When the user responds, it writes the resolution directly to toolsrv.
|
||||||
|
func notifyBypass(req *toolsrv.BypassRequest, sessionID string, dialFn func() *toolsrv.Conn) {
|
||||||
if notifier == nil {
|
if notifier == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
notifier.sendNotification(req)
|
notifier.sendNotification(req, sessionID, dialFn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *bypassNotifier) sendNotification(req *bypass.Request) {
|
func (n *bypassNotifier) sendNotification(req *toolsrv.BypassRequest, sessionID string, dialFn func() *toolsrv.Conn) {
|
||||||
obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
|
obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
|
||||||
|
|
||||||
summary := "Elevation Request"
|
summary := "Elevation Request"
|
||||||
body := fmt.Sprintf("<b>%s</b>\ncwd: %s", escapeMarkup(req.Cmd), escapeMarkup(req.Cwd))
|
body := fmt.Sprintf("<b>%s</b>\ncwd: %s", escapeMarkup(req.Cmd), escapeMarkup(req.Cwd))
|
||||||
if req.SessionID != "" {
|
if sessionID != "" {
|
||||||
body = fmt.Sprintf("session: %s\n%s", escapeMarkup(req.SessionID), body)
|
body = fmt.Sprintf("session: %s\n%s", escapeMarkup(sessionID), body)
|
||||||
}
|
}
|
||||||
|
|
||||||
actions := []string{
|
actions := []string{
|
||||||
"approve", "Approve",
|
"approve", "Approve",
|
||||||
"deny", "Deny",
|
"deny", "Deny",
|
||||||
"persist", "Persist",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hints := map[string]dbus.Variant{
|
hints := map[string]dbus.Variant{
|
||||||
|
|
@ -77,7 +84,7 @@ func (n *bypassNotifier) sendNotification(req *bypass.Request) {
|
||||||
body,
|
body,
|
||||||
actions,
|
actions,
|
||||||
hints,
|
hints,
|
||||||
int32(300000), // timeout ms (matches TTL)
|
int32(300000), // timeout ms (5 minutes)
|
||||||
)
|
)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
return
|
return
|
||||||
|
|
@ -87,7 +94,10 @@ func (n *bypassNotifier) sendNotification(req *bypass.Request) {
|
||||||
call.Store(¬ifID)
|
call.Store(¬ifID)
|
||||||
|
|
||||||
n.mu.Lock()
|
n.mu.Lock()
|
||||||
n.notifID[notifID] = req.ID
|
n.notifID[notifID] = &pendingRequest{
|
||||||
|
id: req.ID,
|
||||||
|
session: &sessionRef{dialFn: dialFn},
|
||||||
|
}
|
||||||
n.mu.Unlock()
|
n.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,26 +112,16 @@ func (n *bypassNotifier) listenSignals(ch chan *dbus.Signal) {
|
||||||
action, _ := sig.Body[1].(string)
|
action, _ := sig.Body[1].(string)
|
||||||
|
|
||||||
n.mu.Lock()
|
n.mu.Lock()
|
||||||
reqID, ok := n.notifID[nid]
|
pr, ok := n.notifID[nid]
|
||||||
delete(n.notifID, nid)
|
delete(n.notifID, nid)
|
||||||
n.mu.Unlock()
|
n.mu.Unlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok || pr == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var res bypass.Resolution
|
approved := action == "approve"
|
||||||
switch action {
|
go n.resolve(pr, approved)
|
||||||
case "approve":
|
|
||||||
res = bypass.ResolveApprove
|
|
||||||
case "deny":
|
|
||||||
res = bypass.ResolveDeny
|
|
||||||
case "persist":
|
|
||||||
res = bypass.ResolvePersist
|
|
||||||
default:
|
|
||||||
res = bypass.ResolveDeny
|
|
||||||
}
|
|
||||||
n.broker.Resolve(reqID, res)
|
|
||||||
|
|
||||||
case "org.freedesktop.Notifications.NotificationClosed":
|
case "org.freedesktop.Notifications.NotificationClosed":
|
||||||
if len(sig.Body) < 2 {
|
if len(sig.Body) < 2 {
|
||||||
|
|
@ -131,25 +131,33 @@ func (n *bypassNotifier) listenSignals(ch chan *dbus.Signal) {
|
||||||
reason, _ := sig.Body[1].(uint32)
|
reason, _ := sig.Body[1].(uint32)
|
||||||
|
|
||||||
n.mu.Lock()
|
n.mu.Lock()
|
||||||
reqID, ok := n.notifID[nid]
|
pr, ok := n.notifID[nid]
|
||||||
delete(n.notifID, nid)
|
delete(n.notifID, nid)
|
||||||
n.mu.Unlock()
|
n.mu.Unlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok || pr == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// reason 2 = dismissed by user, 1 = expired, 3 = closed by call, 4 = undefined
|
// reason 2 = dismissed by user, 1 = expired
|
||||||
// Treat dismiss as deny
|
// Treat dismiss/expire as deny
|
||||||
if reason == 2 {
|
if reason == 1 || reason == 2 {
|
||||||
n.broker.Resolve(reqID, bypass.ResolveDeny)
|
go n.resolve(pr, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *bypassNotifier) resolve(pr *pendingRequest, approved bool) {
|
||||||
|
conn := pr.session.dialFn()
|
||||||
|
if conn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn.ResolveBypass(pr.id, approved, "") //nolint:errcheck
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func escapeMarkup(s string) string {
|
func escapeMarkup(s string) string {
|
||||||
// Basic XML/HTML entity escaping for notification body
|
|
||||||
var out []byte
|
var out []byte
|
||||||
for _, c := range []byte(s) {
|
for _, c := range []byte(s) {
|
||||||
switch c {
|
switch c {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package fs
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"ollie/cmd/olliesrv/internal/bypass"
|
|
||||||
"ollie/virtfs"
|
"ollie/virtfs"
|
||||||
olog "ollie/log"
|
olog "ollie/log"
|
||||||
"ollie/cmd/olliesrv/internal/session"
|
"ollie/cmd/olliesrv/internal/session"
|
||||||
|
|
@ -19,7 +18,7 @@ type Config struct {
|
||||||
SessionsDir string
|
SessionsDir string
|
||||||
Yolo bool
|
Yolo bool
|
||||||
ModelCache *ModelCache
|
ModelCache *ModelCache
|
||||||
BypassBroker *bypass.Broker
|
BypassNotify session.BypassNotifyFunc
|
||||||
Shutdown func()
|
Shutdown func()
|
||||||
Invalidate func()
|
Invalidate func()
|
||||||
}
|
}
|
||||||
|
|
@ -34,7 +33,7 @@ func NewRoot(cfg Config) *Tree {
|
||||||
AgentsDir: cfg.AgentsDir,
|
AgentsDir: cfg.AgentsDir,
|
||||||
SessionsDir: cfg.SessionsDir,
|
SessionsDir: cfg.SessionsDir,
|
||||||
Yolo: cfg.Yolo,
|
Yolo: cfg.Yolo,
|
||||||
BypassBroker: cfg.BypassBroker,
|
BypassNotify: cfg.BypassNotify,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build the tree from the spec.
|
// Build the tree from the spec.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import (
|
||||||
|
|
||||||
"ollie/cmd/olliesrv/internal/agent"
|
"ollie/cmd/olliesrv/internal/agent"
|
||||||
"ollie/cmd/olliesrv/internal/backend"
|
"ollie/cmd/olliesrv/internal/backend"
|
||||||
"ollie/cmd/olliesrv/internal/bypass"
|
|
||||||
"ollie/cmd/olliesrv/internal/session"
|
"ollie/cmd/olliesrv/internal/session"
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
"ollie/toolsrv"
|
"ollie/toolsrv"
|
||||||
|
|
@ -40,7 +39,6 @@ var treeSpecUID, treeSpecGID string
|
||||||
// buildTreeSpec constructs the full 9P namespace.
|
// buildTreeSpec constructs the full 9P namespace.
|
||||||
func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
||||||
mc := cfg.ModelCache
|
mc := cfg.ModelCache
|
||||||
broker := cfg.BypassBroker
|
|
||||||
|
|
||||||
u, _ := user.Current()
|
u, _ := user.Current()
|
||||||
treeSpecUID = "ollie"
|
treeSpecUID = "ollie"
|
||||||
|
|
@ -166,54 +164,6 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
||||||
// bypass/
|
|
||||||
virtfs.DirNode("bypass",
|
|
||||||
virtfs.Doc("Bypass broker for sandboxed command approval"),
|
|
||||||
virtfs.FileNode("pending", 0444,
|
|
||||||
virtfs.Doc("Blocks until bypass request; returns JSON {id, cmd, cwd, env}"),
|
|
||||||
virtfs.BlockOnce(func(ctx context.Context, _ string) ([]byte, string, error) {
|
|
||||||
if broker == nil {
|
|
||||||
return nil, "", errNoBypass
|
|
||||||
}
|
|
||||||
req := broker.NextPending(ctx)
|
|
||||||
if req == nil {
|
|
||||||
return nil, "", fmt.Errorf("context cancelled")
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
return append(data, '\n'), "", nil
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
virtfs.FileNode("resolve", 0222,
|
|
||||||
virtfs.Doc("Resolve bypass request: write JSON {id, approved} or {id, error}"),
|
|
||||||
virtfs.Write(func(data []byte) error {
|
|
||||||
if broker == nil {
|
|
||||||
return errNoBypass
|
|
||||||
}
|
|
||||||
var msg struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Approved bool `json:"approved"`
|
|
||||||
Error string `json:"error"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &msg); err != nil {
|
|
||||||
return fmt.Errorf("invalid JSON: %w", err)
|
|
||||||
}
|
|
||||||
var res bypass.Resolution
|
|
||||||
if msg.Approved {
|
|
||||||
res = bypass.ResolveApprove
|
|
||||||
} else {
|
|
||||||
res = bypass.ResolveDeny
|
|
||||||
}
|
|
||||||
if !broker.Resolve(msg.ID, res) {
|
|
||||||
return fmt.Errorf("unknown request ID: %s", msg.ID)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// session/
|
// session/
|
||||||
virtfs.DirNode("session",
|
virtfs.DirNode("session",
|
||||||
virtfs.Doc("Session management"),
|
virtfs.Doc("Session management"),
|
||||||
|
|
@ -244,16 +194,14 @@ func buildTreeSpec(cfg *Config) virtfs.FsNodeDecl {
|
||||||
virtfs.Read(func() ([]byte, error) { return session.BuildIndex(), nil }),
|
virtfs.Read(func() ([]byte, error) { return session.BuildIndex(), nil }),
|
||||||
),
|
),
|
||||||
virtfs.Each("{sname}", func() ([]virtfs.FsNodeDecl, error) {
|
virtfs.Each("{sname}", func() ([]virtfs.FsNodeDecl, error) {
|
||||||
return buildSessionEntries(broker)
|
return buildSessionEntries()
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var errNoBypass = fmt.Errorf("bypass broker not available")
|
|
||||||
|
|
||||||
// buildSessionEntries produces one FsNodeDecl per session.
|
// buildSessionEntries produces one FsNodeDecl per session.
|
||||||
func buildSessionEntries(broker *bypass.Broker) ([]virtfs.FsNodeDecl, error) {
|
func buildSessionEntries() ([]virtfs.FsNodeDecl, error) {
|
||||||
sessions := session.Sessions()
|
sessions := session.Sessions()
|
||||||
var out []virtfs.FsNodeDecl
|
var out []virtfs.FsNodeDecl
|
||||||
|
|
||||||
|
|
@ -274,7 +222,7 @@ func buildSessionEntries(broker *bypass.Broker) ([]virtfs.FsNodeDecl, error) {
|
||||||
Aliases: []string{s.ID},
|
Aliases: []string{s.ID},
|
||||||
Remove: removeFn,
|
Remove: removeFn,
|
||||||
Rename: renameFn,
|
Rename: renameFn,
|
||||||
Children: buildSessionChildren(s, broker, removeFn, renameFn),
|
Children: buildSessionChildren(s, removeFn, renameFn),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
|
|
@ -283,7 +231,6 @@ func buildSessionEntries(broker *bypass.Broker) ([]virtfs.FsNodeDecl, error) {
|
||||||
// buildSessionChildren returns the file nodes for a single session.
|
// buildSessionChildren returns the file nodes for a single session.
|
||||||
func buildSessionChildren(
|
func buildSessionChildren(
|
||||||
s *session.Session,
|
s *session.Session,
|
||||||
broker *bypass.Broker,
|
|
||||||
removeFn func() error,
|
removeFn func() error,
|
||||||
renameFn func(string) error,
|
renameFn func(string) error,
|
||||||
) []virtfs.FsNodeDecl {
|
) []virtfs.FsNodeDecl {
|
||||||
|
|
@ -350,26 +297,6 @@ func buildSessionChildren(
|
||||||
virtfs.FileNode("id", 0444,
|
virtfs.FileNode("id", 0444,
|
||||||
virtfs.Read(func() ([]byte, error) { return []byte(s.ID + "\n"), nil }),
|
virtfs.Read(func() ([]byte, error) { return []byte(s.ID + "\n"), nil }),
|
||||||
),
|
),
|
||||||
virtfs.FileNode("bypass", 0666,
|
|
||||||
virtfs.Read(func() ([]byte, error) {
|
|
||||||
if broker == nil {
|
|
||||||
return nil, errNoBypass
|
|
||||||
}
|
|
||||||
p := broker.SessionPolicy(s.ID)
|
|
||||||
return p.Marshal()
|
|
||||||
}),
|
|
||||||
virtfs.Write(func(data []byte) error {
|
|
||||||
if broker == nil {
|
|
||||||
return errNoBypass
|
|
||||||
}
|
|
||||||
var p bypass.Policy
|
|
||||||
if err := bypass.ParsePolicy(data, &p); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
broker.SetSessionPolicy(s.ID, p)
|
|
||||||
return nil
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
virtfs.DirNode("agent",
|
virtfs.DirNode("agent",
|
||||||
virtfs.FileNode("new", 0666,
|
virtfs.FileNode("new", 0666,
|
||||||
virtfs.Request(func(_ context.Context, data []byte) ([]byte, error) {
|
virtfs.Request(func(_ context.Context, data []byte) ([]byte, error) {
|
||||||
|
|
|
||||||
|
|
@ -256,9 +256,9 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) {
|
||||||
sess.Uname = sess.Agents()[0].ID()
|
sess.Uname = sess.Agents()[0].ID()
|
||||||
Register(sess.Name(), sess)
|
Register(sess.Name(), sess)
|
||||||
|
|
||||||
// Start bypass approval loop for restored sessions (if broker is configured)
|
// Start bypass approval loop for restored sessions (if notify function is configured)
|
||||||
if pkgBypassBroker != nil && !ps.Paused {
|
if pkgBypassNotify != nil && !ps.Paused {
|
||||||
sess.StartBypassLoop(pkgBypassBroker)
|
sess.StartBypassLoop(pkgBypassNotify)
|
||||||
}
|
}
|
||||||
|
|
||||||
toolCount := 0
|
toolCount := 0
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,13 @@ var (
|
||||||
sessions = make(map[string]*Session)
|
sessions = make(map[string]*Session)
|
||||||
|
|
||||||
// Package config, set via Init.
|
// Package config, set via Init.
|
||||||
serverCtx context.Context
|
serverCtx context.Context
|
||||||
pkgLog *olog.Logger
|
pkgLog *olog.Logger
|
||||||
pkgSink *olog.Sink
|
pkgSink *olog.Sink
|
||||||
pkgAgentsDir string
|
pkgAgentsDir string
|
||||||
pkgSessionsDir string
|
pkgSessionsDir string
|
||||||
pkgYolo bool
|
pkgYolo bool
|
||||||
pkgBypassBroker BypassEvaluator
|
pkgBypassNotify BypassNotifyFunc
|
||||||
)
|
)
|
||||||
|
|
||||||
// InitConfig configures the session package.
|
// InitConfig configures the session package.
|
||||||
|
|
@ -46,7 +46,7 @@ type InitConfig struct {
|
||||||
AgentsDir string
|
AgentsDir string
|
||||||
SessionsDir string
|
SessionsDir string
|
||||||
Yolo bool
|
Yolo bool
|
||||||
BypassBroker BypassEvaluator // may be nil if bypass is disabled
|
BypassNotify BypassNotifyFunc // may be nil if bypass is disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the session package with the given configuration.
|
// Init initializes the session package with the given configuration.
|
||||||
|
|
@ -57,7 +57,7 @@ func Init(cfg InitConfig) {
|
||||||
pkgAgentsDir = cfg.AgentsDir
|
pkgAgentsDir = cfg.AgentsDir
|
||||||
pkgSessionsDir = cfg.SessionsDir
|
pkgSessionsDir = cfg.SessionsDir
|
||||||
pkgYolo = cfg.Yolo
|
pkgYolo = cfg.Yolo
|
||||||
pkgBypassBroker = cfg.BypassBroker
|
pkgBypassNotify = cfg.BypassNotify
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sessions returns a snapshot of all sessions.
|
// Sessions returns a snapshot of all sessions.
|
||||||
|
|
@ -320,9 +320,9 @@ func CreateEmpty(name, remote string) (*Session, error) {
|
||||||
sess.Keeper = infra.Keeper
|
sess.Keeper = infra.Keeper
|
||||||
sess.SetToolsConn(infra.ToolsConn)
|
sess.SetToolsConn(infra.ToolsConn)
|
||||||
|
|
||||||
// Start bypass approval loop (if broker is configured)
|
// Start bypass approval loop (if notify function is configured)
|
||||||
if pkgBypassBroker != nil {
|
if pkgBypassNotify != nil {
|
||||||
sess.StartBypassLoop(pkgBypassBroker)
|
sess.StartBypassLoop(pkgBypassNotify)
|
||||||
}
|
}
|
||||||
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
|
|
|
||||||
|
|
@ -112,19 +112,17 @@ func (s *Session) SetToolsConn(conn *toolsrv.Conn) {
|
||||||
s.toolsConn = conn
|
s.toolsConn = conn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BypassNotifyFunc is called when a bypass request arrives.
|
||||||
|
// It receives the request, session ID, and a dial function to get a connection for resolution.
|
||||||
|
type BypassNotifyFunc func(req *toolsrv.BypassRequest, sessionID string, dialFn func() *toolsrv.Conn)
|
||||||
|
|
||||||
// StartBypassLoop starts a goroutine that reads bypass requests from toolsrv
|
// StartBypassLoop starts a goroutine that reads bypass requests from toolsrv
|
||||||
// and evaluates them via the bypass broker. This should be called after
|
// and calls notifyFn for each one. This should be called after SetToolsConn.
|
||||||
// SetToolsConn when the toolsrv connection is ready.
|
func (s *Session) StartBypassLoop(notifyFn BypassNotifyFunc) {
|
||||||
func (s *Session) StartBypassLoop(broker BypassEvaluator) {
|
go s.runBypassLoop(notifyFn)
|
||||||
go s.runBypassLoop(broker)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BypassEvaluator evaluates bypass requests. Implemented by bypass.Broker.
|
func (s *Session) runBypassLoop(notifyFn BypassNotifyFunc) {
|
||||||
type BypassEvaluator interface {
|
|
||||||
EvaluateRequest(sessionID, cmd, cwd string, env map[string]string) (bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) runBypassLoop(broker BypassEvaluator) {
|
|
||||||
for {
|
for {
|
||||||
// Get a fresh connection for each request (blocking reads don't multiplex well)
|
// Get a fresh connection for each request (blocking reads don't multiplex well)
|
||||||
conn := s.DialToolServer()
|
conn := s.DialToolServer()
|
||||||
|
|
@ -150,23 +148,10 @@ func (s *Session) runBypassLoop(broker BypassEvaluator) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate the request
|
// Notify (non-blocking) - the notifier will write resolution when user responds
|
||||||
approved, evalErr := broker.EvaluateRequest(s.ID, req.Cmd, req.Cwd, req.Env)
|
if notifyFn != nil {
|
||||||
|
notifyFn(req, s.ID, s.DialToolServer)
|
||||||
// Send resolution back to toolsrv
|
|
||||||
resolveConn := s.DialToolServer()
|
|
||||||
if resolveConn == nil {
|
|
||||||
s.log.Warn("bypass: can't dial to resolve request %s", req.ID)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
errMsg := ""
|
|
||||||
if evalErr != nil {
|
|
||||||
errMsg = evalErr.Error()
|
|
||||||
}
|
|
||||||
if err := resolveConn.ResolveBypass(req.ID, approved, errMsg); err != nil {
|
|
||||||
s.log.Warn("bypass: resolve failed for %s: %v", req.ID, err)
|
|
||||||
}
|
|
||||||
resolveConn.Close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,10 @@ import (
|
||||||
|
|
||||||
"9fans.net/go/plan9/client"
|
"9fans.net/go/plan9/client"
|
||||||
"ollie/cmd/olliesrv/internal/agent"
|
"ollie/cmd/olliesrv/internal/agent"
|
||||||
"ollie/cmd/olliesrv/internal/bypass"
|
|
||||||
"ollie/env"
|
"ollie/env"
|
||||||
"ollie/cmd/olliesrv/internal/fs"
|
"ollie/cmd/olliesrv/internal/fs"
|
||||||
olog "ollie/log"
|
olog "ollie/log"
|
||||||
"ollie/paths"
|
"ollie/paths"
|
||||||
"ollie/cmd/olliesrv/internal/session"
|
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
)
|
)
|
||||||
|
|
@ -65,31 +63,9 @@ func runServer(sockPath string) {
|
||||||
|
|
||||||
modelCache := fs.NewModelCache()
|
modelCache := fs.NewModelCache()
|
||||||
|
|
||||||
// Start bypass broker (must be before NewRoot so it can be passed in)
|
// Desktop notifications for bypass prompts
|
||||||
policyPath := filepath.Join(paths.DataDir(), "bypass-policy.yaml")
|
if conn, err := dbus.SessionBus(); err == nil {
|
||||||
var bypassBroker *bypass.Broker
|
initBypassNotifier(conn)
|
||||||
{
|
|
||||||
notifyFn := func(req *bypass.Request) {
|
|
||||||
notifyBypass(req)
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
bypassBroker, err = bypass.NewBroker(bypass.BrokerConfig{
|
|
||||||
PolicyPath: policyPath,
|
|
||||||
Notify: notifyFn,
|
|
||||||
Logf: func(f string, a ...any) { fmt.Printf(f+"\n", a...) },
|
|
||||||
SessionValid: func(id string) bool { return session.Lookup(id) != nil },
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "warning: bypass broker: %v\n", err)
|
|
||||||
} else {
|
|
||||||
// Desktop notifications for bypass prompts (uses D-Bus notifications API directly)
|
|
||||||
if conn, err := dbus.SessionBus(); err == nil {
|
|
||||||
initBypassNotifier(conn, bypassBroker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if bypassBroker != nil {
|
|
||||||
defer bypassBroker.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rootTree := fs.NewRoot(fs.Config{
|
rootTree := fs.NewRoot(fs.Config{
|
||||||
|
|
@ -100,7 +76,7 @@ func runServer(sockPath string) {
|
||||||
Sink: sink,
|
Sink: sink,
|
||||||
Yolo: *yolo,
|
Yolo: *yolo,
|
||||||
ModelCache: modelCache,
|
ModelCache: modelCache,
|
||||||
BypassBroker: bypassBroker,
|
BypassNotify: notifyBypass,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create 9P server
|
// Create 9P server
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue