~bigbes/sr-ht-spec

ref: cac9fe1fb81fc0c100dab64c30ffe5d5f31a8442 sr-ht-spec/authn/principal.go -rw-r--r-- 5.3 KiB
cac9fe1f — bigbes docs: three receive hooks, and the read plane's rev guard 27 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package authn

import (
	"context"
	"fmt"
)

// Kind enumerates the principals spec.sr.ht distinguishes. There are three
// values but only two of them carry authority: the design's "authorization is
// about agents, not people" collapses every human other than the instance owner
// into the anonymous case, because there is no second human in the model to
// grant anything to.
type Kind string

const (
	// KindAnonymous is an unauthenticated request — no cookie, an unreadable
	// cookie, or a cookie belonging to somebody who is not the instance owner.
	// It is a normal, expected state: the read plane is anonymous-capable.
	KindAnonymous Kind = "anonymous"

	// KindOwner is bigbes: the unified-login cookie resolved to the username in
	// [sr.ht] owner-name. This is the principal whose git push *is* the
	// approval, and the only one that may approve a proposal.
	KindOwner Kind = "owner"

	// KindAgent is a bot holding the agent bearer token. It may propose and it
	// may read; the refs rule in gitx is what stops it touching the approved
	// branch.
	KindAgent Kind = "agent"
)

// Principal is the resolved identity of a request. It is a value type with no
// pointers into request state, so it can be stashed in a context, logged, and
// passed to service/ without aliasing surprises.
//
// This is what the API layer and gitx's refs rule branch on, and it is
// deliberately the narrowest thing that supports both: which kind, and — for an
// agent — the two provenance fields that every agent write must carry.
type Principal struct {
	// Kind is which of the three principals this is. The zero value is the
	// anonymous case, so a Principal read out of a context that never had one
	// set is safe rather than privileged.
	Kind Kind

	// Owner is the instance owner username (no leading '~') this principal acts
	// as or on behalf of: itself for KindOwner, the human an agent writes for
	// for KindAgent. Empty for KindAnonymous.
	Owner string

	// Agent is the agent identity string, e.g. "claude-code/spec-writer".
	// KindAgent only. It may be empty on a read — it is demanded at the write,
	// which is the only place the design requires it.
	Agent string

	// Session is the agent's session ID, e.g. a UUID. KindAgent only, with the
	// same read/write asymmetry as Agent.
	Session string

	// TokenName is the human-readable name of the agent_token row that
	// authenticated this request. KindAgent only, diagnostics only — with one
	// token and no scopes it grants nothing.
	TokenName string

	// CookieUser is whatever username the unified-login cookie carried, even
	// when that user was not the instance owner and Kind is therefore
	// KindAnonymous. Display and logging only: never an authorization input.
	CookieUser string
}

// Anonymous returns the principal for an unauthenticated request.
func Anonymous() Principal { return Principal{Kind: KindAnonymous} }

// IsAnonymous reports whether the principal carries no authority. Written as
// "not one of the two that do" so that an unrecognised or zero Kind is denied
// rather than accidentally admitted.
func (p Principal) IsAnonymous() bool { return p.Kind != KindOwner && p.Kind != KindAgent }

// IsOwner reports whether this is the human owner — the principal that may
// approve proposals and whose pushes need no review.
func (p Principal) IsOwner() bool { return p.Kind == KindOwner }

// IsAgent reports whether this is an agent — the principal gitx confines to
// proposals/*.
func (p Principal) IsAgent() bool { return p.Kind == KindAgent }

// String renders the principal for logs. It never includes the token name's
// secret (there is none — the name is not the token) and never includes the
// cookie value.
func (p Principal) String() string {
	switch p.Kind {
	case KindOwner:
		return "owner ~" + p.Owner
	case KindAgent:
		agent := p.Agent
		if agent == "" {
			agent = "(unnamed)"
		}
		session := p.Session
		if session == "" {
			session = "(no session)"
		}
		return fmt.Sprintf("agent %s session %s for ~%s", agent, session, p.Owner)
	default:
		if p.CookieUser != "" {
			return "anonymous (cookie user ~" + p.CookieUser + ")"
		}
		return "anonymous"
	}
}

// AgentWriteFor builds the provenance inputs for an agent write at the given
// base revision, enforcing that the mandatory fields are present. It fails for
// a non-agent principal: the human write path goes through native
// receive-pack and constructs no commit here.
func (p Principal) AgentWriteFor(base string) (AgentWrite, error) {
	if !p.IsAgent() {
		return AgentWrite{}, fmt.Errorf("%w: %s", ErrNotAgent, p)
	}
	w := AgentWrite{Agent: p.Agent, Session: p.Session, Base: base}
	if err := w.Validate(); err != nil {
		return AgentWrite{}, err
	}
	return w, nil
}

type contextKey struct{ name string }

var principalCtxKey = &contextKey{"authn.principal"}

// WithPrincipal returns a copy of ctx carrying p.
func WithPrincipal(ctx context.Context, p Principal) context.Context {
	return context.WithValue(ctx, principalCtxKey, p)
}

// PrincipalFromContext returns the principal stored by WithPrincipal, or the
// anonymous principal when none was stored. It never panics: an
// unauthenticated request is ordinary here, and a handler reached without the
// middleware must degrade to *less* authority, not more.
func PrincipalFromContext(ctx context.Context) Principal {
	p, ok := ctx.Value(principalCtxKey).(Principal)
	if !ok {
		return Anonymous()
	}
	return p
}