this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

mod/modregistry: treat "forbidden" as "not found"

When considering what repositories might contain a module, the module
resolution logic is careful to distinguish between a simple "not found"
error (discarded silently) and any other kind of error (triggers an
error in the whole module resolution process).

When a user has authenticated but does not have access to a given
module, the HTTP standard allows the server to return a 403 (Forbidden)
error to indicate that the user doesn't have permission to access a
page.

When considering possible candidates for modules, we don't want to fail
if there are some modules that exist that we can't access, so this CL
changes the logic to treat any 403 error the same as "not found" in this
respect.

We also make the error message when there is a permanent error
more specific so it actually mentions the module that it's trying to
resolve.

We also add tests for this and for token-server-based authentication,
verifying that with the OCI dependency updated as it is in this CL,
issue #2955 is actually fixed. The `registrytest` package API is changed
to allow this, because the previous `AuthHandler` function was not
sufficient to allow an independent token handler server listening on a
different port.

Fixes #2955.

Signed-off-by: Roger Peppe <rogpeppe@gmail.com>
Change-Id: I665b1aa28be255bb2e4a00bfc606b0899575fec1
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1191618
Reviewed-by: Rustam Abdullaev <rustam@cue.works>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>

+423 -45
+5 -4
cmd/cue/cmd/script_test.go
··· 33 33 "time" 34 34 35 35 "cuelabs.dev/go/oci/ociregistry/ocimem" 36 - "cuelabs.dev/go/oci/ociregistry/ociserver" 37 36 "github.com/google/shlex" 38 37 "github.com/rogpeppe/go-internal/goproxytest" 39 38 "github.com/rogpeppe/go-internal/gotooltest" ··· 142 141 usage() 143 142 } 144 143 145 - srv := httptest.NewServer(registrytest.AuthHandler(ociserver.New(ocimem.New(), nil), auth)) 146 - u, _ := url.Parse(srv.URL) 147 - ts.Setenv(args[0], u.Host) 144 + srv, err := registrytest.NewServer(ocimem.New(), auth) 145 + if err != nil { 146 + ts.Fatalf("cannot start registrytest server: %v", err) 147 + } 148 + ts.Setenv(args[0], srv.Host()) 148 149 ts.Defer(srv.Close) 149 150 }, 150 151 // memregistry starts an HTTP server with enough endpoints to test `cue login`.
+47
cmd/cue/cmd/testdata/script/issue2955.txtar
··· 1 + # Test that cue mod tidy works even when a registry 2 + # responds with a 401 error instead of a 403 error. 3 + # This is exactly the same as registry_token_auth_tidy.txtar 4 + # except for the presence of the "always401" field 5 + # in auth.json. 6 + 7 + env DOCKER_CONFIG=$WORK/dockerconfig 8 + env-fill $DOCKER_CONFIG/config.json 9 + env CUE_DEBUG=http 10 + exec cue mod tidy 11 + exec cue export . 12 + cmp stdout expect-stdout 13 + 14 + -- dockerconfig/config.json -- 15 + { 16 + "auths": { 17 + "${DEBUG_REGISTRY_HOST}": { 18 + "identitytoken": "registrytest-refresh" 19 + } 20 + } 21 + } 22 + -- expect-stdout -- 23 + "ok" 24 + -- main.cue -- 25 + package main 26 + import "example.com/e" 27 + 28 + e.foo 29 + 30 + -- cue.mod/module.cue -- 31 + module: "test.org" 32 + -- _registry/auth.json -- 33 + { 34 + "useTokenServer": true, 35 + "acl": { 36 + "allow": ["^example\\.com/e$"] 37 + }, 38 + "always401": true 39 + } 40 + -- _registry/example.com_e_v0.0.1/cue.mod/module.cue -- 41 + module: "example.com/e@v0" 42 + 43 + -- _registry/example.com_e_v0.0.1/main.cue -- 44 + package e 45 + 46 + foo: "ok" 47 +
+1 -1
cmd/cue/cmd/testdata/script/registry_auth.txtar
··· 10 10 env-fill dockerconfig/badpassword.json 11 11 cp dockerconfig/badpassword.json dockerconfig/config.json 12 12 ! exec cue export . 13 - stderr 'import failed: cannot find package "example.com/e": cannot fetch example.com/e@v0.0.1: module example.com/e@v0.0.1: error response: 401 Unauthorized; body: "invalid credentials' 13 + stderr 'import failed: cannot find package "example.com/e": cannot fetch example.com/e@v0.0.1: module example.com/e@v0.0.1: 401 Unauthorized: unauthorized: authentication required: invalid user-password credentials' 14 14 15 15 # Sanity-check that a configured default helper which is not installed 16 16 # is not treated as a fatal error. See https://cuelang.org/issue/2934.
+1 -1
cmd/cue/cmd/testdata/script/registry_auth_logins.txtar
··· 11 11 env-fill cueconfig/badtoken.json 12 12 cp cueconfig/badtoken.json cueconfig/logins.json 13 13 ! exec cue export . 14 - stderr 'import failed: cannot find package .* 401 Unauthorized; body: "invalid credentials' 14 + stderr 'import failed: cannot find package .* 401 Unauthorized: unauthorized: authentication required: invalid bearer credentials' 15 15 16 16 # An invalid logins.json should result in an immediate error. 17 17 env CUE_CONFIG_DIR=$WORK/badconfig
+42
cmd/cue/cmd/testdata/script/registry_token_auth.txtar
··· 1 + # Test that we can authenticate to a registry using 2 + # a token server for authentication. 3 + 4 + env DOCKER_CONFIG=$WORK/dockerconfig 5 + env-fill $DOCKER_CONFIG/config.json 6 + env CUE_DEBUG=http 7 + exec cue export . 8 + cmp stdout expect-stdout 9 + 10 + -- dockerconfig/config.json -- 11 + { 12 + "auths": { 13 + "${DEBUG_REGISTRY_HOST}": { 14 + "identitytoken": "registrytest-refresh" 15 + } 16 + } 17 + } 18 + -- expect-stdout -- 19 + "ok" 20 + -- main.cue -- 21 + package main 22 + import "example.com/e" 23 + 24 + e.foo 25 + 26 + -- cue.mod/module.cue -- 27 + module: "test.org" 28 + deps: "example.com/e": v: "v0.0.1" 29 + -- _registry/auth.json -- 30 + { 31 + "useTokenServer": true 32 + } 33 + -- _registry_prefix -- 34 + somewhere/other 35 + -- _registry/example.com_e_v0.0.1/cue.mod/module.cue -- 36 + module: "example.com/e@v0" 37 + 38 + -- _registry/example.com_e_v0.0.1/main.cue -- 39 + package e 40 + 41 + foo: "ok" 42 +
+44
cmd/cue/cmd/testdata/script/registry_token_auth_tidy.txtar
··· 1 + # Test that we can use cue mod tidy on a registry that 2 + # responds with 403 (forbidden) status codes 3 + # for some registry repositories. 4 + 5 + env DOCKER_CONFIG=$WORK/dockerconfig 6 + env-fill $DOCKER_CONFIG/config.json 7 + env CUE_DEBUG=http 8 + exec cue mod tidy 9 + exec cue export . 10 + cmp stdout expect-stdout 11 + 12 + -- dockerconfig/config.json -- 13 + { 14 + "auths": { 15 + "${DEBUG_REGISTRY_HOST}": { 16 + "identitytoken": "registrytest-refresh" 17 + } 18 + } 19 + } 20 + -- expect-stdout -- 21 + "ok" 22 + -- main.cue -- 23 + package main 24 + import "example.com/e" 25 + 26 + e.foo 27 + 28 + -- cue.mod/module.cue -- 29 + module: "test.org" 30 + -- _registry/auth.json -- 31 + { 32 + "useTokenServer": true, 33 + "acl": { 34 + "allow": ["^example\\.com/e$"] 35 + } 36 + } 37 + -- _registry/example.com_e_v0.0.1/cue.mod/module.cue -- 38 + module: "example.com/e@v0" 39 + 40 + -- _registry/example.com_e_v0.0.1/main.cue -- 41 + package e 42 + 43 + foo: "ok" 44 +
+1 -1
go.mod
··· 3 3 go 1.21 4 4 5 5 require ( 6 - cuelabs.dev/go/oci/ociregistry v0.0.0-20240314152124-224736b49f2e 6 + cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2 7 7 github.com/cockroachdb/apd/v3 v3.2.1 8 8 github.com/emicklei/proto v1.10.0 9 9 github.com/go-quicktest/qt v1.101.0
+2 -2
go.sum
··· 1 - cuelabs.dev/go/oci/ociregistry v0.0.0-20240314152124-224736b49f2e h1:GwCVItFUPxwdsEYnlUcJ6PJxOjTeFFCKOh6QWg4oAzQ= 2 - cuelabs.dev/go/oci/ociregistry v0.0.0-20240314152124-224736b49f2e/go.mod h1:ApHceQLLwcOkCEXM1+DyCXTHEJhNGDpJ2kmV6axsx24= 1 + cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2 h1:BnG6pr9TTr6CYlrJznYUDj6V7xldD1W+1iXPum0wT/w= 2 + cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2/go.mod h1:pK23AUVXuNzzTpfMCA06sxZGeVQ/75FdVtW249de9Uo= 3 3 github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= 4 4 github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= 5 5 github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+1 -1
internal/mod/modload/testdata/tidy/badimportpath.txtar
··· 3 3 -- tidy-check-error -- 4 4 module is not tidy: cannot find module providing package x.com/Foo--bar@v0 5 5 -- want -- 6 - error: failed to resolve "x.com/Foo--bar@v0": cannot obtain versions for module "x.com/Foo--bar@v0": invalid OCI request: invalid repository name 6 + error: failed to resolve "x.com/Foo--bar@v0": cannot obtain versions for module "x.com/Foo--bar@v0": module x.com/Foo--bar@v0: invalid OCI request: name invalid: invalid repository name 7 7 -- cue.mod/module.cue -- 8 8 language: version: "v0.99.99" 9 9 module: "main.org@v0"
+254 -29
internal/registrytest/registry.go
··· 11 11 "net/http" 12 12 "net/http/httptest" 13 13 "net/url" 14 + "regexp" 14 15 "strings" 15 16 16 17 "cuelabs.dev/go/oci/ociregistry" ··· 28 29 ) 29 30 30 31 // AuthConfig specifies authorization requirements for the server. 31 - // Currently it only supports basic and bearer auth. 32 32 type AuthConfig struct { 33 + // Username and Password hold the basic auth credentials. 34 + // If UseTokenServer is true, these apply to the token server 35 + // rather than to the registry itself. 33 36 Username string `json:"username"` 34 37 Password string `json:"password"` 35 38 39 + // BearerToken holds a bearer token to use as auth. 40 + // If UseTokenServer is true, this applies to the token server 41 + // rather than to the registry itself. 36 42 BearerToken string `json:"bearerToken"` 43 + 44 + // UseTokenServer starts a token server and directs client 45 + // requests to acquire auth tokens from that server. 46 + UseTokenServer bool `json:"useTokenServer"` 47 + 48 + // ACL holds the ACL for an authenticated client. 49 + // If it's nil, the user is allowed full access. 50 + // Note: there's only one ACL because we only 51 + // support a single authenticated user. 52 + ACL *ACL `json:"acl,omitempty"` 53 + 54 + // Use401InsteadOf403 causes the server to send a 401 55 + // response even when the credentials are present and correct. 56 + Use401InsteadOf403 bool `json:"always401"` 57 + } 58 + 59 + // ACL determines what endpoints an authenticated user can accesse 60 + // Both Allow and Deny hold a list of regular expressions that 61 + // are matched against an HTTP request formatted as a string: 62 + // 63 + // METHOD URL_PATH 64 + // 65 + // For example: 66 + // 67 + // GET /v2/foo/bar 68 + type ACL struct { 69 + // Allow holds the list of allowed paths for a user. 70 + // If none match, the user is forbidden. 71 + Allow []string 72 + // Deny holds the list of denied paths for a user. 73 + // If any match, the user is forbidden. 74 + Deny []string 37 75 } 38 76 39 77 // Upload uploads the modules found inside fsys (stored ··· 67 105 // 68 106 // If there's a file named auth.json in the root directory, 69 107 // it will cause access to the server to be gated by the 70 - // specified authorization. See the AuthConfig type for 108 + // specified authorization. See the [AuthConfig] type for 71 109 // details. 72 110 // 73 111 // The Registry should be closed after use. 74 112 func New(fsys fs.FS, prefix string) (*Registry, error) { 75 - handler, err := NewHandler(fsys, prefix) 113 + r := ocimem.New() 114 + 115 + authConfigData, err := upload(context.Background(), ocifilter.Sub(r, prefix), fsys) 116 + if err != nil { 117 + return nil, err 118 + } 119 + var authConfig *AuthConfig 120 + if authConfigData != nil { 121 + if err := json.Unmarshal(authConfigData, &authConfig); err != nil { 122 + return nil, fmt.Errorf("invalid auth.json: %v", err) 123 + } 124 + } 125 + return NewServer(ocifilter.ReadOnly(r), authConfig) 126 + } 127 + 128 + // NewServer is like New except that instead of uploading 129 + // the contents of a filesystem, it just serves the contents 130 + // of the given registry guarded by the given auth configuration. 131 + // If auth is nil, no authentication will be required. 132 + func NewServer(r ociregistry.Interface, auth *AuthConfig) (*Registry, error) { 133 + var tokenSrv *httptest.Server 134 + if auth != nil && auth.UseTokenServer { 135 + tokenSrv = httptest.NewServer(tokenHandler(auth)) 136 + } 137 + r, err := authzRegistry(auth, r) 76 138 if err != nil { 77 139 return nil, err 78 140 } 79 - srv := httptest.NewServer(handler) 141 + srv := httptest.NewServer(&registryHandler{ 142 + auth: auth, 143 + registry: ociserver.New(r, nil), 144 + tokenSrv: tokenSrv, 145 + }) 80 146 u, err := url.Parse(srv.URL) 81 147 if err != nil { 82 148 return nil, err 83 149 } 84 150 return &Registry{ 85 - srv: srv, 86 - host: u.Host, 151 + srv: srv, 152 + host: u.Host, 153 + tokenSrv: tokenSrv, 154 + }, nil 155 + } 156 + 157 + // authzRegistry wraps r by checking whether the client has authorization 158 + // to read any given repository. 159 + func authzRegistry(auth *AuthConfig, r ociregistry.Interface) (ociregistry.Interface, error) { 160 + if auth == nil { 161 + return r, nil 162 + } 163 + allow := func(repoName string) bool { 164 + return true 165 + } 166 + if auth.ACL != nil { 167 + allowCheck, err := regexpMatcher(auth.ACL.Allow) 168 + if err != nil { 169 + return nil, fmt.Errorf("invalid allow list: %v", err) 170 + } 171 + denyCheck, err := regexpMatcher(auth.ACL.Deny) 172 + if err != nil { 173 + return nil, fmt.Errorf("invalid deny list: %v", err) 174 + } 175 + allow = func(repoName string) bool { 176 + return allowCheck(repoName) && !denyCheck(repoName) 177 + } 178 + } 179 + return ocifilter.AccessChecker(r, func(repoName string, access ocifilter.AccessKind) (_err error) { 180 + if !allow(repoName) { 181 + if auth.Use401InsteadOf403 { 182 + // TODO this response should be associated with a 183 + // Www-Authenticate header, but this won't do that. 184 + // Given that the ociauth logic _should_ turn 185 + // this back into a 403 error again, perhaps 186 + // we're OK. 187 + return ociregistry.ErrUnauthorized 188 + } 189 + // TODO should we be a bit more sophisticated and only 190 + // return ErrDenied when the repository doesn't exist? 191 + return ociregistry.ErrDenied 192 + } 193 + return nil 194 + }), nil 195 + } 196 + 197 + func regexpMatcher(patStrs []string) (func(string) bool, error) { 198 + pats := make([]*regexp.Regexp, len(patStrs)) 199 + for i, s := range patStrs { 200 + pat, err := regexp.Compile(s) 201 + if err != nil { 202 + return nil, fmt.Errorf("invalid regexp in ACL: %v", err) 203 + } 204 + pats[i] = pat 205 + } 206 + return func(name string) bool { 207 + for _, pat := range pats { 208 + if pat.MatchString(name) { 209 + return true 210 + } 211 + } 212 + return false 87 213 }, nil 88 214 } 89 215 90 - // NewHandler is similar to [New] except that it just returns 91 - // the HTTP handler for the server instead of actually starting 92 - // a server. 93 - func NewHandler(fsys fs.FS, prefix string) (http.Handler, error) { 94 - r := ocimem.New() 216 + type registryHandler struct { 217 + auth *AuthConfig 218 + registry http.Handler 219 + tokenSrv *httptest.Server 220 + } 221 + 222 + const ( 223 + registryAuthToken = "ok-token-for-registrytest" 224 + registryUnauthToken = "unauth-token-for-registrytest" 225 + ) 226 + 227 + func (h *registryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 228 + if h.auth == nil { 229 + h.registry.ServeHTTP(w, req) 230 + return 231 + } 232 + if h.tokenSrv == nil { 233 + h.serveDirectAuth(w, req) 234 + return 235 + } 236 + 237 + // Auth with token server. 238 + wwwAuth := fmt.Sprintf("Bearer realm=%q,service=registrytest", h.tokenSrv.URL) 239 + authHeader := req.Header.Get("Authorization") 240 + if authHeader == "" { 241 + w.Header().Set("Www-Authenticate", wwwAuth) 242 + writeError(w, ociregistry.ErrUnauthorized) 243 + return 244 + } 245 + kind, token, ok := strings.Cut(authHeader, " ") 246 + if !ok || kind != "Bearer" { 247 + w.Header().Set("Www-Authenticate", wwwAuth) 248 + writeError(w, ociregistry.ErrUnauthorized) 249 + return 250 + } 251 + switch token { 252 + case registryAuthToken: 253 + // User is authorized. 254 + case registryUnauthToken: 255 + writeError(w, ociregistry.ErrDenied) 256 + return 257 + default: 258 + // If we don't recognize the token, then presumably 259 + // the client isn't authenticated so it's 401 not 403. 260 + w.Header().Set("Www-Authenticate", wwwAuth) 261 + writeError(w, ociregistry.ErrUnauthorized) 262 + return 263 + } 264 + h.registry.ServeHTTP(w, req) 265 + } 95 266 96 - authConfigData, err := upload(context.Background(), ocifilter.Sub(r, prefix), fsys) 97 - if err != nil { 98 - return nil, err 267 + func (h *registryHandler) serveDirectAuth(w http.ResponseWriter, req *http.Request) { 268 + auth := req.Header.Get("Authorization") 269 + if auth == "" { 270 + if h.auth.BearerToken != "" { 271 + // Note that this lacks information like the realm, 272 + // but we don't need it for our test cases yet. 273 + w.Header().Set("Www-Authenticate", "Bearer service=registry") 274 + } else { 275 + w.Header().Set("Www-Authenticate", "Basic service=registry") 276 + } 277 + writeError(w, fmt.Errorf("%w: no credentials", ociregistry.ErrUnauthorized)) 278 + return 99 279 } 100 - var handler http.Handler = ociserver.New(ocifilter.ReadOnly(r), nil) 101 - if authConfigData != nil { 102 - var cfg AuthConfig 103 - if err := json.Unmarshal(authConfigData, &cfg); err != nil { 104 - return nil, fmt.Errorf("invalid auth.json: %v", err) 280 + if h.auth.BearerToken != "" { 281 + token, ok := strings.CutPrefix(auth, "Bearer ") 282 + if !ok || token != h.auth.BearerToken { 283 + writeError(w, fmt.Errorf("%w: invalid bearer credentials", ociregistry.ErrUnauthorized)) 284 + return 105 285 } 106 - handler = AuthHandler(handler, &cfg) 286 + } else { 287 + username, password, ok := req.BasicAuth() 288 + if !ok || username != h.auth.Username || password != h.auth.Password { 289 + writeError(w, fmt.Errorf("%w: invalid user-password credentials", ociregistry.ErrUnauthorized)) 290 + return 291 + } 107 292 } 108 - return handler, nil 293 + h.registry.ServeHTTP(w, req) 109 294 } 110 295 111 - // AuthHandler wraps the given handler with logic that checks 112 - // that the incoming requests fulfil the auth requirements defined 296 + func tokenHandler(*AuthConfig) http.Handler { 297 + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 298 + if req.Method != "POST" { 299 + http.Error(w, "only POST supported", http.StatusMethodNotAllowed) 300 + return 301 + } 302 + req.ParseForm() 303 + if req.Form.Get("service") != "registrytest" { 304 + http.Error(w, "invalid service", http.StatusBadRequest) 305 + return 306 + } 307 + if req.Form.Get("grant_type") != "refresh_token" { 308 + http.Error(w, "invalid grant type", http.StatusBadRequest) 309 + return 310 + } 311 + refreshToken := req.Form.Get("refresh_token") 312 + if refreshToken != "registrytest-refresh" { 313 + http.Error(w, fmt.Sprintf("invalid refresh token %q", refreshToken), http.StatusForbidden) 314 + return 315 + } 316 + // See ociauth.wireToken for the full JSON format. 317 + data, _ := json.Marshal(map[string]string{ 318 + "token": registryAuthToken, 319 + }) 320 + w.Header().Set("Content-Type", "application/json") 321 + w.Write(data) 322 + }) 323 + } 324 + 325 + // authnHandler wraps the given handler with logic that checks 326 + // that the incoming requests fulfil the authenticiation requirements defined 113 327 // in cfg. If cfg is nil or there are no auth requirements, it returns handler 114 328 // unchanged. 115 - func AuthHandler(handler http.Handler, cfg *AuthConfig) http.Handler { 329 + func authnHandler(cfg *AuthConfig, handler http.Handler) http.Handler { 116 330 if cfg == nil || (*cfg == AuthConfig{}) { 117 331 return handler 118 332 } ··· 126 340 } else { 127 341 w.Header().Set("Www-Authenticate", "Basic service=registry") 128 342 } 129 - http.Error(w, "no credentials", http.StatusUnauthorized) 343 + writeError(w, fmt.Errorf("%w: no credentials", ociregistry.ErrUnauthorized)) 130 344 return 131 345 } 132 346 if cfg.BearerToken != "" { 133 347 token, ok := strings.CutPrefix(auth, "Bearer ") 134 348 if !ok || token != cfg.BearerToken { 135 - http.Error(w, "invalid credentials", http.StatusUnauthorized) 349 + writeError(w, fmt.Errorf("%w: invalid bearer credentials", ociregistry.ErrUnauthorized)) 136 350 return 137 351 } 138 352 } else { 139 353 username, password, ok := req.BasicAuth() 140 354 if !ok || username != cfg.Username || password != cfg.Password { 141 - http.Error(w, "invalid credentials", http.StatusUnauthorized) 355 + writeError(w, fmt.Errorf("%w: invalid user-password credentials", ociregistry.ErrUnauthorized)) 142 356 return 143 357 } 144 358 } 145 359 handler.ServeHTTP(w, req) 146 360 }) 361 + } 362 + 363 + func writeError(w http.ResponseWriter, err error) { 364 + data, httpStatus := ociregistry.MarshalError(err) 365 + w.Header().Set("Content-Type", "application/json") 366 + w.WriteHeader(httpStatus) 367 + w.Write(data) 147 368 } 148 369 149 370 func pushContent(ctx context.Context, client *modregistry.Client, mods map[module.Version]*moduleContent) error { ··· 191 412 } 192 413 193 414 type Registry struct { 194 - srv *httptest.Server 195 - host string 415 + srv *httptest.Server 416 + tokenSrv *httptest.Server 417 + host string 196 418 } 197 419 198 420 func (r *Registry) Close() { 199 421 r.srv.Close() 422 + if r.tokenSrv != nil { 423 + r.tokenSrv.Close() 424 + } 200 425 } 201 426 202 427 // Host returns the hostname for the registry server;
+9 -3
mod/modconfig/modconfig_test.go
··· 17 17 "testing" 18 18 "time" 19 19 20 + "cuelabs.dev/go/oci/ociregistry/ocimem" 21 + "cuelabs.dev/go/oci/ociregistry/ociserver" 20 22 "github.com/go-quicktest/qt" 21 23 "golang.org/x/sync/errgroup" 22 24 "golang.org/x/tools/txtar" ··· 137 139 -- bar.example_v0.0.1/x/x.cue -- 138 140 package x 139 141 `)) 140 - rh, err := registrytest.NewHandler(txtarfs.FS(modules), "") 142 + ctx := context.Background() 143 + rmem := ocimem.New() 144 + err := registrytest.Upload(ctx, rmem, txtarfs.FS(modules)) 141 145 qt.Assert(t, qt.IsNil(err)) 146 + rh := ociserver.New(rmem, nil) 142 147 agent := cueversion.UserAgent("cuelang.org/go") 143 148 checked := false 144 149 checkUserAgentHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ··· 161 166 162 167 r, err := NewRegistry(nil) 163 168 qt.Assert(t, qt.IsNil(err)) 164 - ctx := context.Background() 165 169 gotRequirements, err := r.Requirements(ctx, module.MustNewVersion("bar.example@v0", "v0.0.1")) 166 170 qt.Assert(t, qt.IsNil(err)) 167 171 qt.Assert(t, qt.HasLen(gotRequirements, 0)) ··· 191 195 package bar 192 196 `, reg.mod, reg.mod, reg.mod)))) 193 197 mux := http.NewServeMux() 194 - rh, err := registrytest.NewHandler(fsys, "") 198 + r := ocimem.New() 199 + err := registrytest.Upload(context.Background(), r, fsys) 195 200 qt.Assert(t, qt.IsNil(err)) 201 + rh := ociserver.New(r, nil) 196 202 mux.HandleFunc("/v2/", func(w http.ResponseWriter, r *http.Request) { 197 203 auth := r.Header.Get("Authorization") 198 204 if !strings.HasPrefix(auth, fmt.Sprintf("Bearer access_%d_", i)) {
+16 -3
mod/modregistry/client.go
··· 27 27 "errors" 28 28 "fmt" 29 29 "io" 30 + "net/http" 30 31 "strings" 31 32 32 33 "cuelabs.dev/go/oci/ociregistry" ··· 107 108 } 108 109 rd, err := loc.Registry.GetTag(ctx, loc.Repository, loc.Tag) 109 110 if err != nil { 111 + // TODO should we use isNotExist here too? 110 112 if errors.Is(err, ociregistry.ErrManifestUnknown) { 111 113 return nil, fmt.Errorf("module %v: %w", m, ErrNotFound) 112 114 } 113 - return nil, fmt.Errorf("module %v: %v", m, err) 115 + return nil, fmt.Errorf("module %v: %w", m, err) 114 116 } 115 117 defer rd.Close() 116 118 data, err := io.ReadAll(rd) ··· 188 190 return true 189 191 }) 190 192 if _err != nil && !isNotExist(_err) { 191 - return nil, _err 193 + return nil, fmt.Errorf("module %v: %w", m, _err) 192 194 } 193 195 semver.Sort(versions) 194 196 return versions, nil ··· 403 405 } 404 406 405 407 func isNotExist(err error) bool { 406 - return errors.Is(err, ociregistry.ErrNameUnknown) || errors.Is(err, ociregistry.ErrNameInvalid) 408 + if errors.Is(err, ociregistry.ErrNameUnknown) || 409 + errors.Is(err, ociregistry.ErrNameInvalid) { 410 + return true 411 + } 412 + // A 403 error might have been sent as a response 413 + // without explicitly including a "denied" error code. 414 + // We treat this as a "not found" error because there's 415 + // nothing the user can do about it. 416 + if herr := ociregistry.HTTPError(nil); errors.As(err, &herr) { 417 + return herr.StatusCode() == http.StatusForbidden 418 + } 419 + return false 407 420 } 408 421 409 422 func isModule(m *ocispec.Manifest) bool {