detached 0e6522c

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)
44
45// AllScopes returns the ten scopes in a stable order.
46func AllScopes() []Scope {
47	return []Scope{
48		ScopeRepoRead, ScopeRepoWrite, ScopeRepoAdmin,
49		ScopeMRWrite, ScopeMRMerge, ScopeMRReview,
50		ScopeCIWrite, ScopeSecretsWrite, ScopeRunnerRegister,
51		ScopeTokenAdmin,
52	}
53}
54
55// ParseScope parses the text form of a scope, such as "repo:read".
56func ParseScope(s string) (Scope, error) {
57	sc := Scope(s)
58	if slices.Contains(AllScopes(), sc) {
59		return sc, nil
60	}
61	return "", fmt.Errorf("%w: %q", ErrInvalidScope, s)
62}
63
64// String returns the scope as text.
65func (s Scope) String() string { return string(s) }
66
67// TokenHash is the SHA-256 of a token secret. Secrets are never stored.
68type TokenHash [sha256.Size]byte
69
70// HashTokenSecret returns the hash of a token secret.
71func HashTokenSecret(secret string) TokenHash { return sha256.Sum256([]byte(secret)) }
72
73// String returns the hash as lowercase hex.
74func (h TokenHash) String() string { return hex.EncodeToString(h[:]) }
75
76// Token is a stored credential: what it may do, where, and until when.
77type Token struct {
78	// ID identifies the token; it is not secret.
79	ID    string
80	Label string
81	Hash  TokenHash
82	// Scopes is never empty.
83	Scopes []Scope
84	// Repos limits the token to these repositories; empty means every one.
85	Repos []RepoName
86	// BranchPrefixes limits the branches the token may write to those under
87	// one of these prefixes (each ends with "/"); empty means every branch.
88	// A limited token cannot write tags or other refs, nor create repositories.
89	BranchPrefixes []string
90	// ExpiresAt is the zero time for a token that never expires.
91	ExpiresAt  time.Time
92	CreatedAt  time.Time
93	LastUsedAt time.Time // zero until first used
94}
95
96// Expired reports whether the token is no longer valid at now.
97func (t Token) Expired(now time.Time) bool {
98	return !t.ExpiresAt.IsZero() && !now.Before(t.ExpiresAt)
99}
100
101const (
102	// MaxBranchPrefixLen is the longest branch prefix, in bytes.
103	MaxBranchPrefixLen = 100
104	// MaxBranchPrefixes is the most branch prefixes of a token.
105	MaxBranchPrefixes = 8
106)
107
108// ParseBranchPrefix validates a branch prefix such as "agent/claude/": it ends
109// with "/", the rest is a valid branch name, and it has at most
110// MaxBranchPrefixLen bytes. Errors match ErrInvalidToken.
111func ParseBranchPrefix(s string) (string, error) {
112	if len(s) > MaxBranchPrefixLen {
113		return "", fmt.Errorf("%w: branch prefix is longer than %d bytes", ErrInvalidToken, MaxBranchPrefixLen)
114	}
115	name, ok := strings.CutSuffix(s, "/")
116	if !ok {
117		return "", fmt.Errorf("%w: branch prefix %q must end with /", ErrInvalidToken, s)
118	}
119	if _, err := ParseBranchName(name); err != nil {
120		return "", fmt.Errorf("%w: branch prefix %q: %w", ErrInvalidToken, s, err)
121	}
122	return s, nil
123}
124
125// ParseBranchPrefixes validates a list of branch prefixes: at most
126// MaxBranchPrefixes, each valid, duplicates removed, order kept. Errors match
127// ErrInvalidToken. The result is nil for an empty list.
128func ParseBranchPrefixes(in []string) ([]string, error) {
129	var out []string
130	for _, s := range in {
131		p, err := ParseBranchPrefix(s)
132		if err != nil {
133			return nil, err
134		}
135		if !slices.Contains(out, p) {
136			out = append(out, p)
137		}
138	}
139	if len(out) > MaxBranchPrefixes {
140		return nil, fmt.Errorf("%w: at most %d branch prefixes", ErrInvalidToken, MaxBranchPrefixes)
141	}
142	return out, nil
143}

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