detached 973e682

token.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6	"crypto/sha256"
7	"encoding/hex"
8	"fmt"
9	"slices"
10	"strings"
11	"time"
12)
13
14// TokenSecretPrefix starts every token secret.
15const TokenSecretPrefix = "ermite_"
16
17// Scope is one permission of a stored token.
18type Scope string
19
20// The scopes of ADR 0006. The last three only matter after v1 and are defined
21// now so that the token format never changes.
22const (
23	// ScopeRepoRead reads private repositories.
24	ScopeRepoRead Scope = "repo:read"
25	// ScopeRepoWrite pushes and creates a repository on first push.
26	ScopeRepoWrite Scope = "repo:write"
27	// ScopeRepoAdmin deletes a repository and changes its visibility or description.
28	ScopeRepoAdmin Scope = "repo:admin"
29	// ScopeMRWrite creates, comments on and closes merge requests.
30	ScopeMRWrite Scope = "mr:write"
31	// ScopeMRMerge merges, and deletes the merged source branch on request.
32	ScopeMRMerge Scope = "mr:merge"
33	// ScopeMRReview sets a verdict on a merge request.
34	ScopeMRReview Scope = "mr:review"
35	// ScopeCIWrite re-runs or cancels CI runs.
36	ScopeCIWrite Scope = "ci:write"
37	// ScopeSecretsWrite sets and deletes secrets and variables.
38	ScopeSecretsWrite Scope = "secrets:write"
39	// ScopeRunnerRegister registers a CI runner.
40	ScopeRunnerRegister Scope = "runner:register"
41	// ScopeTokenAdmin creates, lists and revokes stored tokens.
42	ScopeTokenAdmin Scope = "token:admin"
43	// ScopeIssueWrite creates, comments on, closes and reopens issues. Moderation
44	// is not part of it: it stays with the owner.
45	ScopeIssueWrite Scope = "issue:write"
46)
47
48// AllScopes returns the eleven scopes in a stable order.
49func AllScopes() []Scope {
50	return []Scope{
51		ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin,
52		ScopeMRWrite, ScopeMRMerge, ScopeMRReview,
53		ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister,
54		ScopeTokenAdmin,
55		ScopeIssueWrite,
56	}
57}
58
59// ParseScope parses the text form of a scope, such as "repo:read".
60func ParseScope(s string) (Scope, error) {
61	sc := Scope(s)
62	if slices.Contains(AllScopes(), sc) {
63		return sc, nil
64	}
65	return "", fmt.Errorf("%w: %q", ErrInvalidScope, s)
66}
67
68// String returns the scope as text.
69func (s Scope) String() string { return string(s) }
70
71// TokenHash is the SHA-256 of a token secret. Secrets are never stored.
72type TokenHash [sha256.Size]byte
73
74// HashTokenSecret returns the hash of a token secret.
75func HashTokenSecret(secret string) TokenHash { return sha256.Sum256([]byte(secret)) }
76
77// String returns the hash as lowercase hex.
78func (h TokenHash) String() string { return hex.EncodeToString(h[:]) }
79
80// Token is a stored credential: what it may do, where, and until when.
81type Token struct {
82	// ID identifies the token; it is not secret.
83	ID    string
84	Label string
85	Hash  TokenHash
86	// Scopes is never empty.
87	Scopes []Scope
88	// Repos limits the token to these repositories; empty means every one.
89	Repos []RepoName
90	// BranchPrefixes limits the branches the token may write to those under
91	// one of these prefixes (each ends with "/"); empty means every branch.
92	// A limited token cannot write tags or other refs, nor create repositories.
93	BranchPrefixes []string
94	// ExpiresAt is the zero time for a token that never expires.
95	ExpiresAt  time.Time
96	CreatedAt  time.Time
97	LastUsedAt time.Time // zero until first used
98}
99
100// Expired reports whether the token is no longer valid at now.
101func (t Token) Expired(now time.Time) bool {
102	return !t.ExpiresAt.IsZero() && !now.Before(t.ExpiresAt)
103}
104
105const (
106	// MaxBranchPrefixLen is the longest branch prefix, in bytes.
107	MaxBranchPrefixLen = 100
108	// MaxBranchPrefixes is the most branch prefixes of a token.
109	MaxBranchPrefixes = 8
110)
111
112// ParseBranchPrefix validates a branch prefix such as "agent/claude/": it ends
113// with "/", the rest is a valid branch name, and it has at most
114// MaxBranchPrefixLen bytes. Errors match ErrInvalidToken.
115func ParseBranchPrefix(s string) (string, error) {
116	if len(s) > MaxBranchPrefixLen {
117		return "", fmt.Errorf("%w: branch prefix is longer than %d bytes", ErrInvalidToken, MaxBranchPrefixLen)
118	}
119	name, ok := strings.CutSuffix(s, "/")
120	if !ok {
121		return "", fmt.Errorf("%w: branch prefix %q must end with /", ErrInvalidToken, s)
122	}
123	if _, err := ParseBranchName(name); err != nil {
124		return "", fmt.Errorf("%w: branch prefix %q: %w", ErrInvalidToken, s, err)
125	}
126	return s, nil
127}
128
129// ParseBranchPrefixes validates a list of branch prefixes: at most
130// MaxBranchPrefixes, each valid, duplicates removed, order kept. Errors match
131// ErrInvalidToken. The result is nil for an empty list.
132func ParseBranchPrefixes(in []string) ([]string, error) {
133	var out []string
134	for _, s := range in {
135		p, err := ParseBranchPrefix(s)
136		if err != nil {
137			return nil, err
138		}
139		if !slices.Contains(out, p) {
140			out = append(out, p)
141		}
142	}
143	if len(out) > MaxBranchPrefixes {
144		return nil, fmt.Errorf("%w: at most %d branch prefixes", ErrInvalidToken, MaxBranchPrefixes)
145	}
146	return out, nil
147}

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 4

Tags 45

↑↓ moveenter openesc close