Medium severity6.5NVD Advisory· Published Apr 3, 2026· Updated Apr 21, 2026
CVE-2025-68153
CVE-2025-68153
Description
Juju is an open source application orchestration engine that enables any application operation on any infrastructure at any scale through special operators called ‘charms’. From versions 2.9 to before 2.9.56 and 3.6 to before 3.6.19, any authenticated user, machine or controller under a Juju controller can modify the resources of an application within the entire controller. This issue has been patched in versions 2.9.56 and 3.6.19.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/juju/jujuGo | < 0.0.0-20260120044552-26ff93c903d5 | 0.0.0-20260120044552-26ff93c903d5 |
Affected products
1Patches
126ff93c903d5fix: only allow users with write permission to upload resources
9 files changed · +346 −59
api/client/resources/client.go+16 −2 modified@@ -5,12 +5,14 @@ package resources import ( "io" + "slices" "strings" "github.com/juju/charm/v8" charmresource "github.com/juju/charm/v8/resource" "github.com/juju/errors" "github.com/juju/names/v4" + "gopkg.in/errgo.v1" "gopkg.in/macaroon.v2" "github.com/juju/juju/api/base" @@ -102,6 +104,18 @@ func newListResourcesArgs(applications []string) (params.ListResourcesArgs, erro return args, nil } +func translateUploadError(err error) error { + permissionCodes := []string{params.CodeForbidden, params.CodeUnauthorized} + var uploadErr *errgo.Err + if errors.As(err, &uploadErr) { + if slices.Contains(permissionCodes, params.ErrCode(uploadErr.Cause())) || + slices.Contains(permissionCodes, params.ErrCode(uploadErr.Underlying())) { + return apiservererrors.ErrPerm + } + } + return err +} + // Upload sends the provided resource blob up to Juju. func (c Client) Upload(application, name, filename string, reader io.ReadSeeker) error { uReq, err := NewUploadRequest(application, name, filename, reader) @@ -115,7 +129,7 @@ func (c Client) Upload(application, name, filename string, reader io.ReadSeeker) var response params.UploadResult // ignored if err := c.httpClient.Do(c.facade.RawAPICaller().Context(), req, &response); err != nil { - return errors.Trace(err) + return errors.Trace(translateUploadError(err)) } return nil @@ -287,7 +301,7 @@ func (c Client) UploadPendingResource(application string, res charmresource.Reso var response params.UploadResult // ignored if err := c.httpClient.Do(c.facade.RawAPICaller().Context(), req, &response); err != nil { - return "", errors.Trace(err) + return "", translateUploadError(errors.Trace(err)) } }
api/client/resources/client_upload_test.go+31 −6 modified@@ -19,6 +19,7 @@ import ( "github.com/kr/pretty" "go.uber.org/mock/gomock" gc "gopkg.in/check.v1" + "gopkg.in/errgo.v1" "github.com/juju/juju/api/client/resources" apicharm "github.com/juju/juju/api/common/charm" @@ -93,7 +94,7 @@ func (s *UploadSuite) TestUploadBadApplication(c *gc.C) { c.Check(err, gc.ErrorMatches, `.*invalid application.*`) } -func (s *UploadSuite) TestUploadFailed(c *gc.C) { +func (s *UploadSuite) assertUploadFailed(c *gc.C, uploadErr error, expectedMsg string) { defer s.setUpMocks(c).Finish() ctx := context.TODO() @@ -110,10 +111,22 @@ func (s *UploadSuite) TestUploadFailed(c *gc.C) { req.Header.Set("Content-Disposition", "form-data; filename=foo.zip") req.ContentLength = int64(len(data)) - s.httpClient.EXPECT().Do(ctx, reqMatcher{c, req}, gomock.Any()).Return(errors.New("boom")) + s.httpClient.EXPECT().Do(ctx, reqMatcher{c, req}, gomock.Any()).Return(uploadErr) err = s.client.Upload("a-application", "spam", "foo.zip", strings.NewReader(data)) - c.Assert(err, gc.ErrorMatches, "boom") + c.Assert(err, gc.ErrorMatches, expectedMsg) +} + +func (s *UploadSuite) TestUploadFailed(c *gc.C) { + s.assertUploadFailed(c, errors.New("upload failed"), "upload failed") +} + +func (s *UploadSuite) TestUploadAuthError(c *gc.C) { + authErr := errgo.Mask(params.Error{ + Code: params.CodeUnauthorized, + Message: "user unauthorized", + }) + s.assertUploadFailed(c, authErr, "permission denied") } func (s *UploadSuite) TestAddPendingResources(c *gc.C) { @@ -228,7 +241,7 @@ func (s *UploadSuite) TestUploadPendingResourceBadApplication(c *gc.C) { c.Assert(err, gc.ErrorMatches, `.*invalid application.*`) } -func (s *UploadSuite) TestUploadPendingResourceFailed(c *gc.C) { +func (s *UploadSuite) assertUploadPendingResourceFailed(c *gc.C, uploadErr error, expectedMsg string) { defer s.setUpMocks(c).Finish() res, apiResult := newResourceResult(c, "spam") @@ -259,8 +272,20 @@ func (s *UploadSuite) TestUploadPendingResourceFailed(c *gc.C) { req.URL.RawQuery = "pendingid=" + expected req.Header.Set("Content-Disposition", "form-data; filename=file.zip") - s.httpClient.EXPECT().Do(ctx, reqMatcher{c, req}, gomock.Any()).Return(errors.New("boom")) + s.httpClient.EXPECT().Do(ctx, reqMatcher{c, req}, gomock.Any()).Return(uploadErr) _, err = s.client.UploadPendingResource("a-application", res[0].Resource, "file.zip", strings.NewReader(data)) - c.Assert(err, gc.ErrorMatches, "boom") + c.Assert(err, gc.ErrorMatches, expectedMsg) +} + +func (s *UploadSuite) TestUploadPendingResourceFailed(c *gc.C) { + s.assertUploadPendingResourceFailed(c, errors.New("upload failed"), "upload failed") +} + +func (s *UploadSuite) TestUploadPendingResourceAuthError(c *gc.C) { + authErr := errgo.Mask(params.Error{ + Code: params.CodeUnauthorized, + Message: "user unauthorized", + }) + s.assertUploadPendingResourceFailed(c, authErr, "permission denied") }
api/httpclient.go+29 −0 modified@@ -8,8 +8,10 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io" "net/http" "net/url" + "strings" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/go-macaroon-bakery/macaroon-bakery/v3/httpbakery" @@ -136,6 +138,33 @@ func encodeMacaroonSlice(ms macaroon.Slice) (string, error) { func unmarshalHTTPErrorResponse(resp *http.Response) error { var body json.RawMessage if err := httprequest.UnmarshalJSONResponse(resp, &body); err != nil { + // Auth errors can come back as plain text as the http handler + // authentication function does not write its error response as json. + // We want to return a suitable error code so the caller can + // handle that case. We don't want to propagate the message + // "unexpected content type text/plain; want application/json". + var decodeErr *httprequest.DecodeResponseError + if errors.As(err, &decodeErr) { + var code string + switch resp.StatusCode { + case http.StatusForbidden: + code = params.CodeForbidden + case http.StatusUnauthorized: + code = params.CodeUnauthorized + } + var errMsg string + msg, readErr := io.ReadAll(decodeErr.Response.Body) + if readErr == nil { + errMsg = strings.Trim(string(msg), "\n") + } else { + // Should never happen. + errMsg = err.Error() + } + return params.Error{ + Message: errMsg, + Code: code, + } + } return errors.Trace(err) } // genericErrorResponse defines a struct that is compatible with all the
api/httpclient_test.go+8 −1 modified@@ -66,7 +66,14 @@ var httpClientTests = []struct { }, { about: "non-JSON error response", handler: http.NotFound, - expectError: `Get http://.*/: unexpected content type text/plain; want application/json; content: 404 page not found`, + expectError: `Get http://.*/: 404 page not found`, +}, { + about: "non-JSON auth error response", + handler: func(w http.ResponseWriter, req *http.Request) { + http.Error(w, "some unauth error", http.StatusUnauthorized) + }, + expectError: `Get http://.*/: some unauth error`, + expectErrorCode: "unauthorized access", }, { about: "bad error response", handler: func(w http.ResponseWriter, req *http.Request) {
apiserver/apiserver.go+29 −4 modified@@ -762,9 +762,18 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) { }, } modelToolsDownloadHandler := newToolsDownloadHandler(httpCtxt) - resourcesHandler := &ResourcesHandler{ - StateAuthFunc: func(req *http.Request, tagKinds ...string) (ResourcesBackend, state.PoolHelper, names.Tag, error) { - st, entity, err := httpCtxt.stateForRequestAuthenticatedTag(req, tagKinds...) + var resourcesUploadAuthorizer httpcontext.CompositeAuthorizer = []httpcontext.Authorizer{ + controllerAdminAuthorizer{ + st: systemState, + }, + modelPermissionAuthorizer{ + userAccess: systemState.UserPermission, + perm: permission.WriteAccess, + }, + } + resourceUploadHandler := &ResourcesUploadHandler{ + StateFunc: func(req *http.Request) (ResourcesBackend, state.PoolHelper, names.Tag, error) { + st, entity, err := httpCtxt.stateForRequestAuthenticated(req) if err != nil { return nil, nil, nil, errors.Trace(err) } @@ -783,6 +792,16 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) { return nil }, } + resourceDownloadHandler := &ResourcesDownloadHandler{ + StateAuthFunc: func(req *http.Request, tagKinds ...string) (ResourcesBackend, state.PoolHelper, error) { + st, _, err := httpCtxt.stateForRequestAuthenticatedTag(req, tagKinds...) + if err != nil { + return nil, nil, errors.Trace(err) + } + rst := st.Resources() + return rst, st, nil + }, + } unitResourcesHandler := &UnitResourcesHandler{ NewOpener: func(req *http.Request, tagKinds ...string) (resources.Opener, state.PoolHelper, error) { st, _, err := httpCtxt.stateForRequestAuthenticatedTag(req, tagKinds...) @@ -887,7 +906,13 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) { unauthenticated: true, }, { pattern: modelRoutePrefix + "/applications/:application/resources/:resource", - handler: resourcesHandler, + methods: []string{"GET"}, + handler: resourceDownloadHandler, + }, { + pattern: modelRoutePrefix + "/applications/:application/resources/:resource", + methods: []string{"PUT"}, + handler: resourceUploadHandler, + authorizer: resourcesUploadAuthorizer, }, { pattern: modelRoutePrefix + "/units/:unit/resources/:resource", handler: unitResourcesHandler,
apiserver/resources_auth_test.go+139 −0 added@@ -0,0 +1,139 @@ +// Copyright 2025 Canonical Ltd. +// Licensed under the AGPLv3, see LICENCE file for details. + +package apiserver_test + +import ( + "encoding/json" + "fmt" + "mime" + "net/http" + "net/url" + "strings" + + jc "github.com/juju/testing/checkers" + "github.com/juju/utils/v3" + gc "gopkg.in/check.v1" + + apitesting "github.com/juju/juju/apiserver/testing" + "github.com/juju/juju/core/permission" + "github.com/juju/juju/core/resources" + "github.com/juju/juju/rpc/params" + "github.com/juju/juju/state" + "github.com/juju/juju/testing/factory" +) + +type resourcesAuthSuite struct { + apiserverBaseSuite +} + +func (s *resourcesAuthSuite) resourcesURL(app, res string) *url.URL { + u := s.URL(fmt.Sprintf("/model/%s/applications/%s/resources/%s", s.Model.UUID(), app, res), nil) + return u +} + +func (s *resourcesAuthSuite) assertJSONErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) { + uploadResponse := s.assertResponse(c, resp, expCode) + c.Check(uploadResponse.Error, gc.NotNil) + c.Check(uploadResponse.Error.Message, gc.Matches, expError) +} + +func (s *resourcesAuthSuite) assertPlainErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) { + body := apitesting.AssertResponse(c, resp, expCode, "text/plain; charset=utf-8") + c.Assert(string(body), gc.Matches, expError+"\n") +} + +func (s *resourcesAuthSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.UploadResult { + body := apitesting.AssertResponse(c, resp, expStatus, params.ContentTypeJSON) + var uploadResult params.UploadResult + err := json.Unmarshal(body, &uploadResult) + c.Assert(err, jc.ErrorIsNil, gc.Commentf("Body: %s", body)) + return uploadResult +} + +var _ = gc.Suite(&resourcesAuthSuite{}) + +func (s *resourcesAuthSuite) TestResourcesUploadedSecurely(c *gc.C) { + url := s.resourcesURL("tomcat", "jdk") + url.Scheme = "http" + resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ + Method: "PUT", + URL: url.String(), + ExpectStatus: http.StatusBadRequest, + }) + defer resp.Body.Close() +} + +func (s *resourcesAuthSuite) TestRequiresAuth(c *gc.C) { + resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: s.resourcesURL("tomcat", "jdk").String()}) + defer resp.Body.Close() + s.assertPlainErrorResponse(c, resp, http.StatusUnauthorized, "authentication failed: no credentials provided") +} + +func (s *resourcesAuthSuite) TestAuthRejectsNonsUser(c *gc.C) { + // Add a machine and try to login. + machine, err := s.State.AddMachine("quantal", state.JobHostUnits) + c.Assert(err, jc.ErrorIsNil) + err = machine.SetProvisioned("foo", "", "fake_nonce", nil) + c.Assert(err, jc.ErrorIsNil) + password, err := utils.RandomPassword() + c.Assert(err, jc.ErrorIsNil) + err = machine.SetPassword(password) + c.Assert(err, jc.ErrorIsNil) + + resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ + Tag: machine.Tag().String(), + Password: password, + Method: "PUT", + URL: s.resourcesURL("tomcat", "jdk").String(), + Nonce: "fake_nonce", + }) + s.assertPlainErrorResponse( + c, resp, http.StatusForbidden, + "authorization failed: permission denied", + ) + resp.Body.Close() + + // Now try a user login. + content, err := resources.GenerateContent(strings.NewReader("resource")) + c.Assert(err, jc.ErrorIsNil) + filename := mime.BEncoding.Encode("utf-8", "foo.txt") + disp := mime.FormatMediaType( + "form-data", + map[string]string{"filename": filename}, + ) + + resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ + Method: "PUT", + URL: s.resourcesURL("tomcat", "jdk").String(), + ContentType: "application/octet-stream", + ExtraHeaders: map[string]string{ + "Content-Sha384": content.Fingerprint.String(), + "Content-Length": fmt.Sprintf("%d", content.Size), + "Content-Disposition": disp, + }, + Body: strings.NewReader("fake_nonce"), + }) + s.assertJSONErrorResponse(c, resp, http.StatusNotFound, `application "tomcat" not found`) + resp.Body.Close() +} + +func (s *resourcesAuthSuite) TestAuthRejectsUserWithoutPermission(c *gc.C) { + u := s.Factory.MakeUser(c, &factory.UserParams{ + Name: "oryx", + Password: "gardener", + Access: permission.ReadAccess, + }) + + resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ + Tag: u.Tag().String(), + Password: "gardener", + Method: "PUT", + URL: s.resourcesURL("tomcat", "jdk").String(), + }) + defer resp.Body.Close() + s.assertPlainErrorResponse( + c, resp, http.StatusForbidden, + "authorization failed: permission denied", + ) +}
apiserver/resources.go+43 −18 modified@@ -40,16 +40,22 @@ type ResourcesBackend interface { UpdatePendingResource(applicationID, pendingID, userID string, res charmresource.Resource, r io.Reader) (resources.Resource, error) } -// ResourcesHandler is the HTTP handler for client downloads and +// ResourcesUploadHandler is the HTTP handler for client // uploads of resources. -type ResourcesHandler struct { - StateAuthFunc func(*http.Request, ...string) (ResourcesBackend, state.PoolHelper, names.Tag, error) +type ResourcesUploadHandler struct { ChangeAllowedFunc func(*http.Request) error + StateFunc func(*http.Request) (ResourcesBackend, state.PoolHelper, names.Tag, error) +} + +// ResourcesDownloadHandler is the HTTP handler for client +// downloads of resources. +type ResourcesDownloadHandler struct { + StateAuthFunc func(*http.Request, ...string) (ResourcesBackend, state.PoolHelper, error) } // ServeHTTP implements http.Handler. -func (h *ResourcesHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { - backend, poolhelper, tag, err := h.StateAuthFunc(req, names.UserTagKind, names.MachineTagKind, names.ControllerAgentTagKind, names.ApplicationTagKind) +func (h *ResourcesDownloadHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + backend, poolhelper, err := h.StateAuthFunc(req, names.UserTagKind, names.MachineTagKind, names.ControllerAgentTagKind, names.ApplicationTagKind) if err != nil { if err := sendError(resp, err); err != nil { logger.Errorf("%v", err) @@ -75,6 +81,36 @@ func (h *ResourcesHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request if _, err := io.Copy(resp, reader); err != nil { logger.Errorf("resource download failed: %v", err) } + default: + if err := sendError(resp, errors.MethodNotAllowedf("unsupported method: %q", req.Method)); err != nil { + logger.Errorf("%v", err) + } + } +} + +func (h *ResourcesDownloadHandler) download(backend ResourcesBackend, req *http.Request) (io.ReadCloser, int64, error) { + defer req.Body.Close() + + query := req.URL.Query() + application := query.Get(":application") + name := query.Get(":resource") + + resource, reader, err := backend.OpenResource(application, name) + return reader, resource.Size, errors.Trace(err) +} + +// ServeHTTP implements http.Handler. +func (h *ResourcesUploadHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + backend, closer, tag, err := h.StateFunc(req) + if err != nil { + if err := sendError(resp, err); err != nil { + logger.Errorf("%v", err) + } + return + } + defer closer.Release() + + switch req.Method { case "PUT": if err := h.ChangeAllowedFunc(req); err != nil { if err := sendError(resp, err); err != nil { @@ -99,18 +135,7 @@ func (h *ResourcesHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request } } -func (h *ResourcesHandler) download(backend ResourcesBackend, req *http.Request) (io.ReadCloser, int64, error) { - defer req.Body.Close() - - query := req.URL.Query() - application := query.Get(":application") - name := query.Get(":resource") - - resource, reader, err := backend.OpenResource(application, name) - return reader, resource.Size, errors.Trace(err) -} - -func (h *ResourcesHandler) upload(backend ResourcesBackend, req *http.Request, username string) (*params.UploadResult, error) { +func (h *ResourcesUploadHandler) upload(backend ResourcesBackend, req *http.Request, username string) (*params.UploadResult, error) { defer req.Body.Close() uploaded, err := h.readResource(backend, req) @@ -153,7 +178,7 @@ type uploadedResource struct { } // readResource extracts the relevant info from the request. -func (h *ResourcesHandler) readResource(backend ResourcesBackend, req *http.Request) (*uploadedResource, error) { +func (h *ResourcesUploadHandler) readResource(backend ResourcesBackend, req *http.Request) (*uploadedResource, error) { uReq, err := extractUploadRequest(req) if err != nil { return nil, errors.Trace(err)
apiserver/resources_test.go+50 −27 modified@@ -35,12 +35,13 @@ import ( type ResourcesHandlerSuite struct { testing.IsolationSuite - stateAuthErr error - backend *fakeBackend - username string - req *http.Request - recorder *httptest.ResponseRecorder - handler *apiserver.ResourcesHandler + stateAuthErr error + backend *fakeBackend + username string + req *http.Request + recorder *httptest.ResponseRecorder + uploadHandler *apiserver.ResourcesUploadHandler + downloadHandler *apiserver.ResourcesDownloadHandler } var _ = gc.Suite(&ResourcesHandlerSuite{}) @@ -59,13 +60,27 @@ func (s *ResourcesHandlerSuite) SetUpTest(c *gc.C) { c.Assert(err, jc.ErrorIsNil) s.req = req s.recorder = httptest.NewRecorder() - s.handler = &apiserver.ResourcesHandler{ - StateAuthFunc: s.authState, + s.uploadHandler = &apiserver.ResourcesUploadHandler{ + StateFunc: s.stateUpload, ChangeAllowedFunc: func(*http.Request) error { return nil }, } + s.downloadHandler = &apiserver.ResourcesDownloadHandler{ + StateAuthFunc: s.authStateDownload, + } +} + +func (s *ResourcesHandlerSuite) authStateDownload(req *http.Request, tagKinds ...string) ( + apiserver.ResourcesBackend, state.PoolHelper, error, +) { + if s.stateAuthErr != nil { + return nil, nil, errors.Trace(s.stateAuthErr) + } + + ph := apiservertesting.StubPoolHelper{StubRelease: func() bool { return false }} + return s.backend, ph, nil } -func (s *ResourcesHandlerSuite) authState(req *http.Request, tagKinds ...string) ( +func (s *ResourcesHandlerSuite) stateUpload(req *http.Request) ( apiserver.ResourcesBackend, state.PoolHelper, names.Tag, error, ) { if s.stateAuthErr != nil { @@ -80,42 +95,50 @@ func (s *ResourcesHandlerSuite) authState(req *http.Request, tagKinds ...string) func (s *ResourcesHandlerSuite) TestExpectedAuthTags(c *gc.C) { expectedTags := set.NewStrings(names.UserTagKind, names.MachineTagKind, names.ControllerAgentTagKind, names.ApplicationTagKind) - s.handler.StateAuthFunc = func(req *http.Request, tagKinds ...string) (apiserver.ResourcesBackend, state.PoolHelper, names.Tag, error) { + s.downloadHandler.StateAuthFunc = func(req *http.Request, tagKinds ...string) (apiserver.ResourcesBackend, state.PoolHelper, error) { gotTags := set.NewStrings(tagKinds...) if gotTags.Difference(expectedTags).Size() != 0 || expectedTags.Difference(gotTags).Size() != 0 { c.Fatalf("unexpected tag kinds %v", tagKinds) - return nil, nil, nil, errors.NotValidf("tag kinds %v", tagKinds) + return nil, nil, errors.NotValidf("tag kinds %v", tagKinds) } ph := apiservertesting.StubPoolHelper{StubRelease: func() bool { return false }} - tag := names.NewUserTag(s.username) - return s.backend, ph, tag, nil + return s.backend, ph, nil } s.req.Method = "GET" - s.handler.ServeHTTP(s.recorder, s.req) + s.downloadHandler.ServeHTTP(s.recorder, s.req) s.checkResp(c, http.StatusOK, "application/octet-stream", resourceBody) } func (s *ResourcesHandlerSuite) TestStateAuthFailure(c *gc.C) { failure, expected := apiFailure("<failure>", "") s.stateAuthErr = failure - s.handler.ServeHTTP(s.recorder, s.req) + s.uploadHandler.ServeHTTP(s.recorder, s.req) s.checkResp(c, http.StatusInternalServerError, "application/json", expected) } -func (s *ResourcesHandlerSuite) TestUnsupportedMethod(c *gc.C) { - s.req.Method = "POST" +func (s *ResourcesHandlerSuite) TestDownloadUnsupportedMethod(c *gc.C) { + s.req.Method = "PUT" + + s.downloadHandler.ServeHTTP(s.recorder, s.req) + + _, expected := apiFailure(`unsupported method: "PUT"`, params.CodeMethodNotAllowed) + s.checkResp(c, http.StatusMethodNotAllowed, "application/json", expected) +} + +func (s *ResourcesHandlerSuite) TestUploadUnsupportedMethod(c *gc.C) { + s.req.Method = "GET" - s.handler.ServeHTTP(s.recorder, s.req) + s.uploadHandler.ServeHTTP(s.recorder, s.req) - _, expected := apiFailure(`unsupported method: "POST"`, params.CodeMethodNotAllowed) + _, expected := apiFailure(`unsupported method: "GET"`, params.CodeMethodNotAllowed) s.checkResp(c, http.StatusMethodNotAllowed, "application/json", expected) } func (s *ResourcesHandlerSuite) TestGetSuccess(c *gc.C) { s.req.Method = "GET" - s.handler.ServeHTTP(s.recorder, s.req) + s.downloadHandler.ServeHTTP(s.recorder, s.req) s.checkResp(c, http.StatusOK, "application/octet-stream", resourceBody) } @@ -127,7 +150,7 @@ func (s *ResourcesHandlerSuite) TestPutSuccess(c *gc.C) { s.backend.ReturnSetResource = res req, _ := newUploadRequest(c, "spam", "a-application", uploadContent) - s.handler.ServeHTTP(s.recorder, req) + s.uploadHandler.ServeHTTP(s.recorder, req) expected := mustMarshalJSON(¶ms.UploadResult{ Resource: api.Resource2API(res), @@ -143,12 +166,12 @@ func (s *ResourcesHandlerSuite) TestPutChangeBlocked(c *gc.C) { s.backend.ReturnSetResource = res expectedError := apiservererrors.OperationBlockedError("test block") - s.handler.ChangeAllowedFunc = func(*http.Request) error { + s.uploadHandler.ChangeAllowedFunc = func(*http.Request) error { return expectedError } req, _ := newUploadRequest(c, "spam", "a-application", uploadContent) - s.handler.ServeHTTP(s.recorder, req) + s.uploadHandler.ServeHTTP(s.recorder, req) expected := mustMarshalJSON(¶ms.ErrorResult{apiservererrors.ServerError(expectedError)}) s.checkResp(c, http.StatusBadRequest, "application/json", string(expected)) @@ -162,7 +185,7 @@ func (s *ResourcesHandlerSuite) TestPutSuccessDockerResource(c *gc.C) { s.backend.ReturnSetResource = res req, _ := newUploadRequest(c, "spam", "a-application", uploadContent) - s.handler.ServeHTTP(s.recorder, req) + s.uploadHandler.ServeHTTP(s.recorder, req) expected := mustMarshalJSON(¶ms.UploadResult{ Resource: api.Resource2API(res), @@ -181,7 +204,7 @@ func (s *ResourcesHandlerSuite) TestPutExtensionMismatch(c *gc.C) { req, _ := newUploadRequest(c, "spam", "a-application", content) req.Header.Set("Content-Disposition", "form-data; filename=different.ext") - s.handler.ServeHTTP(s.recorder, req) + s.uploadHandler.ServeHTTP(s.recorder, req) _, expected := apiFailure(`incorrect extension on resource upload "different.ext", expected ".tgz"`, "") @@ -199,7 +222,7 @@ func (s *ResourcesHandlerSuite) TestPutWithPending(c *gc.C) { req, _ := newUploadRequest(c, "spam", "a-application", content) req.URL.RawQuery += "&pendingid=some-unique-id" - s.handler.ServeHTTP(s.recorder, req) + s.uploadHandler.ServeHTTP(s.recorder, req) expected := mustMarshalJSON(¶ms.UploadResult{ Resource: api.Resource2API(res), @@ -215,7 +238,7 @@ func (s *ResourcesHandlerSuite) TestPutSetResourceFailure(c *gc.C) { s.backend.SetResourceErr = failure req, _ := newUploadRequest(c, "spam", "a-application", content) - s.handler.ServeHTTP(s.recorder, req) + s.uploadHandler.ServeHTTP(s.recorder, req) s.checkResp(c, http.StatusInternalServerError, "application/json", expected) }
go.mod+1 −1 modified@@ -113,6 +113,7 @@ require ( golang.org/x/tools v0.41.0 google.golang.org/api v0.256.0 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c + gopkg.in/errgo.v1 v1.0.1 gopkg.in/httprequest.v1 v1.2.1 gopkg.in/ini.v1 v1.67.0 gopkg.in/juju/environschema.v1 v1.0.1 @@ -264,7 +265,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect google.golang.org/grpc v1.77.0 // indirect google.golang.org/protobuf v1.36.10 // indirect - gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/gobwas/glob.v0 v0.2.3 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/juju/juju/commit/26ff93c903d55b0712c6fb3f6b254710edb971d4nvdPatchWEB
- github.com/advisories/GHSA-245v-p8fj-vwm2ghsaADVISORY
- github.com/juju/juju/security/advisories/GHSA-245v-p8fj-vwm2nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2025-68153ghsaADVISORY
News mentions
0No linked articles in our index yet.