session_admission.go RawHistoryFolder

1package daemon
2
3import (
4	"path/filepath"
5	"slices"
6	"strings"
7	"unicode/utf8"
8
9	"github.com/bnema/vev/internal/ports"
10	"github.com/bnema/vev/internal/protocol"
11)
12
13// admissionCwdMaxBytes bounds the client-supplied working directory a local
14// admitted attachment may carry. It mirrors the Hello wire bound (uint16
15// length) so an admission can never authorize a value the wire could not carry.
16const admissionCwdMaxBytes = 65535
17
18// sessionHelloAdmission reads the provisioned admission metadata the accepting
19// side stamped onto an admitted connection, through the optional structural
20// provider seam. A connection built without that seam - a direct package
21// caller, a sessionwire constructor without admission, or a raw test transport - reports
22// present=false, and the daemon applies the standard Hello validation.
23// The provider hands back a defensive copy, so the admission the daemon
24// validates can never be mutated through the connection.
25func sessionHelloAdmission(tr ports.ServerConnection) (ports.SessionAdmission, bool) {
26	provider, ok := tr.(ports.SessionAdmissionProvider)
27	if !ok || provider == nil {
28		return ports.SessionAdmission{}, false
29	}
30	admission, ok := provider.SessionAdmission()
31	if !ok {
32		return ports.SessionAdmission{}, false
33	}
34	return admission, true
35}
36
37// admissionError is the typed refusal for a Hello that contradicts, or is not
38// covered by, the provisioned admission it arrived under. It is a client
39// refusal rather than an internal failure: the caller can surface it and retry
40// under the accepted authority.
41func admissionError(text string) error {
42	return &protoErr{protocol.ErrNoSuchTarget, text}
43}
44
45// validateSessionHelloAdmission enforces the closed admission contract for one
46// Hello that arrived on an admitted connection. The admission is the accepting
47// side's own authority, so every field the peer declared in Hello is checked
48// against it rather than trusted:
49//   - the connection must be an attachment stream, so a control or observation
50//     stream can never become an attachment by sending a Hello;
51//   - a local admission carries a client-owned policy and an optional, bounded,
52//     absolute working directory, while a remote admission carries a
53//     daemon-owned policy and no client environment or working directory at all;
54//   - the Hello's locality, environment policy, and ordered environment must
55//     equal the admitted values exactly;
56//   - the closed admission variant maps to exactly one intent and credential
57//     shape, so a create can never smuggle a resume token or a target, and an
58//     exact attach can never create.
59func validateSessionHelloAdmission(h protocol.Hello, admission ports.SessionAdmission) error {
60	if err := admission.Validate(); err != nil {
61		return admissionError("invalid session admission")
62	}
63	if admission.Purpose != ports.BrokerStreamAttachment {
64		return admissionError("stream admission is not an attachment")
65	}
66	switch admission.Origin {
67	case ports.SessionOriginLocal:
68		if admission.Policy.EnvironmentPolicy != protocol.EnvironmentPolicyClientOwned {
69			return admissionError("local admission requires a client-owned environment policy")
70		}
71		if err := validateLocalAdmissionCwd(h.Cwd); err != nil {
72			return err
73		}
74	case ports.SessionOriginRemote:
75		if admission.Policy.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
76			return admissionError("remote admission requires a daemon-owned environment policy")
77		}
78		if len(h.Env) != 0 || h.Cwd != "" {
79			return admissionError("remote admission carries a client environment or working directory")
80		}
81	default:
82		return admissionError("unknown session admission origin")
83	}
84	if h.Remote != (admission.Origin == ports.SessionOriginRemote) {
85		return admissionError("hello locality contradicts the admitted origin")
86	}
87	if h.EnvironmentPolicy != admission.Policy.EnvironmentPolicy {
88		return admissionError("hello environment policy contradicts the admitted policy")
89	}
90	if !slices.Equal(h.Env, admission.Env) {
91		return admissionError("hello environment contradicts the admitted environment")
92	}
93	switch admission.Admission {
94	case ports.BrokerAdmissionCreateEphemeral:
95		if h.Intent != protocol.IntentEphemeral {
96			return admissionError("ephemeral admission requires an ephemeral hello")
97		}
98		if h.Name != "" || h.ExactTarget != nil || h.SessionTarget != nil || h.ResumeToken != 0 {
99			return admissionError("ephemeral admission forbids a name, target, or resume token")
100		}
101	case ports.BrokerAdmissionCreateNamed:
102		if h.Intent != protocol.IntentNew {
103			return admissionError("named admission requires a new-session hello")
104		}
105		if h.Name != admission.Name {
106			return admissionError("hello name contradicts the admitted session name")
107		}
108		if h.ExactTarget != nil || h.SessionTarget != nil || h.ResumeToken != 0 {
109			return admissionError("named admission forbids a target or resume token")
110		}
111	case ports.BrokerAdmissionAttachNamed:
112		if h.Intent != protocol.IntentAttach {
113			return admissionError("named attach admission requires an attach hello")
114		}
115		if h.Name != admission.Name {
116			return admissionError("hello name contradicts the admitted session name")
117		}
118		if h.ExactTarget != nil || h.SessionTarget != nil || h.ResumeToken != 0 {
119			return admissionError("named attach admission forbids a target or resume token")
120		}
121	case ports.BrokerAdmissionExact:
122		if h.ExactTarget == nil || *h.ExactTarget != admission.Target {
123			return admissionError("hello exact target contradicts the admitted target")
124		}
125		if h.Name != admission.Target.SessionName {
126			return admissionError("hello name contradicts the admitted target")
127		}
128		if h.ResumeToken == 0 {
129			if h.Intent != protocol.IntentAttach {
130				return admissionError("fresh exact admission requires an attach hello")
131			}
132		} else if h.Intent != protocol.IntentResume {
133			return admissionError("resume exact admission requires a resume hello")
134		}
135		if h.SessionTarget != nil && (h.SessionTarget.LifecycleID != admission.Target.LifecycleID ||
136			h.SessionTarget.SessionName != admission.Target.SessionName) {
137			return admissionError("session target contradicts the admitted target")
138		}
139	default:
140		return admissionError("unknown attachment admission")
141	}
142	return nil
143}
144
145// validateLocalAdmissionCwd bounds the client-supplied working directory of a
146// local admitted attachment. It stays a client data field rather than a proof:
147// an absent value falls back to the daemon's existing home resolution, and a
148// present value must be a bounded, valid, absolute path so it can never name a
149// relative or malformed location.
150func validateLocalAdmissionCwd(cwd string) error {
151	if cwd == "" {
152		return nil
153	}
154	if len(cwd) > admissionCwdMaxBytes {
155		return admissionError("client working directory is too long")
156	}
157	if !utf8.ValidString(cwd) || strings.ContainsRune(cwd, 0) {
158		return admissionError("client working directory is invalid")
159	}
160	if !filepath.IsAbs(cwd) {
161		return admissionError("client working directory is not absolute")
162	}
163	return nil
164}
165
166// validateResumeAdmittedTarget refuses an admitted exact resume whose target
167// does not name the exact session lifecycle the resume token owns, before any
168// claim, transport replacement, or ownership mutation. An unknown or
169// still-parking credential reports no owned session and returns nil, so the
170// existing fail-closed resume path keeps its own semantics for that race.
171func (d *Daemon) validateResumeAdmittedTarget(token uint64, target protocol.ExactSessionTarget) error {
172	if token == 0 {
173		return nil
174	}
175	sess := d.resumeTokenOwnedSession(token)
176	if sess == nil {
177		return nil
178	}
179	sess.mu.Lock()
180	name, incarnation := sess.name, sess.incarnation
181	sess.mu.Unlock()
182	if name != target.SessionName || incarnation != target.LifecycleID {
183		return admissionError("resume target contradicts the admitted session")
184	}
185	return nil
186}
187
188// resumeTokenOwnedSession resolves the exact session a resume credential
189// currently owns: the parked entry, an in-flight parking entry, or the live
190// attachment holding the token. It takes d.mu then a session lock, matching the
191// daemon's existing lock order, and returns nil when no session is owned.
192func (d *Daemon) resumeTokenOwnedSession(token uint64) *session {
193	d.mu.Lock()
194	defer d.mu.Unlock()
195	if sess := d.resume.sessionFor(token); sess != nil {
196		return sess
197	}
198	for _, sess := range d.sessions {
199		if sess == nil {
200			continue
201		}
202		sess.mu.Lock()
203		for candidate := range sess.attachments {
204			if candidate.attachmentActivity() == attachmentActive && candidate.resumeCapable && candidate.resumeToken == token {
205				sess.mu.Unlock()
206				return sess
207			}
208		}
209		sess.mu.Unlock()
210	}
211	return nil
212}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close