9p: permission model for session files
Session files now have owner=session_id, group=agent. Permission checking is enforced on open for session files only: prompt/fifo.in: mode 0022 (group+other write, owner cannot) ctl: mode 0222 (everyone can write) Self-prompt rejection is now a natural consequence of the permission model — the session (owner) has no write bit on its own prompt file. Cross-agent prompting works via group 'agent' write bit. User access works via 'other' write bit. On attach, sessions are added to group 'agent', other users to 'user'. Removed the explicit self-prompt check from clunk.
This commit is contained in:
parent
f9c9bc8969
commit
a25dab6ee5
92
server.go
92
server.go
|
|
@ -325,6 +325,66 @@ func (s *Server) InGroup(group, user string) bool {
|
|||
return s.groups[group][user]
|
||||
}
|
||||
|
||||
// fileOwnerGroup returns the uid and gid for a given path.
|
||||
// Session files are owned by the session ID with group "agent".
|
||||
// Everything else is owned by "ollie" with group "ollie".
|
||||
func (s *Server) fileOwnerGroup(path string) (uid, gid string) {
|
||||
if strings.HasPrefix(path, "/s/") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 2)
|
||||
if len(parts) >= 1 && parts[0] != "new" {
|
||||
// Check if parts[0] is a session (not a script like sh, bfg, etc.)
|
||||
if sess := s.sessionStore.Session(parts[0]); sess != nil {
|
||||
return parts[0], "agent"
|
||||
}
|
||||
}
|
||||
}
|
||||
return "ollie", "ollie"
|
||||
}
|
||||
|
||||
// checkPerm verifies that uname has the requested access (mode) to path.
|
||||
// mode is the 9P open mode: OREAD=0, OWRITE=1, ORDWR=2, OEXEC=3.
|
||||
// Only enforced on session files (/s/{session_id}/...).
|
||||
func (s *Server) checkPerm(uname, path string, mode uint8) error {
|
||||
uid, gid := s.fileOwnerGroup(path)
|
||||
if uid == "ollie" {
|
||||
return nil // non-session files: no enforcement
|
||||
}
|
||||
dir := s.makeStat(path)
|
||||
perm := uint32(dir.Mode) & 0777
|
||||
|
||||
// Determine which permission bits apply.
|
||||
var bits uint32
|
||||
if uname == uid {
|
||||
bits = (perm >> 6) & 7
|
||||
} else if s.InGroup(gid, uname) {
|
||||
bits = (perm >> 3) & 7
|
||||
} else {
|
||||
bits = perm & 7
|
||||
}
|
||||
|
||||
// Map 9P open mode to required permission bit.
|
||||
omode := mode & 3
|
||||
switch omode {
|
||||
case plan9.OREAD:
|
||||
if bits&4 == 0 {
|
||||
return fmt.Errorf("permission denied")
|
||||
}
|
||||
case plan9.OWRITE:
|
||||
if bits&2 == 0 {
|
||||
return fmt.Errorf("permission denied")
|
||||
}
|
||||
case plan9.ORDWR:
|
||||
if bits&6 != 6 {
|
||||
return fmt.Errorf("permission denied")
|
||||
}
|
||||
case plan9.OEXEC:
|
||||
if bits&1 == 0 {
|
||||
return fmt.Errorf("permission denied")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// defaultTranscriptDir returns the transcript directory from OLLIE_TRANSCRIPT_PATH or the default.
|
||||
func defaultTranscriptDir() string {
|
||||
|
|
@ -711,6 +771,14 @@ func (s *Server) attach(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
qid := plan9.Qid{Type: QTDir, Path: 0}
|
||||
cs.fids[fc.Fid] = &fid{path: "/", qid: qid}
|
||||
s.log.Debug("Tattach uname=%q", fc.Uname)
|
||||
// Assign group membership based on whether uname is a session ID.
|
||||
if fc.Uname != "" {
|
||||
if sess := s.sessionStore.Session(fc.Uname); sess != nil {
|
||||
s.AddGroup("agent", fc.Uname)
|
||||
} else {
|
||||
s.AddGroup("user", fc.Uname)
|
||||
}
|
||||
}
|
||||
return &plan9.Fcall{Type: plan9.Rattach, Tag: fc.Tag, Qid: qid}
|
||||
}
|
||||
|
||||
|
|
@ -778,6 +846,10 @@ func (s *Server) open(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
return errFcall(fc, "bad fid")
|
||||
}
|
||||
|
||||
if err := s.checkPerm(cs.uname, f.path, fc.Mode); err != nil {
|
||||
return errFcall(fc, err.Error())
|
||||
}
|
||||
|
||||
f.mode = fc.Mode
|
||||
s.log.Debug("Topen fid=%d path=%q mode=%d", fc.Fid, f.path, fc.Mode)
|
||||
return &plan9.Fcall{Type: plan9.Ropen, Tag: fc.Tag, Qid: f.qid}
|
||||
|
|
@ -1220,13 +1292,6 @@ func (s *Server) clunk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
|
|||
if writable {
|
||||
s.log.Debug("Tclunk flush path=%q writeBuf=%d uname=%q", path, len(data), uname)
|
||||
input := strings.TrimSpace(string(data))
|
||||
// Reject self-prompt before async dispatch so the error reaches the client.
|
||||
if uname != "" && strings.HasPrefix(path, "/s/") {
|
||||
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 3)
|
||||
if len(parts) == 3 && parts[2] == "prompt" && uname == parts[1] {
|
||||
return errFcall(fc, "self-prompt rejected")
|
||||
}
|
||||
}
|
||||
if s.isAsyncWrite(path) {
|
||||
go s.handleWrite(path, input, uname) //nolint:errcheck
|
||||
} else if err := s.handleWrite(path, input, uname); err != nil {
|
||||
|
|
@ -1579,8 +1644,10 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
|||
}
|
||||
} else {
|
||||
switch base {
|
||||
case "ctl", "prompt", "fifo.in":
|
||||
mode = 0200
|
||||
case "ctl":
|
||||
mode = 0222 // owner+group+other write
|
||||
case "prompt", "fifo.in":
|
||||
mode = 0022 // group+other write; owner cannot write (no self-prompt)
|
||||
case "chat", "usage", "cost", "ctxsz", "models", "fifo.out", "offset", "statewait", "systemprompt":
|
||||
mode = 0444
|
||||
case "cfg":
|
||||
|
|
@ -1622,13 +1689,14 @@ func (s *Server) makeStat(path string) plan9.Dir {
|
|||
}
|
||||
}
|
||||
|
||||
uid, gid := s.fileOwnerGroup(path)
|
||||
dir := plan9.Dir{
|
||||
Qid: qid,
|
||||
Mode: mode,
|
||||
Name: base,
|
||||
Uid: "ollie",
|
||||
Gid: "ollie",
|
||||
Muid: "ollie",
|
||||
Uid: uid,
|
||||
Gid: gid,
|
||||
Muid: uid,
|
||||
}
|
||||
|
||||
// For chat and tailable mutable files, report actual size and
|
||||
|
|
|
|||
Reference in New Issue