Documentation
¶
Overview ¶
Package access owns who a caller is and what they may do.
It exists because those two questions were answered inside auth.go, which made auth.go the file every other route had to reach into: fifteen of them called withUserPathSpaceAndStore, nineteen called pathValueRequired. Splitting the route packages apart is only possible once the question every route asks first has a home of its own.
Index ¶
- Constants
- func DeniedRouteName(r *http.Request) string
- func Mint(secret, userID, sessionID string, now time.Time, ttl time.Duration) (string, error)
- func UserIDFromRequest(r *http.Request, secret string) (string, bool)
- func UserIDFromToken(tokenStr, secret string) (string, bool)
- type Claims
- type Guard
- func (g *Guard) ActiveUser(w http.ResponseWriter, r *http.Request) (string, bool)
- func (g *Guard) ExplicitSpace(w http.ResponseWriter, r *http.Request, userID, spaceID string) (string, string, bool)
- func (g *Guard) MemberAllows(ctx context.Context, userID, spaceID string, action corespace.Action) bool
- func (g *Guard) MemberOfResourceSpace(w http.ResponseWriter, r *http.Request, userID, spaceID, notFound string) bool
- func (g *Guard) RejectDisabled(w http.ResponseWriter, user *coreidentity.User) bool
- func (g *Guard) SpaceAction(w http.ResponseWriter, r *http.Request, userID, spaceID string, ...) (string, bool)
- func (g *Guard) SpaceRole(w http.ResponseWriter, r *http.Request, userID, spaceID string) (string, bool)
- func (g *Guard) SystemAdmin(w http.ResponseWriter, r *http.Request) (string, bool)
- func (g *Guard) UserAndDefaultSpace(w http.ResponseWriter, r *http.Request, explicitSpaceID string) (userID, spaceID string, ok bool)
- func (g *Guard) UserAndPathSpace(w http.ResponseWriter, r *http.Request, store any, unavailable string) (userID, spaceID string, ok bool)
- func (g *Guard) UserAndStore(w http.ResponseWriter, r *http.Request, store any, unavailable string) (string, bool)
Constants ¶
const DisabledMessage = "account_disabled"
DisabledMessage is what a disabled account is told, and it is deliberately specific: the person holding the credential has already proved it is theirs, and "wrong password" would send them to reset one that works.
const TypeAccess = "access"
TypeAccess separates an access token from anything else this key might ever sign. Refresh tokens are opaque random strings rather than JWTs, so today there is nothing to confuse an access token with; the claim is here so that stays true if that ever changes.
Variables ¶
This section is empty.
Functions ¶
func DeniedRouteName ¶
DeniedRouteName names the refused route for the audit trail.
The registered pattern rather than the request path: the path carries ids a caller chose, and the trail should record which door was tried, not what the caller typed into it.
func UserIDFromRequest ¶
UserIDFromRequest returns the subject of r's bearer token.
func UserIDFromToken ¶
UserIDFromToken returns the subject of a token taken from somewhere other than the Authorization header -- the WebSocket upgrade carries it as a query parameter, because a browser cannot set a header on that request.
Types ¶
type Claims ¶
type Claims struct {
jwt.RegisteredClaims
Sub string `json:"sub"`
Typ string `json:"typ,omitempty"`
// Sid names the login chain this token belongs to, matching session_id in
// user_refresh_token. It is what lets a logout revoke the right session.
Sid string `json:"sid,omitempty"`
}
Claims is what an access token carries.
func ClaimsFromRequest ¶
ClaimsFromRequest reads and verifies the bearer token on r.
func Verify ¶
Verify checks a token and confirms it is an access token.
An empty typ is accepted: tokens signed before the claim existed are still valid until they expire, and rejecting them would sign every user out at upgrade -- which in a deployment with no email means an operator issuing a login code to each of them by hand.
type Guard ¶
type Guard struct {
JWTSecret string
Users coreidentity.UserStore
Spaces corespace.Store
Grants coreidentity.SystemGrantStore
// Audit records refusals. Nil discards them, which is what a deployment
// without a database has.
Audit *audit.Recorder
}
Guard answers, for one request, who is calling and whether they may proceed.
Every method writes the refusal itself and reports whether the caller may continue, so a route reads as a list of gates rather than a tree of error handling.
func (*Guard) ActiveUser ¶
ActiveUser authenticates the caller and refuses a disabled account.
This is where "disable this account" becomes immediate. The access token is a signed JWT the server never stores, so it cannot be retired -- the only way to stop honouring one is to check where the identity is resolved, and this is the single funnel every authenticated route reaches. The cost is one primary-key read per request, strictly less than the ListSpaceMembers every space-scoped route already does. Waiting out the token instead would make "disable" mean "in about a week", which is not the feature.
func (*Guard) ExplicitSpace ¶
func (g *Guard) ExplicitSpace(w http.ResponseWriter, r *http.Request, userID, spaceID string) (string, string, bool)
ExplicitSpace resolves the space a caller may act in, defaulting to their personal space when the path named none.
func (*Guard) MemberAllows ¶
func (g *Guard) MemberAllows(ctx context.Context, userID, spaceID string, action corespace.Action) bool
memberAllows reports whether the caller's role in the space permits the action.
Unlike authorizeSpaceAction it writes no response and records no denial. It answers a question a handler asks before it knows whether the permission is needed at all — deleting a comment requires it only when the comment is someone else's — so a false here is not a refused request.
func (*Guard) MemberOfResourceSpace ¶
func (g *Guard) MemberOfResourceSpace(w http.ResponseWriter, r *http.Request, userID, spaceID, notFound string) bool
MemberOfResourceSpace authorizes a route addressed by a resource ID rather than by a space, where the space comes from the record the ID names.
It cannot reuse UserAndPathSpace, which takes the space from the request path; here the caller has not said which space it is acting in, and must not be allowed to. The refusal is deliberately the same 404 a missing record gets: an opaque ID is an identifier and not a credential, and answering 403 would make every such route an oracle for whether an ID exists. See docs/design/unified-artifacts.md section 6.1.
func (*Guard) RejectDisabled ¶
func (g *Guard) RejectDisabled(w http.ResponseWriter, user *coreidentity.User) bool
RejectDisabled writes the refusal for a disabled account and reports whether the caller may continue. The login paths use it so the rule is stated once.
func (*Guard) SpaceAction ¶
func (*Guard) SpaceRole ¶
func (g *Guard) SpaceRole(w http.ResponseWriter, r *http.Request, userID, spaceID string) (string, bool)
SpaceRole reports the caller's role in the space, or "" when they are not a member. It is for a route that has already established membership and needs to know how much the member may do.
func (*Guard) SystemAdmin ¶
SystemAdmin authorizes a deployment-scoped route.
Deliberately a sibling of SpaceAction rather than a branch inside it. A system grant is not an argument to a space check: an administrator reaching a space's issues, artifacts, or traces passes the same membership test as anyone else. Merging the two would make that boundary depend on nobody ever passing the grant down -- see docs/design/system-administration.md section 5.2, and the test that fails if this changes.
func (*Guard) UserAndDefaultSpace ¶
func (g *Guard) UserAndDefaultSpace(w http.ResponseWriter, r *http.Request, explicitSpaceID string) (userID, spaceID string, ok bool)
UserAndDefaultSpace authorizes a route that does not name a space in its path.
It exists for clients that have a server but have not chosen a space: a CLI or Desktop session knows its login and nothing else. An explicit space_id is honoured and still checked for membership; an empty one resolves to the caller's personal space, which is the private single-user case the product already represents that way.
func (*Guard) UserAndPathSpace ¶
func (g *Guard) UserAndPathSpace(w http.ResponseWriter, r *http.Request, store any, unavailable string) (userID, spaceID string, ok bool)
UserAndPathSpace is the preamble of every space-scoped route: the store exists, the caller is active, and they are in the space the path names.