access

package
v0.2.0-alpha.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

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 withUserPathTeamAndStore, 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

View Source
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.

View Source
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

func DeniedRouteName(r *http.Request) string

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 Mint

func Mint(secret, userID, sessionID string, now time.Time, ttl time.Duration) (string, error)

Mint signs an access token for userID within sessionID.

func UserIDFromRequest

func UserIDFromRequest(r *http.Request, secret string) (string, bool)

UserIDFromRequest returns the subject of r's bearer token.

func UserIDFromToken

func UserIDFromToken(tokenStr, secret string) (string, bool)

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

func ClaimsFromRequest(r *http.Request, secret string) (*Claims, bool)

ClaimsFromRequest reads and verifies the bearer token on r.

func Verify

func Verify(tokenStr, secret string) (*Claims, bool)

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
	Teams     coreteam.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

func (g *Guard) ActiveUser(w http.ResponseWriter, r *http.Request) (string, bool)

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 ListTeamMembers every team-scoped route already does. Waiting out the token instead would make "disable" mean "in about a week", which is not the feature.

func (*Guard) ExplicitTeam

func (g *Guard) ExplicitTeam(w http.ResponseWriter, r *http.Request, userID, teamID string) (string, string, bool)

ExplicitTeam resolves the team a caller may act in, defaulting to their personal team when the path named none.

func (*Guard) MemberAllows

func (g *Guard) MemberAllows(ctx context.Context, userID, teamID string, action coreteam.Action) bool

memberAllows reports whether the caller's role in the team permits the action.

Unlike authorizeTeamAction 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) MemberOfResourceTeam

func (g *Guard) MemberOfResourceTeam(w http.ResponseWriter, r *http.Request, userID, teamID, notFound string) bool

MemberOfResourceTeam authorizes a route addressed by a resource ID rather than by a team, where the team comes from the record the ID names.

It cannot reuse UserAndPathTeam, which takes the team from the request path; here the caller has not said which team 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) SystemAdmin

func (g *Guard) SystemAdmin(w http.ResponseWriter, r *http.Request) (string, bool)

SystemAdmin authorizes a deployment-scoped route.

Deliberately a sibling of TeamAction rather than a branch inside it. A system grant is not an argument to a team check: an administrator reaching a team'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) TeamAction

func (g *Guard) TeamAction(w http.ResponseWriter, r *http.Request, userID, teamID string, action coreteam.Action) (string, bool)

func (*Guard) TeamRole

func (g *Guard) TeamRole(w http.ResponseWriter, r *http.Request, userID, teamID string) (string, bool)

TeamRole reports the caller's role in the team, 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) UserAndDefaultTeam

func (g *Guard) UserAndDefaultTeam(w http.ResponseWriter, r *http.Request, explicitTeamID string) (userID, teamID string, ok bool)

UserAndDefaultTeam authorizes a route that does not name a team in its path.

It exists for clients that have a server but have not chosen a team: a CLI or Desktop session knows its login and nothing else. An explicit team_id is honoured and still checked for membership; an empty one resolves to the caller's personal team, which is the private single-user case the product already represents that way.

func (*Guard) UserAndPathTeam

func (g *Guard) UserAndPathTeam(w http.ResponseWriter, r *http.Request, store any, unavailable string) (userID, teamID string, ok bool)

UserAndPathTeam is the preamble of every team-scoped route: the store exists, the caller is active, and they are in the team the path names.

func (*Guard) UserAndStore

func (g *Guard) UserAndStore(w http.ResponseWriter, r *http.Request, store any, unavailable string) (string, bool)

UserAndStore authenticates the caller and refuses when the feature's store is absent.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL