detached 851827c

refactor(domain): unexport the principal kind and expose a label

commit
851827caf8314a1f5e5ad5a3c823eb6601e35639
author
brice · · 2026-10-10 07:10 UTC
parent
2bea74e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +37 -38
M internal/app/tokens.go +2 -2
@@ -101,7 +101,7 @@ func (t *Tokens) Create(ctx context.Context, p domain.Principal, params CreateTo
101101 log.Info().
102102 Str("token_id", tok.ID).Str("label", tok.Label).
103103 Int("scopes", len(tok.Scopes)).Int("repos", len(tok.Repos)).
104- Str("created_by", p.Kind().String()).Str("created_by_token", p.TokenID()).
104+ Str("created_by", p.Label()).Str("created_by_token", p.TokenID()).
105105 Msg("token created")
106106 return CreatedToken{Token: tok, Secret: secret}, nil
107107 }
@@ -205,7 +205,7 @@ func (t *Tokens) Revoke(ctx context.Context, p domain.Principal, id string) erro
205205 return err
206206 }
207207 log := zerowrap.FromCtx(ctx)
208- log.Info().Str("token_id", id).Str("revoked_by", p.Kind().String()).Str("revoked_by_token", p.TokenID()).Msg("token revoked")
208+ log.Info().Str("token_id", id).Str("revoked_by", p.Label()).Str("revoked_by_token", p.TokenID()).Msg("token revoked")
209209 return nil
210210 }
211211
@@ -101,7 +101,7 @@ func (t *Tokens) Create(ctx context.Context, p domain.Principal, params CreateTo
101 log.Info().101 log.Info().
102 Str("token_id", tok.ID).Str("label", tok.Label).102 Str("token_id", tok.ID).Str("label", tok.Label).
103 Int("scopes", len(tok.Scopes)).Int("repos", len(tok.Repos)).103 Int("scopes", len(tok.Scopes)).Int("repos", len(tok.Repos)).
104- Str("created_by", p.Kind().String()).Str("created_by_token", p.TokenID()).104+ Str("created_by", p.Label()).Str("created_by_token", p.TokenID()).
105 Msg("token created")105 Msg("token created")
106 return CreatedToken{Token: tok, Secret: secret}, nil106 return CreatedToken{Token: tok, Secret: secret}, nil
107 }107 }
@@ -205,7 +205,7 @@ func (t *Tokens) Revoke(ctx context.Context, p domain.Principal, id string) erro
205 return err205 return err
206 }206 }
207 log := zerowrap.FromCtx(ctx)207 log := zerowrap.FromCtx(ctx)
208- log.Info().Str("token_id", id).Str("revoked_by", p.Kind().String()).Str("revoked_by_token", p.TokenID()).Msg("token revoked")208+ log.Info().Str("token_id", id).Str("revoked_by", p.Label()).Str("revoked_by_token", p.TokenID()).Msg("token revoked")
209 return nil209 return nil
210 }210 }
211 211
M internal/app/tokens_test.go +1 -1
@@ -256,7 +256,7 @@ func TestAuthenticate(t *testing.T) {
256256 f.store.EXPECT().Touch(ctx, "t1", now).Return(nil)
257257 p, err := f.auth.Authenticate(ctx, secret)
258258 require.NoError(t, err)
259- assert.Equal(t, domain.PrincipalToken, p.Kind())
259+ assert.True(t, p.IsToken())
260260 assert.Equal(t, "t1", p.TokenID())
261261 assert.True(t, p.Allows(domain.ScopeRepoWrite, name(t, "a")))
262262 assert.False(t, p.Allows(domain.ScopeRepoWrite, name(t, "b")))
@@ -256,7 +256,7 @@ func TestAuthenticate(t *testing.T) {
256 f.store.EXPECT().Touch(ctx, "t1", now).Return(nil)256 f.store.EXPECT().Touch(ctx, "t1", now).Return(nil)
257 p, err := f.auth.Authenticate(ctx, secret)257 p, err := f.auth.Authenticate(ctx, secret)
258 require.NoError(t, err)258 require.NoError(t, err)
259- assert.Equal(t, domain.PrincipalToken, p.Kind())259+ assert.True(t, p.IsToken())
260 assert.Equal(t, "t1", p.TokenID())260 assert.Equal(t, "t1", p.TokenID())
261 assert.True(t, p.Allows(domain.ScopeRepoWrite, name(t, "a")))261 assert.True(t, p.Allows(domain.ScopeRepoWrite, name(t, "a")))
262 assert.False(t, p.Allows(domain.ScopeRepoWrite, name(t, "b")))262 assert.False(t, p.Allows(domain.ScopeRepoWrite, name(t, "b")))
M internal/domain/name_test.go +2 -1
@@ -108,5 +108,6 @@ func TestPrincipal(t *testing.T) {
108108 assert.True(t, zero.IsAnonymous())
109109 assert.False(t, zero.IsRoot())
110110 assert.True(t, domain.Root().IsRoot())
111- assert.Equal(t, domain.PrincipalAnonymous, domain.Anonymous().Kind())
111+ assert.Equal(t, "anonymous", domain.Anonymous().Label())
112+ assert.False(t, domain.Root().IsToken())
112113 }
@@ -108,5 +108,6 @@ func TestPrincipal(t *testing.T) {
108 assert.True(t, zero.IsAnonymous())108 assert.True(t, zero.IsAnonymous())
109 assert.False(t, zero.IsRoot())109 assert.False(t, zero.IsRoot())
110 assert.True(t, domain.Root().IsRoot())110 assert.True(t, domain.Root().IsRoot())
111- assert.Equal(t, domain.PrincipalAnonymous, domain.Anonymous().Kind())111+ assert.Equal(t, "anonymous", domain.Anonymous().Label())
112+ assert.False(t, domain.Root().IsToken())
112 }113 }
M internal/domain/principal.go +28 -30
@@ -7,34 +7,20 @@ import (
77 "time"
88 )
99
10-// PrincipalKind tells who a caller is.
11-type PrincipalKind uint8
10+// principalKind tells who a caller is.
11+type principalKind uint8
1212
1313 // Principal kinds. The zero value is anonymous, the least privileged.
1414 const (
15- // PrincipalAnonymous is a caller without credentials.
16- PrincipalAnonymous PrincipalKind = iota
17- // PrincipalRoot is the holder of the admin token.
18- PrincipalRoot
19- // PrincipalToken is the holder of a stored token.
20- PrincipalToken
15+ kindAnonymous principalKind = iota
16+ kindRoot
17+ kindToken
2118 )
2219
23-// String returns "anonymous", "root" or "token".
24-func (k PrincipalKind) String() string {
25- switch k {
26- case PrincipalRoot:
27- return "root"
28- case PrincipalToken:
29- return "token"
30- }
31- return "anonymous"
32-}
33-
3420 // Principal is the authenticated identity of a caller: nobody, the root
3521 // token, or a stored token with its scopes and repositories.
3622 type Principal struct {
37- kind PrincipalKind
23+ kind principalKind
3824 tokenID string
3925 scopes []Scope
4026 repos []RepoName
@@ -42,15 +28,15 @@ type Principal struct {
4228 }
4329
4430 // Anonymous returns the principal of a caller without credentials.
45-func Anonymous() Principal { return Principal{kind: PrincipalAnonymous} }
31+func Anonymous() Principal { return Principal{kind: kindAnonymous} }
4632
4733 // Root returns the principal of the admin token holder.
48-func Root() Principal { return Principal{kind: PrincipalRoot} }
34+func Root() Principal { return Principal{kind: kindRoot} }
4935
5036 // TokenPrincipal returns the principal of the holder of a stored token.
5137 func TokenPrincipal(t Token) Principal {
5238 return Principal{
53- kind: PrincipalToken,
39+ kind: kindToken,
5440 tokenID: t.ID,
5541 scopes: slices.Clone(t.Scopes),
5642 repos: slices.Clone(t.Repos),
@@ -58,14 +44,26 @@ func TokenPrincipal(t Token) Principal {
5844 }
5945 }
6046
61-// Kind returns the kind of principal.
62-func (p Principal) Kind() PrincipalKind { return p.kind }
47+// Label names the kind of principal for logs and messages: "anonymous",
48+// "root" or "token".
49+func (p Principal) Label() string {
50+ switch p.kind {
51+ case kindRoot:
52+ return "root"
53+ case kindToken:
54+ return "token"
55+ }
56+ return "anonymous"
57+}
58+
59+// IsToken reports whether p holds a stored token.
60+func (p Principal) IsToken() bool { return p.kind == kindToken }
6361
6462 // IsRoot reports whether p may do everything.
65-func (p Principal) IsRoot() bool { return p.kind == PrincipalRoot }
63+func (p Principal) IsRoot() bool { return p.kind == kindRoot }
6664
6765 // IsAnonymous reports whether p has no credentials.
68-func (p Principal) IsAnonymous() bool { return p.kind == PrincipalAnonymous }
66+func (p Principal) IsAnonymous() bool { return p.kind == kindAnonymous }
6967
7068 // TokenID returns the ID of the stored token, or "" for other principals.
7169 func (p Principal) TokenID() string { return p.tokenID }
@@ -84,9 +82,9 @@ func (p Principal) ExpiresAt() time.Time { return p.expiresAt }
8482 // every scope; anonymous none.
8583 func (p Principal) HasScope(s Scope) bool {
8684 switch p.kind {
87- case PrincipalRoot:
85+ case kindRoot:
8886 return true
89- case PrincipalToken:
87+ case kindToken:
9088 return slices.Contains(p.scopes, s)
9189 }
9290 return false
@@ -98,7 +96,7 @@ func (p Principal) Allows(s Scope, repo RepoName) bool {
9896 if !p.HasScope(s) {
9997 return false
10098 }
101- if p.kind == PrincipalToken && len(p.repos) > 0 {
99+ if p.kind == kindToken && len(p.repos) > 0 {
102100 return slices.Contains(p.repos, repo)
103101 }
104102 return true
@@ -7,34 +7,20 @@ import (
7 "time"7 "time"
8 )8 )
9 9
10-// PrincipalKind tells who a caller is.10+// principalKind tells who a caller is.
11-type PrincipalKind uint811+type principalKind uint8
12 12
13 // Principal kinds. The zero value is anonymous, the least privileged.13 // Principal kinds. The zero value is anonymous, the least privileged.
14 const (14 const (
15- // PrincipalAnonymous is a caller without credentials.15+ kindAnonymous principalKind = iota
16- PrincipalAnonymous PrincipalKind = iota16+ kindRoot
17- // PrincipalRoot is the holder of the admin token.17+ kindToken
18- PrincipalRoot
19- // PrincipalToken is the holder of a stored token.
20- PrincipalToken
21 )18 )
22 19
23-// String returns "anonymous", "root" or "token".
24-func (k PrincipalKind) String() string {
25- switch k {
26- case PrincipalRoot:
27- return "root"
28- case PrincipalToken:
29- return "token"
30- }
31- return "anonymous"
32-}
33-
34 // Principal is the authenticated identity of a caller: nobody, the root20 // Principal is the authenticated identity of a caller: nobody, the root
35 // token, or a stored token with its scopes and repositories.21 // token, or a stored token with its scopes and repositories.
36 type Principal struct {22 type Principal struct {
37- kind PrincipalKind23+ kind principalKind
38 tokenID string24 tokenID string
39 scopes []Scope25 scopes []Scope
40 repos []RepoName26 repos []RepoName
@@ -42,15 +28,15 @@ type Principal struct {
42 }28 }
43 29
44 // Anonymous returns the principal of a caller without credentials.30 // Anonymous returns the principal of a caller without credentials.
45-func Anonymous() Principal { return Principal{kind: PrincipalAnonymous} }31+func Anonymous() Principal { return Principal{kind: kindAnonymous} }
46 32
47 // Root returns the principal of the admin token holder.33 // Root returns the principal of the admin token holder.
48-func Root() Principal { return Principal{kind: PrincipalRoot} }34+func Root() Principal { return Principal{kind: kindRoot} }
49 35
50 // TokenPrincipal returns the principal of the holder of a stored token.36 // TokenPrincipal returns the principal of the holder of a stored token.
51 func TokenPrincipal(t Token) Principal {37 func TokenPrincipal(t Token) Principal {
52 return Principal{38 return Principal{
53- kind: PrincipalToken,39+ kind: kindToken,
54 tokenID: t.ID,40 tokenID: t.ID,
55 scopes: slices.Clone(t.Scopes),41 scopes: slices.Clone(t.Scopes),
56 repos: slices.Clone(t.Repos),42 repos: slices.Clone(t.Repos),
@@ -58,14 +44,26 @@ func TokenPrincipal(t Token) Principal {
58 }44 }
59 }45 }
60 46
61-// Kind returns the kind of principal.47+// Label names the kind of principal for logs and messages: "anonymous",
62-func (p Principal) Kind() PrincipalKind { return p.kind }48+// "root" or "token".
49+func (p Principal) Label() string {
50+ switch p.kind {
51+ case kindRoot:
52+ return "root"
53+ case kindToken:
54+ return "token"
55+ }
56+ return "anonymous"
57+}
58+
59+// IsToken reports whether p holds a stored token.
60+func (p Principal) IsToken() bool { return p.kind == kindToken }
63 61
64 // IsRoot reports whether p may do everything.62 // IsRoot reports whether p may do everything.
65-func (p Principal) IsRoot() bool { return p.kind == PrincipalRoot }63+func (p Principal) IsRoot() bool { return p.kind == kindRoot }
66 64
67 // IsAnonymous reports whether p has no credentials.65 // IsAnonymous reports whether p has no credentials.
68-func (p Principal) IsAnonymous() bool { return p.kind == PrincipalAnonymous }66+func (p Principal) IsAnonymous() bool { return p.kind == kindAnonymous }
69 67
70 // TokenID returns the ID of the stored token, or "" for other principals.68 // TokenID returns the ID of the stored token, or "" for other principals.
71 func (p Principal) TokenID() string { return p.tokenID }69 func (p Principal) TokenID() string { return p.tokenID }
@@ -84,9 +82,9 @@ func (p Principal) ExpiresAt() time.Time { return p.expiresAt }
84 // every scope; anonymous none.82 // every scope; anonymous none.
85 func (p Principal) HasScope(s Scope) bool {83 func (p Principal) HasScope(s Scope) bool {
86 switch p.kind {84 switch p.kind {
87- case PrincipalRoot:85+ case kindRoot:
88 return true86 return true
89- case PrincipalToken:87+ case kindToken:
90 return slices.Contains(p.scopes, s)88 return slices.Contains(p.scopes, s)
91 }89 }
92 return false90 return false
@@ -98,7 +96,7 @@ func (p Principal) Allows(s Scope, repo RepoName) bool {
98 if !p.HasScope(s) {96 if !p.HasScope(s) {
99 return false97 return false
100 }98 }
101- if p.kind == PrincipalToken && len(p.repos) > 0 {99+ if p.kind == kindToken && len(p.repos) > 0 {
102 return slices.Contains(p.repos, repo)100 return slices.Contains(p.repos, repo)
103 }101 }
104 return true102 return true
M internal/domain/token_test.go +4 -4
@@ -48,8 +48,8 @@ func TestPrincipalScopes(t *testing.T) {
4848 tok := domain.Token{ID: "t1", Scopes: []domain.Scope{domain.ScopeRepoRead, domain.ScopeRepoWrite}, Repos: []domain.RepoName{a}}
4949 p := domain.TokenPrincipal(tok)
5050
51- assert.Equal(t, domain.PrincipalToken, p.Kind())
52- assert.Equal(t, "token", p.Kind().String())
51+ assert.True(t, p.IsToken())
52+ assert.Equal(t, "token", p.Label())
5353 assert.Equal(t, "t1", p.TokenID())
5454 assert.False(t, p.IsRoot())
5555 assert.False(t, p.IsAnonymous())
@@ -73,8 +73,8 @@ func TestPrincipalScopes(t *testing.T) {
7373 assert.True(t, domain.Root().Allows(domain.ScopeTokenAdmin, a))
7474 assert.False(t, domain.Anonymous().HasScope(domain.ScopeRepoRead))
7575 assert.False(t, domain.Anonymous().Allows(domain.ScopeRepoRead, a))
76- assert.Equal(t, "root", domain.Root().Kind().String())
77- assert.Equal(t, "anonymous", domain.Anonymous().Kind().String())
76+ assert.Equal(t, "root", domain.Root().Label())
77+ assert.Equal(t, "anonymous", domain.Anonymous().Label())
7878 }
7979
8080 func mustName(t *testing.T, s string) domain.RepoName {
@@ -48,8 +48,8 @@ func TestPrincipalScopes(t *testing.T) {
48 tok := domain.Token{ID: "t1", Scopes: []domain.Scope{domain.ScopeRepoRead, domain.ScopeRepoWrite}, Repos: []domain.RepoName{a}}48 tok := domain.Token{ID: "t1", Scopes: []domain.Scope{domain.ScopeRepoRead, domain.ScopeRepoWrite}, Repos: []domain.RepoName{a}}
49 p := domain.TokenPrincipal(tok)49 p := domain.TokenPrincipal(tok)
50 50
51- assert.Equal(t, domain.PrincipalToken, p.Kind())51+ assert.True(t, p.IsToken())
52- assert.Equal(t, "token", p.Kind().String())52+ assert.Equal(t, "token", p.Label())
53 assert.Equal(t, "t1", p.TokenID())53 assert.Equal(t, "t1", p.TokenID())
54 assert.False(t, p.IsRoot())54 assert.False(t, p.IsRoot())
55 assert.False(t, p.IsAnonymous())55 assert.False(t, p.IsAnonymous())
@@ -73,8 +73,8 @@ func TestPrincipalScopes(t *testing.T) {
73 assert.True(t, domain.Root().Allows(domain.ScopeTokenAdmin, a))73 assert.True(t, domain.Root().Allows(domain.ScopeTokenAdmin, a))
74 assert.False(t, domain.Anonymous().HasScope(domain.ScopeRepoRead))74 assert.False(t, domain.Anonymous().HasScope(domain.ScopeRepoRead))
75 assert.False(t, domain.Anonymous().Allows(domain.ScopeRepoRead, a))75 assert.False(t, domain.Anonymous().Allows(domain.ScopeRepoRead, a))
76- assert.Equal(t, "root", domain.Root().Kind().String())76+ assert.Equal(t, "root", domain.Root().Label())
77- assert.Equal(t, "anonymous", domain.Anonymous().Kind().String())77+ assert.Equal(t, "anonymous", domain.Anonymous().Label())
78 }78 }
79 79
80 func mustName(t *testing.T, s string) domain.RepoName {80 func mustName(t *testing.T, s string) domain.RepoName {

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