From 62a7d0d13f8ecce507d3ca06fdc51a342178e804 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 12 Aug 2026 08:51:19 +0200 Subject: [PATCH] bypass: remove socket-based execution (dead code) The old bypass broker used a Unix socket to receive requests and execute commands locally. This was replaced by the 9P-based flow where toolsrv executes commands after receiving approval from olliesrv. Removed: - acceptLoop, handleConn, executeAndStream, executeWithSudo - sendFrame, indexOf, socketDir helpers - SocketPath and Credential config options - Frame constants (FrameData, FrameExit) - net.Conn field on Request The broker now only handles: - Policy management (global + per-session) - Request evaluation (EvaluateRequest) - Rate limiting - User notification callbacks Execution happens in toolsrv, not olliesrv. --- cmd/olliesrv/internal/bypass/broker.go | 357 +------------------- cmd/olliesrv/internal/bypass/broker_test.go | 236 +++++-------- cmd/olliesrv/internal/bypass/request.go | 8 +- cmd/olliesrv/main.go | 25 -- 4 files changed, 102 insertions(+), 524 deletions(-) diff --git a/cmd/olliesrv/internal/bypass/broker.go b/cmd/olliesrv/internal/bypass/broker.go index 7769b0e..d9eb3e8 100644 --- a/cmd/olliesrv/internal/bypass/broker.go +++ b/cmd/olliesrv/internal/bypass/broker.go @@ -1,33 +1,24 @@ package bypass import ( - "encoding/binary" - "encoding/json" "fmt" - "net" - "os" - "os/exec" - "strings" "sync" - "syscall" "time" "golang.org/x/time/rate" ) const ( - RequestTTL = 300 * time.Second - FrameData = 'd' - FrameExit = 'x' - BurstLimit = 3 // max denied/unanswered requests in burst window - RefillInterval = 10 * time.Second // one slot recovers every 10s (30s to full) + RequestTTL = 300 * time.Second + BurstLimit = 3 // max denied/unanswered requests in burst window + RefillInterval = 10 * time.Second // one slot recovers every 10s (30s to full) ) -// Broker manages elevation requests. +// Broker manages bypass request evaluation and policy. +// It does NOT execute commands - that happens in toolsrv. type Broker struct { policy *PolicyStore notify NotifyFunc - credential CredentialFunc logf func(string, ...any) sessionValid func(string) bool @@ -37,16 +28,12 @@ type Broker struct { rateMu sync.Mutex limiters map[string]*rate.Limiter // sessionID -> denial rate limiter - - listener net.Listener } // BrokerConfig configures the broker. type BrokerConfig struct { - SocketPath string PolicyPath string // disk path for global policy YAML Notify NotifyFunc - Credential CredentialFunc // prompts user for sudo password; nil = sudo unsupported Logf func(string, ...any) SessionValid func(sessionID string) bool // returns true if session exists } @@ -54,11 +41,7 @@ type BrokerConfig struct { // NotifyFunc is called when a new elevation request needs user attention. type NotifyFunc func(*Request) -// CredentialFunc prompts the user for a sudo password. Returns the password -// or an error if the user cancels. Called only after elevation is approved. -type CredentialFunc func(req *Request) (string, error) - -// NewBroker creates and starts the elevation broker. +// NewBroker creates the bypass broker. func NewBroker(cfg BrokerConfig) (*Broker, error) { if cfg.Logf == nil { cfg.Logf = func(string, ...any) {} @@ -67,40 +50,23 @@ func NewBroker(cfg BrokerConfig) (*Broker, error) { cfg.Notify = func(*Request) {} } - // Ensure socket directory exists - dir := socketDir(cfg.SocketPath) - os.MkdirAll(dir, 0700) //nolint:errcheck - - // Remove stale socket - os.Remove(cfg.SocketPath) //nolint:errcheck - - ln, err := net.Listen("unix", cfg.SocketPath) - if err != nil { - return nil, fmt.Errorf("elevate: listen %s: %w", cfg.SocketPath, err) - } - // Restrict socket permissions - os.Chmod(cfg.SocketPath, 0600) //nolint:errcheck - b := &Broker{ policy: NewPolicyStore(cfg.PolicyPath), notify: cfg.Notify, - credential: cfg.Credential, logf: cfg.Logf, sessionValid: cfg.SessionValid, pending: make(map[string]*Request), sessions: make(map[string]*Policy), limiters: make(map[string]*rate.Limiter), - listener: ln, } - go b.acceptLoop() - cfg.Logf("elevate: listening on %s", cfg.SocketPath) + cfg.Logf("elevate: broker ready") return b, nil } // Close shuts down the broker. func (b *Broker) Close() { - b.listener.Close() + // No resources to clean up now that socket is gone } // Pending returns all pending requests. @@ -184,9 +150,6 @@ func (b *Broker) RemoveSession(sessionID string) { } // limiterFor returns (or creates) the denial rate limiter for a session. -// The limiter starts full (BurstLimit tokens available). Tokens are only -// consumed on denial/timeout, not on approval. One token refills every -// RefillInterval (10s by default), so a full burst window is 30s. func (b *Broker) limiterFor(sessionID string) *rate.Limiter { b.rateMu.Lock() defer b.rateMu.Unlock() @@ -204,7 +167,6 @@ func (b *Broker) checkRateLimit(sessionID string) bool { } // recordDenial consumes a token from the session's denial budget. -// Called when a request is denied or times out (user not responding). func (b *Broker) recordDenial(sessionID string) { if sessionID == "" { return @@ -213,7 +175,7 @@ func (b *Broker) recordDenial(sessionID string) { } // EvaluateRequest evaluates a bypass request from toolsrv and returns the decision. -// This is the 9P-based flow where execution happens in toolsrv, not here. +// Execution happens in toolsrv, not here. // Returns (approved, error). If needs user approval, blocks until resolved or timeout. func (b *Broker) EvaluateRequest(sessionID, cmd, cwd string, env map[string]string, sudo bool) (bool, error) { // Validate session @@ -300,304 +262,3 @@ func (b *Broker) addSessionRule(sessionID string, r Rule) { } p.Add(r) } - -func (b *Broker) acceptLoop() { - for { - conn, err := b.listener.Accept() - if err != nil { - return // listener closed - } - go b.handleConn(conn) - } -} - -func (b *Broker) handleConn(conn net.Conn) { - defer conn.Close() - - // Read request (JSON terminated by newline) - conn.SetReadDeadline(time.Now().Add(10 * time.Second)) //nolint:errcheck - buf := make([]byte, 0, 4096) - tmp := make([]byte, 4096) - for { - n, err := conn.Read(tmp) - if n > 0 { - buf = append(buf, tmp[:n]...) - } - if err != nil || len(buf) > 1024*1024 { - break - } - if idx := indexOf(buf, '\n'); idx >= 0 { - buf = buf[:idx] - break - } - } - conn.SetReadDeadline(time.Time{}) //nolint:errcheck - - var msg struct { - Cmd string `json:"cmd"` - Cwd string `json:"cwd"` - Env map[string]string `json:"env"` - Session string `json:"session"` - Sudo bool `json:"sudo"` - } - if err := json.Unmarshal(buf, &msg); err != nil { - sendFrame(conn, FrameData, []byte(fmt.Sprintf("elevate: bad request: %v\n", err))) - sendFrame(conn, FrameExit, []byte("1")) - return - } - - if msg.Cmd == "" { - sendFrame(conn, FrameData, []byte("elevate: empty command\n")) - sendFrame(conn, FrameExit, []byte("1")) - return - } - - // Identify caller session from request payload - sessionID := msg.Session - if sessionID == "" { - b.logf("elevate: denied cmd=%q (no session identity)", msg.Cmd) - sendFrame(conn, FrameData, []byte("elevate-broker: denied (no session identity)\n")) - sendFrame(conn, FrameExit, []byte("1")) - return - } - if b.sessionValid != nil && !b.sessionValid(sessionID) { - b.logf("elevate: denied cmd=%q session=%s (session not found)", msg.Cmd, sessionID) - sendFrame(conn, FrameData, []byte("elevate-broker: denied (session not found)\n")) - sendFrame(conn, FrameExit, []byte("1")) - return - } - - // Rate limit: leaky bucket — 3 denied/unanswered requests per 30s window - if !b.checkRateLimit(sessionID) { - b.logf("elevate: rate-limited session=%s cmd=%q", sessionID, msg.Cmd) - sendFrame(conn, FrameData, []byte("elevate-broker: rate-limited (too many denied/unanswered requests)\n")) - sendFrame(conn, FrameExit, []byte("1")) - return - } - - // Check policy (session policy includes global) - effective := b.SessionPolicy(sessionID) - if effective.Matches(msg.Cmd, "") { - b.logf("elevate: auto-approved cmd=%q session=%s", msg.Cmd, sessionID) - if msg.Sudo { - b.executeWithSudo(conn, msg.Cmd, msg.Cwd, msg.Env, &Request{Cmd: msg.Cmd, SessionID: sessionID}) - } else { - b.executeAndStream(conn, msg.Cmd, msg.Cwd, msg.Env) - } - return - } - - // Create pending request - req := &Request{ - ID: nextRequestID(), - Cmd: msg.Cmd, - Cwd: msg.Cwd, - Env: msg.Env, - SessionID: sessionID, - Sudo: msg.Sudo, - CreatedAt: time.Now(), - resolved: make(chan Resolution, 1), - conn: conn, - } - - b.mu.Lock() - b.pending[req.ID] = req - b.mu.Unlock() - - // Notify (D-Bus notification, etc.) - b.notify(req) - b.logf("elevate: pending id=%s cmd=%q session=%s", req.ID, msg.Cmd, sessionID) - - // Wait for resolution or timeout - timer := time.NewTimer(RequestTTL) - defer timer.Stop() - - var res Resolution - select { - case res = <-req.Resolved(): - case <-timer.C: - // Timeout - remove from pending and deny - b.mu.Lock() - delete(b.pending, req.ID) - b.mu.Unlock() - res = ResolveTimeout - } - - switch res { - case ResolveApprove, ResolvePersist: - b.logf("elevate: approved id=%s cmd=%q", req.ID, msg.Cmd) - if req.Sudo { - b.executeWithSudo(conn, msg.Cmd, msg.Cwd, msg.Env, req) - } else { - b.executeAndStream(conn, msg.Cmd, msg.Cwd, msg.Env) - } - default: - b.recordDenial(sessionID) - b.logf("elevate: denied id=%s cmd=%q reason=%s", req.ID, msg.Cmd, res) - sendFrame(conn, FrameData, []byte(fmt.Sprintf("elevate-broker: request %s by user\n", res))) - sendFrame(conn, FrameExit, []byte("1")) - } -} - -func (b *Broker) executeWithSudo(conn net.Conn, cmd, cwd string, env map[string]string, req *Request) { - if b.credential == nil { - sendFrame(conn, FrameData, []byte("elevate-broker: sudo requested but no credential provider configured\n")) - sendFrame(conn, FrameExit, []byte("1")) - return - } - - password, err := b.credential(req) - if err != nil { - b.logf("elevate: credential denied for cmd=%q: %v", cmd, err) - sendFrame(conn, FrameData, []byte(fmt.Sprintf("elevate-broker: credential denied: %v\n", err))) - sendFrame(conn, FrameExit, []byte("1")) - return - } - - // Build: printf '%s\n' '' | sudo -S bash -c '' - // Password is first line on stdin; sudo -S reads it then runs the command. - c := exec.Command("sudo", "-S", "bash", "-c", cmd) - c.Dir = cwd - if len(env) > 0 { - c.Env = make([]string, 0, len(env)) - for k, v := range env { - c.Env = append(c.Env, k+"="+v) - } - } - c.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - - // Pipe password to stdin - stdinPipe, err := c.StdinPipe() - if err != nil { - sendFrame(conn, FrameData, []byte(fmt.Sprintf("elevate: stdin pipe: %v\n", err))) - sendFrame(conn, FrameExit, []byte("1")) - return - } - - // Pipe stdout+stderr to the connection via frames - pr, pw, err := os.Pipe() - if err != nil { - sendFrame(conn, FrameData, []byte(fmt.Sprintf("elevate: pipe: %v\n", err))) - sendFrame(conn, FrameExit, []byte("1")) - return - } - c.Stdout = pw - c.Stderr = pw - - if err := c.Start(); err != nil { - pw.Close() - pr.Close() - sendFrame(conn, FrameData, []byte(fmt.Sprintf("elevate: exec sudo: %v\n", err))) - sendFrame(conn, FrameExit, []byte("1")) - return - } - - // Send password to sudo -S - fmt.Fprintf(stdinPipe, "%s\n", password) - stdinPipe.Close() - pw.Close() - - // Stream output - buf := make([]byte, 4096) - for { - n, err := pr.Read(buf) - if n > 0 { - sendFrame(conn, FrameData, buf[:n]) - } - if err != nil { - break - } - } - pr.Close() - - err = c.Wait() - exitCode := 0 - if err != nil { - if ee, ok := err.(*exec.ExitError); ok { - exitCode = ee.ExitCode() - } else { - exitCode = 1 - } - } - sendFrame(conn, FrameExit, []byte(fmt.Sprintf("%d", exitCode))) -} - -func (b *Broker) executeAndStream(conn net.Conn, cmd, cwd string, env map[string]string) { - c := exec.Command("bash", "-c", cmd) - c.Dir = cwd - if len(env) > 0 { - c.Env = make([]string, 0, len(env)) - for k, v := range env { - c.Env = append(c.Env, k+"="+v) - } - } - c.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - - // Pipe stdout+stderr to the connection via frames - pr, pw, err := os.Pipe() - if err != nil { - sendFrame(conn, FrameData, []byte(fmt.Sprintf("elevate: pipe: %v\n", err))) - sendFrame(conn, FrameExit, []byte("1")) - return - } - c.Stdout = pw - c.Stderr = pw - - if err := c.Start(); err != nil { - pw.Close() - pr.Close() - sendFrame(conn, FrameData, []byte(fmt.Sprintf("elevate: exec: %v\n", err))) - sendFrame(conn, FrameExit, []byte("1")) - return - } - pw.Close() - - // Stream output - buf := make([]byte, 4096) - for { - n, err := pr.Read(buf) - if n > 0 { - sendFrame(conn, FrameData, buf[:n]) - } - if err != nil { - break - } - } - pr.Close() - - err = c.Wait() - exitCode := 0 - if err != nil { - if ee, ok := err.(*exec.ExitError); ok { - exitCode = ee.ExitCode() - } else { - exitCode = 1 - } - } - sendFrame(conn, FrameExit, []byte(fmt.Sprintf("%d", exitCode))) -} - -func sendFrame(conn net.Conn, frameType byte, payload []byte) { - header := make([]byte, 5) - header[0] = frameType - binary.BigEndian.PutUint32(header[1:], uint32(len(payload))) - conn.Write(header) //nolint:errcheck - conn.Write(payload) //nolint:errcheck -} - -func indexOf(b []byte, c byte) int { - for i, v := range b { - if v == c { - return i - } - } - return -1 -} - -func socketDir(path string) string { - idx := strings.LastIndex(path, "/") - if idx < 0 { - return "." - } - return path[:idx] -} diff --git a/cmd/olliesrv/internal/bypass/broker_test.go b/cmd/olliesrv/internal/bypass/broker_test.go index f65d5e3..6cc1d75 100644 --- a/cmd/olliesrv/internal/bypass/broker_test.go +++ b/cmd/olliesrv/internal/bypass/broker_test.go @@ -1,25 +1,18 @@ package bypass import ( - "encoding/binary" - "encoding/json" - "fmt" - "net" "os" "path/filepath" - "strings" "testing" "time" ) -func startTestBroker(t *testing.T) (*Broker, string) { +func startTestBroker(t *testing.T) *Broker { t.Helper() dir := t.TempDir() - sockPath := filepath.Join(dir, "elevate.sock") policyPath := filepath.Join(dir, "policy.yaml") b, err := NewBroker(BrokerConfig{ - SocketPath: sockPath, PolicyPath: policyPath, Logf: t.Logf, SessionValid: func(id string) bool { return id == "test-session" }, @@ -28,152 +21,107 @@ func startTestBroker(t *testing.T) (*Broker, string) { t.Fatal(err) } t.Cleanup(b.Close) - return b, sockPath -} - -func sendRequest(t *testing.T, sockPath, cmd, cwd string) net.Conn { - return sendRequestWithSession(t, sockPath, cmd, cwd, "test-session") -} - -func sendRequestWithSession(t *testing.T, sockPath, cmd, cwd, session string) net.Conn { - t.Helper() - conn, err := net.DialTimeout("unix", sockPath, 2*time.Second) - if err != nil { - t.Fatal(err) - } - req, _ := json.Marshal(struct { - Cmd string `json:"cmd"` - Cwd string `json:"cwd"` - Env map[string]string `json:"env"` - Session string `json:"session,omitempty"` - }{Cmd: cmd, Cwd: cwd, Env: map[string]string{"PATH": "/usr/bin"}, Session: session}) - req = append(req, '\n') - conn.Write(req) //nolint:errcheck - return conn -} - -func readFrames(t *testing.T, conn net.Conn) (string, int) { - t.Helper() - var output string - exitCode := -1 - header := make([]byte, 5) - for { - conn.SetReadDeadline(time.Now().Add(5 * time.Second)) //nolint:errcheck - _, err := conn.Read(header) - if err != nil { - break - } - length := binary.BigEndian.Uint32(header[1:5]) - payload := make([]byte, length) - if length > 0 { - conn.Read(payload) //nolint:errcheck - } - switch header[0] { - case 'd': - output += string(payload) - case 'x': - fmt.Sscanf(string(payload), "%d", &exitCode) - return output, exitCode - } - } - return output, exitCode + return b } func TestBrokerAutoApprove(t *testing.T) { - b, sockPath := startTestBroker(t) + b := startTestBroker(t) // Add a policy rule b.GlobalPolicy().AddGlobal(Rule{Cmd: "echo *"}) - // Send a matching command - conn := sendRequest(t, sockPath, "echo hello", os.TempDir()) - defer conn.Close() - - output, exitCode := readFrames(t, conn) - if exitCode != 0 { - t.Errorf("expected exit 0, got %d; output: %s", exitCode, output) + // Evaluate a matching command + approved, err := b.EvaluateRequest("test-session", "echo hello", os.TempDir(), nil, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) } - if output != "hello\n" { - t.Errorf("expected 'hello\n', got %q", output) + if !approved { + t.Error("expected auto-approval for matching policy") } } func TestBrokerDenyNoSession(t *testing.T) { - _, sockPath := startTestBroker(t) + b := startTestBroker(t) - // Send a request with no session ID - conn := sendRequestWithSession(t, sockPath, "echo hello", os.TempDir(), "") - defer conn.Close() - - output, exitCode := readFrames(t, conn) - if exitCode != 1 { - t.Errorf("expected exit 1, got %d", exitCode) + // Evaluate with no session ID + approved, err := b.EvaluateRequest("", "echo hello", os.TempDir(), nil, false) + if err == nil { + t.Fatal("expected error for empty session") } - if !strings.Contains(output, "no session identity") { - t.Errorf("expected 'no session identity' in output, got %q", output) + if approved { + t.Error("should not approve with no session") + } + if err.Error() != "no session identity" { + t.Errorf("expected 'no session identity', got %q", err.Error()) } } func TestBrokerDenyInvalidSession(t *testing.T) { - _, sockPath := startTestBroker(t) + b := startTestBroker(t) - // Send a request with a session ID that doesn't exist - conn := sendRequestWithSession(t, sockPath, "echo hello", os.TempDir(), "nonexistent-session") - defer conn.Close() - - output, exitCode := readFrames(t, conn) - if exitCode != 1 { - t.Errorf("expected exit 1, got %d", exitCode) + // Evaluate with a session ID that doesn't exist + approved, err := b.EvaluateRequest("nonexistent-session", "echo hello", os.TempDir(), nil, false) + if err == nil { + t.Fatal("expected error for invalid session") } - if !strings.Contains(output, "session not found") { - t.Errorf("expected 'session not found' in output, got %q", output) + if approved { + t.Error("should not approve with invalid session") + } + if err.Error() != "session not found" { + t.Errorf("expected 'session not found', got %q", err.Error()) } } -func TestBrokerDenyOnTimeout(t *testing.T) { - if testing.Short() { - t.Skip("skipping timeout test in short mode") - } +func TestBrokerPendingAndResolve(t *testing.T) { + b := startTestBroker(t) - // Use a very short TTL for testing - dir := t.TempDir() - sockPath := filepath.Join(dir, "elevate.sock") - policyPath := filepath.Join(dir, "policy.yaml") - - b, err := NewBroker(BrokerConfig{ - SocketPath: sockPath, - PolicyPath: policyPath, - Logf: t.Logf, + // Start evaluation in a goroutine (it will block waiting for resolution) + resultCh := make(chan struct { + approved bool + err error }) - if err != nil { - t.Fatal(err) - } - defer b.Close() + go func() { + approved, err := b.EvaluateRequest("test-session", "rm -rf /", os.TempDir(), nil, false) + resultCh <- struct { + approved bool + err error + }{approved, err} + }() - // No policy match → goes to pending - conn := sendRequest(t, sockPath, "rm -rf /", os.TempDir()) - defer conn.Close() - - // Manually deny after a short wait + // Wait for it to appear in pending time.Sleep(100 * time.Millisecond) pending := b.Pending() if len(pending) != 1 { t.Fatalf("expected 1 pending, got %d", len(pending)) } + + // Deny it b.Resolve(pending[0].ID, ResolveDeny) - output, exitCode := readFrames(t, conn) - if exitCode != 1 { - t.Errorf("expected exit 1 on deny, got %d", exitCode) + // Check result + result := <-resultCh + if result.err != nil { + t.Errorf("unexpected error: %v", result.err) + } + if result.approved { + t.Error("expected denial") } - _ = output } -func TestBrokerApproveAndExecute(t *testing.T) { - b, sockPath := startTestBroker(t) +func TestBrokerApprove(t *testing.T) { + b := startTestBroker(t) - conn := sendRequest(t, sockPath, "echo approved", os.TempDir()) - defer conn.Close() + resultCh := make(chan struct { + approved bool + err error + }) + go func() { + approved, err := b.EvaluateRequest("test-session", "echo approved", os.TempDir(), nil, false) + resultCh <- struct { + approved bool + err error + }{approved, err} + }() // Wait for it to appear in pending time.Sleep(100 * time.Millisecond) @@ -185,21 +133,25 @@ func TestBrokerApproveAndExecute(t *testing.T) { // Approve it b.Resolve(pending[0].ID, ResolveApprove) - output, exitCode := readFrames(t, conn) - if exitCode != 0 { - t.Errorf("expected exit 0, got %d; output: %s", exitCode, output) + // Check result + result := <-resultCh + if result.err != nil { + t.Errorf("unexpected error: %v", result.err) } - if output != "approved\n" { - t.Errorf("expected 'approved\n', got %q", output) + if !result.approved { + t.Error("expected approval") } } func TestBrokerPersist(t *testing.T) { - b, sockPath := startTestBroker(t) + b := startTestBroker(t) // First request: no policy, goes to pending - conn1 := sendRequest(t, sockPath, "echo persist-me", os.TempDir()) - defer conn1.Close() + resultCh := make(chan bool) + go func() { + approved, _ := b.EvaluateRequest("test-session", "echo persist-me", os.TempDir(), nil, false) + resultCh <- approved + }() time.Sleep(100 * time.Millisecond) pending := b.Pending() @@ -210,24 +162,17 @@ func TestBrokerPersist(t *testing.T) { // Persist it b.Resolve(pending[0].ID, ResolvePersist) - output, exitCode := readFrames(t, conn1) - if exitCode != 0 { - t.Errorf("first request: expected exit 0, got %d", exitCode) - } - if output != "persist-me\n" { - t.Errorf("first request: expected 'persist-me\n', got %q", output) + if !<-resultCh { + t.Error("first request should be approved") } // Second request with same command: should auto-approve (no pending) - conn2 := sendRequest(t, sockPath, "echo persist-me", os.TempDir()) - defer conn2.Close() - - output2, exitCode2 := readFrames(t, conn2) - if exitCode2 != 0 { - t.Errorf("second request: expected exit 0, got %d", exitCode2) + approved, err := b.EvaluateRequest("test-session", "echo persist-me", os.TempDir(), nil, false) + if err != nil { + t.Fatalf("second request error: %v", err) } - if output2 != "persist-me\n" { - t.Errorf("second request: expected 'persist-me\n', got %q", output2) + if !approved { + t.Error("second request should auto-approve") } // Verify no pending requests @@ -237,7 +182,7 @@ func TestBrokerPersist(t *testing.T) { } func TestBrokerRateLimitLeakyBucket(t *testing.T) { - b, _ := startTestBroker(t) + b := startTestBroker(t) sessionID := "test-session" @@ -260,9 +205,9 @@ func TestBrokerRateLimitLeakyBucket(t *testing.T) { } func TestBrokerRateLimitRefill(t *testing.T) { - b, _ := startTestBroker(t) + b := startTestBroker(t) - sid := "refill-test" + sid := "test-session" // Exhaust the bucket for i := 0; i < BurstLimit; i++ { @@ -272,8 +217,7 @@ func TestBrokerRateLimitRefill(t *testing.T) { t.Fatal("should be rate-limited") } - // Manually set the limiter to allow (simulates time passing) - // We can't easily wait 10s in a unit test, so verify RemoveSession resets. + // RemoveSession resets the limiter b.RemoveSession(sid) if !b.checkRateLimit(sid) { t.Fatal("should be allowed after RemoveSession (fresh limiter)") @@ -281,9 +225,9 @@ func TestBrokerRateLimitRefill(t *testing.T) { } func TestBrokerRemoveSessionCleansUpLimiter(t *testing.T) { - b, _ := startTestBroker(t) + b := startTestBroker(t) - sid := "cleanup-test" + sid := "test-session" // Record some denials b.recordDenial(sid) diff --git a/cmd/olliesrv/internal/bypass/request.go b/cmd/olliesrv/internal/bypass/request.go index 062be21..b570a46 100644 --- a/cmd/olliesrv/internal/bypass/request.go +++ b/cmd/olliesrv/internal/bypass/request.go @@ -2,7 +2,6 @@ package bypass import ( "fmt" - "net" "sync/atomic" "time" ) @@ -33,19 +32,18 @@ func (r Resolution) String() string { } } -// Request represents a pending elevation request. +// Request represents a pending bypass request. type Request struct { ID string Cmd string Cwd string Env map[string]string - SessionID string // ollie session that made the request (from SO_PEERCRED mapping) - Sudo bool // requires sudo credential after approval + SessionID string + Sudo bool CreatedAt time.Time // Resolution channel — exactly one value sent when resolved. resolved chan Resolution - conn net.Conn // the elevate client connection (held open until resolved) } // Resolved returns the channel that receives the resolution. diff --git a/cmd/olliesrv/main.go b/cmd/olliesrv/main.go index e03373b..74972f8 100644 --- a/cmd/olliesrv/main.go +++ b/cmd/olliesrv/main.go @@ -7,10 +7,8 @@ import ( "fmt" "net" "os" - "os/exec" "os/signal" "path/filepath" - "strings" "syscall" "9fans.net/go/plan9/client" @@ -84,39 +82,16 @@ func runServer(sockPath string) { // Server creation deferred until after bypass broker is ready (see below) // Start bypass broker - bypassSocket := filepath.Join(paths.RuntimeDir(), "ollie", "bypass.sock") - os.Setenv("OLLIE_BYPASS_SOCKET", bypassSocket) //nolint:errcheck policyPath := filepath.Join(paths.DataDir(), "bypass-policy.yaml") { notifyFn := func(req *bypass.Request) { notifyBypass(req) } - credentialPrompt := func(req *bypass.Request) (string, error) { - // Try kdialog (KDE), then zenity (GTK), then terminal fallback - title := fmt.Sprintf("sudo password for: %s", req.Cmd) - if path, err := exec.LookPath("kdialog"); err == nil { - out, err := exec.Command(path, "--password", title).Output() - if err != nil { - return "", fmt.Errorf("user cancelled") - } - return strings.TrimRight(string(out), "\n"), nil - } - if path, err := exec.LookPath("zenity"); err == nil { - out, err := exec.Command(path, "--password", "--title", title).Output() - if err != nil { - return "", fmt.Errorf("user cancelled") - } - return strings.TrimRight(string(out), "\n"), nil - } - return "", fmt.Errorf("no credential provider available (install kdialog or zenity)") - } var err error bypassBroker, err = bypass.NewBroker(bypass.BrokerConfig{ - SocketPath: bypassSocket, PolicyPath: policyPath, Notify: notifyFn, - Credential: credentialPrompt, Logf: func(f string, a ...any) { fmt.Printf(f+"\n", a...) }, SessionValid: func(id string) bool { return session.Lookup(id) != nil }, })