detached 59d5a0b

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

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 2

Tags 45

↑↓ moveenter openesc close