ollie/cmd/olliesrv/bypass_notify.go

178 lines
4.1 KiB
Go

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("<b>%s</b>\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(&notifID)
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("&amp;")...)
case '<':
out = append(out, []byte("&lt;")...)
case '>':
out = append(out, []byte("&gt;")...)
default:
out = append(out, c)
}
}
return string(out)
}