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
- 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 Action
- type Claims
- type Guard
- func (g *Guard) ActiveUser(w http.ResponseWriter, r *http.Request) (string, bool)
- func (g *Guard) ExplicitTeam(w http.ResponseWriter, r *http.Request, userID, teamID string) (string, string, bool)
- func (g *Guard) MemberAllows(ctx context.Context, userID, teamID string, action Action) bool
- func (g *Guard) MemberOfResourceTeam(w http.ResponseWriter, r *http.Request, userID, teamID, notFound string) bool
- func (g *Guard) RejectDisabled(w http.ResponseWriter, user *model.User) bool
- func (g *Guard) SystemAdmin(w http.ResponseWriter, r *http.Request) (string, bool)
- func (g *Guard) TeamAction(w http.ResponseWriter, r *http.Request, userID, teamID string, action Action) (string, bool)
- func (g *Guard) TeamRole(w http.ResponseWriter, r *http.Request, userID, teamID string) (string, bool)
- func (g *Guard) UserAndDefaultTeam(w http.ResponseWriter, r *http.Request, explicitTeamID string) (userID, teamID string, ok bool)
- func (g *Guard) UserAndPathTeam(w http.ResponseWriter, r *http.Request, store any, unavailable string) (userID, teamID 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 Action ¶
type Action string
const ( ActionManageTeamMembers Action = "manage_team_members" ActionManageAgents Action = "manage_agents" ActionManageWorkflows Action = "manage_workflows" ActionAssignIssueWorkflow Action = "assign_issue_workflow" ActionRunWorkflow Action = "run_workflow" ActionReadAuditTrail Action = "read_audit_trail" ActionCommentIssue Action = "comment_issue" // ActionModerateIssueComments covers deleting a comment the caller did not // write. Editing another author's comment is permitted to nobody, so it is // not an action here — see internal/service/issue. ActionModerateIssueComments Action = "moderate_issue_comments" )
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 model.UserStore
Teams model.TeamStore
Grants model.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 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 ¶
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 ¶
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 ¶
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 (*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.