olliesrv: expose elevate/ and s/{id}/elevate via 9P
- /elevate/policy: read/write global policy (YAML)
- /elevate/pending/: list pending requests
- /elevate/pending/<id>: read summary, write approve/deny/persist
- /s/{id}/elevate: read/write session policy (includes global)
This commit is contained in:
parent
324abd025b
commit
238d8214a3
10
fs/perm.go
10
fs/perm.go
|
|
@ -17,6 +17,7 @@ import "os"
|
|||
const (
|
||||
PathRoot = "/"
|
||||
PathSessions = "/s/"
|
||||
PathElevate = "/elevate/"
|
||||
PathSessionDir = "s/{id}"
|
||||
PathTools = "t/"
|
||||
PathSessionFile = "s/{id}/{file}"
|
||||
|
|
@ -43,6 +44,14 @@ var Perms = map[string]Perm{
|
|||
"help": 0444,
|
||||
},
|
||||
},
|
||||
PathElevate: {
|
||||
DirMode: 0755,
|
||||
FileMode: 0666,
|
||||
Files: map[string]os.FileMode{
|
||||
"policy": 0666,
|
||||
"pending": 0755,
|
||||
},
|
||||
},
|
||||
PathSessions: {
|
||||
DirMode: 0755,
|
||||
FileMode: 0444,
|
||||
|
|
@ -62,6 +71,7 @@ var Perms = map[string]Perm{
|
|||
PathTools: {DirMode: 0755, FileMode: 0755},
|
||||
PathSessionFile: {
|
||||
Files: map[string]os.FileMode{
|
||||
"elevate": 0666,
|
||||
"plan": 0666,
|
||||
"ctl": 0666,
|
||||
"prompt": 0666,
|
||||
|
|
|
|||
24
main.go
24
main.go
|
|
@ -191,15 +191,7 @@ func runServer(sockPath string) {
|
|||
})
|
||||
|
||||
var srv *server.Server
|
||||
if !*no9p {
|
||||
srv = server.New(server.Config{
|
||||
Sink: sink,
|
||||
SessionMgr: mgr,
|
||||
RootStore: NewRootStore(),
|
||||
InvalidateModels: modelCache.Invalidate,
|
||||
ToolPrompt: execute.ToolPrompt,
|
||||
})
|
||||
}
|
||||
// Server creation deferred until after elevate broker is ready (see below)
|
||||
|
||||
// Start D-Bus adapter.
|
||||
// -tcp implies -nodbus (remote/headless deployments have no session bus).
|
||||
|
|
@ -260,7 +252,18 @@ func runServer(sockPath string) {
|
|||
} else {
|
||||
defer sshProxy.Close()
|
||||
}
|
||||
_ = elevateBroker // used by 9P files (to be added)
|
||||
}
|
||||
|
||||
// Create 9P server (after broker is ready so it can be passed in)
|
||||
if !*no9p {
|
||||
srv = server.New(server.Config{
|
||||
Sink: sink,
|
||||
SessionMgr: mgr,
|
||||
RootStore: NewRootStore(),
|
||||
ElevateBroker: elevateBroker,
|
||||
InvalidateModels: modelCache.Invalidate,
|
||||
ToolPrompt: execute.ToolPrompt,
|
||||
})
|
||||
}
|
||||
|
||||
// Prefetch models asynchronously at startup
|
||||
|
|
@ -397,6 +400,7 @@ func NewRootStore() *fs.Tree {
|
|||
return []os.DirEntry{
|
||||
fs.FileEntry("backends", fs.Perms[fs.PathRoot].FileMode),
|
||||
fs.FileEntry("ctl", 0666),
|
||||
fs.DirEntry("elevate", 0755),
|
||||
fs.FileEntry("generate", 0666),
|
||||
fs.FileEntry("models", fs.Perms[fs.PathRoot].FileMode),
|
||||
fs.FileEntry("help", fs.Perms[fs.PathRoot].FileMode),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"ollie/pkg/elevate"
|
||||
fs "olliesrv/fs"
|
||||
)
|
||||
|
||||
// elevateTree implements fs.FileTree for the /elevate/ namespace.
|
||||
type elevateTree struct {
|
||||
broker *elevate.Broker
|
||||
}
|
||||
|
||||
func newElevateTree(broker *elevate.Broker) *elevateTree {
|
||||
return &elevateTree{broker: broker}
|
||||
}
|
||||
|
||||
func (t *elevateTree) Stat(name string) (os.FileInfo, error) {
|
||||
switch {
|
||||
case name == "policy":
|
||||
return &fs.SyntheticFileInfo{Name_: "policy", Mode_: 0666}, nil
|
||||
case name == "pending":
|
||||
return &fs.SyntheticFileInfo{Name_: "pending", Mode_: 0755, IsDir_: true}, nil
|
||||
case strings.HasPrefix(name, "pending/"):
|
||||
id := strings.TrimPrefix(name, "pending/")
|
||||
if t.broker.PendingByID(id) != nil {
|
||||
return &fs.SyntheticFileInfo{Name_: id, Mode_: 0666}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%s: not found", name)
|
||||
}
|
||||
return nil, fmt.Errorf("%s: not found", name)
|
||||
}
|
||||
|
||||
func (t *elevateTree) List() ([]os.DirEntry, error) {
|
||||
return []os.DirEntry{
|
||||
fs.FileEntry("policy", 0666),
|
||||
fs.DirEntry("pending", 0755),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *elevateTree) Open(name string) (fs.File, error) {
|
||||
switch {
|
||||
case name == "policy":
|
||||
return &elevatePolicyFile{broker: t.broker}, nil
|
||||
case name == "pending":
|
||||
return &elevatePendingDir{broker: t.broker}, nil
|
||||
case strings.HasPrefix(name, "pending/"):
|
||||
id := strings.TrimPrefix(name, "pending/")
|
||||
req := t.broker.PendingByID(id)
|
||||
if req == nil {
|
||||
return nil, fmt.Errorf("%s: not found", name)
|
||||
}
|
||||
return &elevatePendingFile{broker: t.broker, req: req}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%s: not found", name)
|
||||
}
|
||||
|
||||
func (t *elevateTree) Create(name string) error { return fmt.Errorf("cannot create in elevate") }
|
||||
func (t *elevateTree) Delete(name string) error { return fmt.Errorf("cannot delete in elevate") }
|
||||
func (t *elevateTree) Rename(_, _ string) error { return fmt.Errorf("cannot rename in elevate") }
|
||||
|
||||
// --- policy file ---
|
||||
|
||||
type elevatePolicyFile struct {
|
||||
broker *elevate.Broker
|
||||
}
|
||||
|
||||
func (f *elevatePolicyFile) Stat() (os.FileInfo, error) {
|
||||
return &fs.SyntheticFileInfo{Name_: "policy", Mode_: 0666}, nil
|
||||
}
|
||||
|
||||
func (f *elevatePolicyFile) Read() ([]byte, error) {
|
||||
p := f.broker.GlobalPolicy().Global()
|
||||
return p.Marshal()
|
||||
}
|
||||
|
||||
func (f *elevatePolicyFile) Write(data []byte) error {
|
||||
var p elevate.Policy
|
||||
if err := elevate.ParsePolicy(data, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.broker.GlobalPolicy().SetGlobal(p)
|
||||
}
|
||||
|
||||
func (f *elevatePolicyFile) BlockingRead(_ context.Context, _ string) ([]byte, string, error) {
|
||||
return nil, "", fmt.Errorf("not blocking")
|
||||
}
|
||||
func (f *elevatePolicyFile) OneShot() bool { return false }
|
||||
func (f *elevatePolicyFile) IsBlocking() bool { return false }
|
||||
func (f *elevatePolicyFile) Async() bool { return false }
|
||||
|
||||
// --- pending dir ---
|
||||
|
||||
type elevatePendingDir struct {
|
||||
broker *elevate.Broker
|
||||
}
|
||||
|
||||
func (f *elevatePendingDir) Stat() (os.FileInfo, error) {
|
||||
return &fs.SyntheticFileInfo{Name_: "pending", Mode_: 0755, IsDir_: true}, nil
|
||||
}
|
||||
|
||||
func (f *elevatePendingDir) Read() ([]byte, error) {
|
||||
// List pending request IDs
|
||||
var sb strings.Builder
|
||||
for _, r := range f.broker.Pending() {
|
||||
sb.WriteString(r.ID)
|
||||
sb.WriteByte('\t')
|
||||
sb.WriteString(r.Cmd)
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
return []byte(sb.String()), nil
|
||||
}
|
||||
|
||||
func (f *elevatePendingDir) Write([]byte) error { return fmt.Errorf("read-only") }
|
||||
func (f *elevatePendingDir) BlockingRead(_ context.Context, _ string) ([]byte, string, error) {
|
||||
return nil, "", fmt.Errorf("not blocking")
|
||||
}
|
||||
func (f *elevatePendingDir) OneShot() bool { return false }
|
||||
func (f *elevatePendingDir) IsBlocking() bool { return false }
|
||||
func (f *elevatePendingDir) Async() bool { return false }
|
||||
|
||||
// --- pending/<id> file ---
|
||||
|
||||
type elevatePendingFile struct {
|
||||
broker *elevate.Broker
|
||||
req *elevate.Request
|
||||
}
|
||||
|
||||
func (f *elevatePendingFile) Stat() (os.FileInfo, error) {
|
||||
return &fs.SyntheticFileInfo{Name_: f.req.ID, Mode_: 0666}, nil
|
||||
}
|
||||
|
||||
func (f *elevatePendingFile) Read() ([]byte, error) {
|
||||
return []byte(f.req.Summary() + "\n"), nil
|
||||
}
|
||||
|
||||
func (f *elevatePendingFile) Write(data []byte) error {
|
||||
cmd := strings.TrimSpace(string(data))
|
||||
var res elevate.Resolution
|
||||
switch cmd {
|
||||
case "approve":
|
||||
res = elevate.ResolveApprove
|
||||
case "deny":
|
||||
res = elevate.ResolveDeny
|
||||
case "persist":
|
||||
res = elevate.ResolvePersist
|
||||
default:
|
||||
return fmt.Errorf("unknown resolution: %s (use approve/deny/persist)", cmd)
|
||||
}
|
||||
if !f.broker.Resolve(f.req.ID, res) {
|
||||
return fmt.Errorf("request already resolved or timed out")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *elevatePendingFile) BlockingRead(_ context.Context, _ string) ([]byte, string, error) {
|
||||
return nil, "", fmt.Errorf("not blocking")
|
||||
}
|
||||
func (f *elevatePendingFile) OneShot() bool { return false }
|
||||
func (f *elevatePendingFile) IsBlocking() bool { return false }
|
||||
func (f *elevatePendingFile) Async() bool { return false }
|
||||
|
||||
// --- s/{id}/elevate file (session policy) ---
|
||||
|
||||
type sessionElevateFile struct {
|
||||
broker *elevate.Broker
|
||||
sessionID string
|
||||
}
|
||||
|
||||
func (f *sessionElevateFile) Stat() (os.FileInfo, error) {
|
||||
return &fs.SyntheticFileInfo{Name_: "elevate", Mode_: 0666}, nil
|
||||
}
|
||||
|
||||
func (f *sessionElevateFile) Read() ([]byte, error) {
|
||||
p := f.broker.SessionPolicy(f.sessionID)
|
||||
return p.Marshal()
|
||||
}
|
||||
|
||||
func (f *sessionElevateFile) Write(data []byte) error {
|
||||
var p elevate.Policy
|
||||
if err := elevate.ParsePolicy(data, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
f.broker.SetSessionPolicy(f.sessionID, p)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *sessionElevateFile) BlockingRead(_ context.Context, _ string) ([]byte, string, error) {
|
||||
return nil, "", fmt.Errorf("not blocking")
|
||||
}
|
||||
func (f *sessionElevateFile) OneShot() bool { return false }
|
||||
func (f *sessionElevateFile) IsBlocking() bool { return false }
|
||||
func (f *sessionElevateFile) Async() bool { return false }
|
||||
|
|
@ -16,10 +16,11 @@ import (
|
|||
"time"
|
||||
|
||||
"ollie/pkg/backend"
|
||||
"ollie/pkg/elevate"
|
||||
olog "ollie/pkg/log"
|
||||
"olliesrv/session"
|
||||
"ollie/pkg/paths"
|
||||
"olliesrv/fs"
|
||||
"olliesrv/session"
|
||||
|
||||
"9fans.net/go/plan9"
|
||||
)
|
||||
|
|
@ -69,6 +70,7 @@ type Server struct {
|
|||
sink *olog.Sink
|
||||
sessionMgr *session.Manager
|
||||
rootStore *fs.Tree
|
||||
elevateTree *elevateTree
|
||||
groups map[string]map[string]bool // group → set of members
|
||||
invalidateModels func()
|
||||
toolPrompt func(string) string
|
||||
|
|
@ -79,6 +81,7 @@ type Config struct {
|
|||
Sink *olog.Sink
|
||||
SessionMgr *session.Manager
|
||||
RootStore *fs.Tree
|
||||
ElevateBroker *elevate.Broker
|
||||
InvalidateModels func()
|
||||
ToolPrompt func(string) string
|
||||
}
|
||||
|
|
@ -97,6 +100,9 @@ func New(cfg Config) *Server {
|
|||
invalidateModels: cfg.InvalidateModels,
|
||||
toolPrompt: cfg.ToolPrompt,
|
||||
}
|
||||
if cfg.ElevateBroker != nil {
|
||||
s.elevateTree = newElevateTree(cfg.ElevateBroker)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
|
@ -110,9 +116,13 @@ type fsRoute struct {
|
|||
// routes returns the route table for the file tree namespace.
|
||||
// Directory permissions come from the fs.Perms registry.
|
||||
func (s *Server) routes() []fsRoute {
|
||||
return []fsRoute{
|
||||
routes := []fsRoute{
|
||||
{fs.PathSessions, fs.Perms[fs.PathSessions].DirMode, func() FileTree { return s.sessionMgr.Tree() }},
|
||||
}
|
||||
if s.elevateTree != nil {
|
||||
routes = append(routes, fsRoute{fs.PathElevate, fs.Perms[fs.PathElevate].DirMode, func() FileTree { return s.elevateTree }})
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// route returns the tree and entry name for a path, or nil if no route matches.
|
||||
|
|
@ -134,6 +144,15 @@ func (s *Server) route(path string) (FileTree, string) {
|
|||
|
||||
// openEntry opens a File for the given path.
|
||||
func (s *Server) openEntry(path string) (File, error) {
|
||||
// Intercept s/{id}/elevate — session policy via broker
|
||||
if s.elevateTree != nil && strings.HasPrefix(path, fs.PathSessions) {
|
||||
rel := strings.TrimPrefix(path, fs.PathSessions)
|
||||
if parts := strings.SplitN(rel, "/", 2); len(parts) == 2 && parts[1] == "elevate" {
|
||||
sessionID := parts[0]
|
||||
return &sessionElevateFile{broker: s.elevateTree.broker, sessionID: sessionID}, nil
|
||||
}
|
||||
}
|
||||
|
||||
st, name := s.route(path)
|
||||
if st == nil {
|
||||
return nil, fmt.Errorf("%s: not found", path)
|
||||
|
|
|
|||
Reference in New Issue