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 (
|
const (
|
||||||
PathRoot = "/"
|
PathRoot = "/"
|
||||||
PathSessions = "/s/"
|
PathSessions = "/s/"
|
||||||
|
PathElevate = "/elevate/"
|
||||||
PathSessionDir = "s/{id}"
|
PathSessionDir = "s/{id}"
|
||||||
PathTools = "t/"
|
PathTools = "t/"
|
||||||
PathSessionFile = "s/{id}/{file}"
|
PathSessionFile = "s/{id}/{file}"
|
||||||
|
|
@ -43,6 +44,14 @@ var Perms = map[string]Perm{
|
||||||
"help": 0444,
|
"help": 0444,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
PathElevate: {
|
||||||
|
DirMode: 0755,
|
||||||
|
FileMode: 0666,
|
||||||
|
Files: map[string]os.FileMode{
|
||||||
|
"policy": 0666,
|
||||||
|
"pending": 0755,
|
||||||
|
},
|
||||||
|
},
|
||||||
PathSessions: {
|
PathSessions: {
|
||||||
DirMode: 0755,
|
DirMode: 0755,
|
||||||
FileMode: 0444,
|
FileMode: 0444,
|
||||||
|
|
@ -62,6 +71,7 @@ var Perms = map[string]Perm{
|
||||||
PathTools: {DirMode: 0755, FileMode: 0755},
|
PathTools: {DirMode: 0755, FileMode: 0755},
|
||||||
PathSessionFile: {
|
PathSessionFile: {
|
||||||
Files: map[string]os.FileMode{
|
Files: map[string]os.FileMode{
|
||||||
|
"elevate": 0666,
|
||||||
"plan": 0666,
|
"plan": 0666,
|
||||||
"ctl": 0666,
|
"ctl": 0666,
|
||||||
"prompt": 0666,
|
"prompt": 0666,
|
||||||
|
|
|
||||||
24
main.go
24
main.go
|
|
@ -191,15 +191,7 @@ func runServer(sockPath string) {
|
||||||
})
|
})
|
||||||
|
|
||||||
var srv *server.Server
|
var srv *server.Server
|
||||||
if !*no9p {
|
// Server creation deferred until after elevate broker is ready (see below)
|
||||||
srv = server.New(server.Config{
|
|
||||||
Sink: sink,
|
|
||||||
SessionMgr: mgr,
|
|
||||||
RootStore: NewRootStore(),
|
|
||||||
InvalidateModels: modelCache.Invalidate,
|
|
||||||
ToolPrompt: execute.ToolPrompt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start D-Bus adapter.
|
// Start D-Bus adapter.
|
||||||
// -tcp implies -nodbus (remote/headless deployments have no session bus).
|
// -tcp implies -nodbus (remote/headless deployments have no session bus).
|
||||||
|
|
@ -260,7 +252,18 @@ func runServer(sockPath string) {
|
||||||
} else {
|
} else {
|
||||||
defer sshProxy.Close()
|
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
|
// Prefetch models asynchronously at startup
|
||||||
|
|
@ -397,6 +400,7 @@ func NewRootStore() *fs.Tree {
|
||||||
return []os.DirEntry{
|
return []os.DirEntry{
|
||||||
fs.FileEntry("backends", fs.Perms[fs.PathRoot].FileMode),
|
fs.FileEntry("backends", fs.Perms[fs.PathRoot].FileMode),
|
||||||
fs.FileEntry("ctl", 0666),
|
fs.FileEntry("ctl", 0666),
|
||||||
|
fs.DirEntry("elevate", 0755),
|
||||||
fs.FileEntry("generate", 0666),
|
fs.FileEntry("generate", 0666),
|
||||||
fs.FileEntry("models", fs.Perms[fs.PathRoot].FileMode),
|
fs.FileEntry("models", fs.Perms[fs.PathRoot].FileMode),
|
||||||
fs.FileEntry("help", 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"
|
"time"
|
||||||
|
|
||||||
"ollie/pkg/backend"
|
"ollie/pkg/backend"
|
||||||
|
"ollie/pkg/elevate"
|
||||||
olog "ollie/pkg/log"
|
olog "ollie/pkg/log"
|
||||||
"olliesrv/session"
|
|
||||||
"ollie/pkg/paths"
|
"ollie/pkg/paths"
|
||||||
"olliesrv/fs"
|
"olliesrv/fs"
|
||||||
|
"olliesrv/session"
|
||||||
|
|
||||||
"9fans.net/go/plan9"
|
"9fans.net/go/plan9"
|
||||||
)
|
)
|
||||||
|
|
@ -69,6 +70,7 @@ type Server struct {
|
||||||
sink *olog.Sink
|
sink *olog.Sink
|
||||||
sessionMgr *session.Manager
|
sessionMgr *session.Manager
|
||||||
rootStore *fs.Tree
|
rootStore *fs.Tree
|
||||||
|
elevateTree *elevateTree
|
||||||
groups map[string]map[string]bool // group → set of members
|
groups map[string]map[string]bool // group → set of members
|
||||||
invalidateModels func()
|
invalidateModels func()
|
||||||
toolPrompt func(string) string
|
toolPrompt func(string) string
|
||||||
|
|
@ -79,6 +81,7 @@ type Config struct {
|
||||||
Sink *olog.Sink
|
Sink *olog.Sink
|
||||||
SessionMgr *session.Manager
|
SessionMgr *session.Manager
|
||||||
RootStore *fs.Tree
|
RootStore *fs.Tree
|
||||||
|
ElevateBroker *elevate.Broker
|
||||||
InvalidateModels func()
|
InvalidateModels func()
|
||||||
ToolPrompt func(string) string
|
ToolPrompt func(string) string
|
||||||
}
|
}
|
||||||
|
|
@ -97,6 +100,9 @@ func New(cfg Config) *Server {
|
||||||
invalidateModels: cfg.InvalidateModels,
|
invalidateModels: cfg.InvalidateModels,
|
||||||
toolPrompt: cfg.ToolPrompt,
|
toolPrompt: cfg.ToolPrompt,
|
||||||
}
|
}
|
||||||
|
if cfg.ElevateBroker != nil {
|
||||||
|
s.elevateTree = newElevateTree(cfg.ElevateBroker)
|
||||||
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,9 +116,13 @@ type fsRoute struct {
|
||||||
// routes returns the route table for the file tree namespace.
|
// routes returns the route table for the file tree namespace.
|
||||||
// Directory permissions come from the fs.Perms registry.
|
// Directory permissions come from the fs.Perms registry.
|
||||||
func (s *Server) routes() []fsRoute {
|
func (s *Server) routes() []fsRoute {
|
||||||
return []fsRoute{
|
routes := []fsRoute{
|
||||||
{fs.PathSessions, fs.Perms[fs.PathSessions].DirMode, func() FileTree { return s.sessionMgr.Tree() }},
|
{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.
|
// 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.
|
// openEntry opens a File for the given path.
|
||||||
func (s *Server) openEntry(path string) (File, error) {
|
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)
|
st, name := s.route(path)
|
||||||
if st == nil {
|
if st == nil {
|
||||||
return nil, fmt.Errorf("%s: not found", path)
|
return nil, fmt.Errorf("%s: not found", path)
|
||||||
|
|
|
||||||
Reference in New Issue