detached e4d9ea0

fix: address CodeRabbit review — internal auth scope restriction, IPv6 redirect, TTL cap, docs

commit
e4d9ea04729634c774811d4dced2f12a632b2d1e
author
brice · · 2026-03-22 18:30 UTC
parent
c8a82f0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +14 -5
M docs/upgrading.md +2 -2
@@ -26,10 +26,10 @@ Gordon v2.30.0 removes password-based authentication entirely. Only token-based
2626 1. **Before upgrading**, generate a token on your current Gordon instance:
2727
2828 ```bash
29- gordon auth token generate --subject deploy --scopes "push,pull,admin:*:*" --expiry 0
29+ gordon auth token generate --subject deploy --scopes "push,pull" --expiry 0
3030 ```
3131
32- Save this token securely. You will need it after upgrading.
32+ Save this token securely. You will need it after upgrading. Admin scopes (`admin:*:*`) are only available after upgrading to v2.30.0 — regenerate your token with admin scopes after the upgrade if needed.
3333
3434 2. **Update remotes** to use the generated token:
3535
@@ -26,10 +26,10 @@ Gordon v2.30.0 removes password-based authentication entirely. Only token-based
26 1. **Before upgrading**, generate a token on your current Gordon instance:26 1. **Before upgrading**, generate a token on your current Gordon instance:
27 27
28 ```bash28 ```bash
29- gordon auth token generate --subject deploy --scopes "push,pull,admin:*:*" --expiry 029+ gordon auth token generate --subject deploy --scopes "push,pull" --expiry 0
30 ```30 ```
31 31
32- Save this token securely. You will need it after upgrading.32+ Save this token securely. You will need it after upgrading. Admin scopes (`admin:*:*`) are only available after upgrading to v2.30.0 — regenerate your token with admin scopes after the upgrade if needed.
33 33
34 2. **Update remotes** to use the generated token:34 2. **Update remotes** to use the generated token:
35 35
M internal/adapters/in/http/auth/handler.go +4 -1
@@ -168,7 +168,10 @@ func (h *Handler) handleToken(w http.ResponseWriter, r *http.Request) {
168168 func (h *Handler) authenticateTokenCredentials(ctx context.Context, r *http.Request, username, password string, log zerowrap.Logger) (bool, *domain.TokenClaims) {
169169 if httputil.IsLocalhostRequest(r) && h.isInternalAuth(username, password) {
170170 log.Debug().Str("username", username).Msg("internal registry auth accepted")
171- return true, nil
171+ return true, &domain.TokenClaims{
172+ Subject: username,
173+ Scopes: []string{"push", "pull"},
174+ }
172175 }
173176
174177 // Validate JWT token sent via Basic Auth password field.
@@ -168,7 +168,10 @@ func (h *Handler) handleToken(w http.ResponseWriter, r *http.Request) {
168 func (h *Handler) authenticateTokenCredentials(ctx context.Context, r *http.Request, username, password string, log zerowrap.Logger) (bool, *domain.TokenClaims) {168 func (h *Handler) authenticateTokenCredentials(ctx context.Context, r *http.Request, username, password string, log zerowrap.Logger) (bool, *domain.TokenClaims) {
169 if httputil.IsLocalhostRequest(r) && h.isInternalAuth(username, password) {169 if httputil.IsLocalhostRequest(r) && h.isInternalAuth(username, password) {
170 log.Debug().Str("username", username).Msg("internal registry auth accepted")170 log.Debug().Str("username", username).Msg("internal registry auth accepted")
171- return true, nil171+ return true, &domain.TokenClaims{
172+ Subject: username,
173+ Scopes: []string{"push", "pull"},
174+ }
172 }175 }
173 176
174 // Validate JWT token sent via Basic Auth password field.177 // Validate JWT token sent via Basic Auth password field.
M internal/app/run.go +6 -0
@@ -1098,6 +1098,9 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType,
10981098 if parsed <= 0 {
10991099 return auth.Config{}, fmt.Errorf("auth.access_token_ttl must be positive")
11001100 }
1101+ if parsed > auth.MaxAccessTokenLifetime {
1102+ return auth.Config{}, fmt.Errorf("auth.access_token_ttl must not exceed %v", auth.MaxAccessTokenLifetime)
1103+ }
11011104 accessTokenTTL = parsed
11021105 }
11031106 authConfig.AccessTokenTTL = accessTokenTTL
@@ -1812,6 +1815,9 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
18121815 }
18131816 if tlsPort != 443 {
18141817 host = net.JoinHostPort(host, strconv.Itoa(tlsPort))
1818+ } else if strings.Contains(host, ":") {
1819+ // Re-bracket bare IPv6 literal for the URL.
1820+ host = "[" + host + "]"
18151821 }
18161822 target := "https://" + host + r.RequestURI
18171823 http.Redirect(w, r, target, http.StatusPermanentRedirect)
@@ -1098,6 +1098,9 @@ func buildAuthConfig(ctx context.Context, cfg Config, authType domain.AuthType,
1098 if parsed <= 0 {1098 if parsed <= 0 {
1099 return auth.Config{}, fmt.Errorf("auth.access_token_ttl must be positive")1099 return auth.Config{}, fmt.Errorf("auth.access_token_ttl must be positive")
1100 }1100 }
1101+ if parsed > auth.MaxAccessTokenLifetime {
1102+ return auth.Config{}, fmt.Errorf("auth.access_token_ttl must not exceed %v", auth.MaxAccessTokenLifetime)
1103+ }
1101 accessTokenTTL = parsed1104 accessTokenTTL = parsed
1102 }1105 }
1103 authConfig.AccessTokenTTL = accessTokenTTL1106 authConfig.AccessTokenTTL = accessTokenTTL
@@ -1812,6 +1815,9 @@ func runServers(ctx context.Context, v *viper.Viper, cfg Config, registryHandler
1812 }1815 }
1813 if tlsPort != 443 {1816 if tlsPort != 443 {
1814 host = net.JoinHostPort(host, strconv.Itoa(tlsPort))1817 host = net.JoinHostPort(host, strconv.Itoa(tlsPort))
1818+ } else if strings.Contains(host, ":") {
1819+ // Re-bracket bare IPv6 literal for the URL.
1820+ host = "[" + host + "]"
1815 }1821 }
1816 target := "https://" + host + r.RequestURI1822 target := "https://" + host + r.RequestURI
1817 http.Redirect(w, r, target, http.StatusPermanentRedirect)1823 http.Redirect(w, r, target, http.StatusPermanentRedirect)
M internal/usecase/auth/service_test.go +2 -2
@@ -743,7 +743,7 @@ func TestExtendTokenSlidesExpiry(t *testing.T) {
743743
744744 ctx := testContext()
745745
746- // Generate a token with a short remaining life (2h) so ExtendToken produces a different exp
746+ // Generate a token with a short remaining life (MaxAccessTokenLifetime+1m) so ExtendToken produces a different exp
747747 var capturedToken *domain.Token
748748 tokenStore.EXPECT().
749749 SaveToken(mock.Anything, mock.Anything, mock.Anything).
@@ -815,7 +815,7 @@ func TestExtendTokenDebounce(t *testing.T) {
815815
816816 ctx := testContext()
817817
818- // Generate a short-lived token (2h) so the first extension produces a different JWT
818+ // Generate a short-lived token (MaxAccessTokenLifetime+1m) so the first extension produces a different JWT
819819 var capturedToken *domain.Token
820820 tokenStore.EXPECT().
821821 SaveToken(mock.Anything, mock.Anything, mock.Anything).
@@ -743,7 +743,7 @@ func TestExtendTokenSlidesExpiry(t *testing.T) {
743 743
744 ctx := testContext()744 ctx := testContext()
745 745
746- // Generate a token with a short remaining life (2h) so ExtendToken produces a different exp746+ // Generate a token with a short remaining life (MaxAccessTokenLifetime+1m) so ExtendToken produces a different exp
747 var capturedToken *domain.Token747 var capturedToken *domain.Token
748 tokenStore.EXPECT().748 tokenStore.EXPECT().
749 SaveToken(mock.Anything, mock.Anything, mock.Anything).749 SaveToken(mock.Anything, mock.Anything, mock.Anything).
@@ -815,7 +815,7 @@ func TestExtendTokenDebounce(t *testing.T) {
815 815
816 ctx := testContext()816 ctx := testContext()
817 817
818- // Generate a short-lived token (2h) so the first extension produces a different JWT818+ // Generate a short-lived token (MaxAccessTokenLifetime+1m) so the first extension produces a different JWT
819 var capturedToken *domain.Token819 var capturedToken *domain.Token
820 tokenStore.EXPECT().820 tokenStore.EXPECT().
821 SaveToken(mock.Anything, mock.Anything, mock.Anything).821 SaveToken(mock.Anything, mock.Anything, mock.Anything).

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 7

Tags 106

↑↓ moveenter openesc close