178 lines
4.1 KiB
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(¬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)
|
|
}
|