package main import ( "fmt" "sync" toolclient "ollie/cmd/olliesrv/internal/toolclient" "ollie/toolsrv/protocol" "github.com/godbus/dbus/v5" ) // bypassNotifier handles desktop notifications for bypass requests. // When a notification action is clicked, it writes the resolution directly to toolsrvclient. type bypassNotifier struct { conn *dbus.Conn mu sync.Mutex notifID map[uint32]*pendingRequest // notification ID -> request info } type pendingRequest struct { id string session *sessionRef } type sessionRef struct { dialFn func() *toolclient.ToolsrvConn } var notifier *bypassNotifier // initBypassNotifier sets up the notification action listener. func initBypassNotifier(conn *dbus.Conn) { if conn == nil { return } notifier = &bypassNotifier{ conn: conn, notifID: make(map[uint32]*pendingRequest), } // Listen for ActionInvoked signals from the notification daemon conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, "type='signal',interface='org.freedesktop.Notifications',member='ActionInvoked'") //nolint:errcheck conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, "type='signal',interface='org.freedesktop.Notifications',member='NotificationClosed'") //nolint:errcheck ch := make(chan *dbus.Signal, 32) conn.Signal(ch) go notifier.listenSignals(ch) } // notifyBypass sends a desktop notification for a bypass request. // When the user responds, it writes the resolution directly to toolsrvclient. func notifyBypass(req *protocol.BypassRequest, sessionID string, dialFn func() *toolclient.ToolsrvConn) { if notifier == nil { return } notifier.sendNotification(req, sessionID, dialFn) } func (n *bypassNotifier) sendNotification(req *protocol.BypassRequest, sessionID string, dialFn func() *toolclient.ToolsrvConn) { obj := n.conn.Object("org.freedesktop.Notifications", "/org/freedesktop/Notifications") summary := "Elevation Request" body := fmt.Sprintf("%s\ncwd: %s", escapeMarkup(req.Cmd), escapeMarkup(req.Cwd)) if sessionID != "" { body = fmt.Sprintf("session: %s\n%s", escapeMarkup(sessionID), body) } actions := []string{ "approve", "Approve", "deny", "Deny", } hints := map[string]dbus.Variant{ "urgency": dbus.MakeVariant(byte(2)), // critical } call := obj.Call("org.freedesktop.Notifications.Notify", 0, "ollie", // app_name uint32(0), // replaces_id "dialog-warning", // icon summary, body, actions, hints, int32(300000), // timeout ms (5 minutes) ) if call.Err != nil { return } var notifID uint32 call.Store(¬ifID) n.mu.Lock() n.notifID[notifID] = &pendingRequest{ id: req.ID, session: &sessionRef{dialFn: dialFn}, } n.mu.Unlock() } func (n *bypassNotifier) listenSignals(ch chan *dbus.Signal) { for sig := range ch { switch sig.Name { case "org.freedesktop.Notifications.ActionInvoked": if len(sig.Body) < 2 { continue } nid, _ := sig.Body[0].(uint32) action, _ := sig.Body[1].(string) n.mu.Lock() pr, ok := n.notifID[nid] delete(n.notifID, nid) n.mu.Unlock() if !ok || pr == nil { continue } approved := action == "approve" go n.resolve(pr, approved) case "org.freedesktop.Notifications.NotificationClosed": if len(sig.Body) < 2 { continue } nid, _ := sig.Body[0].(uint32) reason, _ := sig.Body[1].(uint32) n.mu.Lock() pr, ok := n.notifID[nid] delete(n.notifID, nid) n.mu.Unlock() if !ok || pr == nil { continue } // reason 2 = dismissed by user, 1 = expired // Treat dismiss/expire as deny if reason == 1 || reason == 2 { 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 { var out []byte for _, c := range []byte(s) { switch c { case '&': out = append(out, []byte("&")...) case '<': out = append(out, []byte("<")...) case '>': out = append(out, []byte(">")...) default: out = append(out, c) } } return string(out) }