func (s *suite) TestMacaroonAuthorization(c *gc.C) { ch := charmRepo.CharmDir("wordpress") curl := charm.MustParseReference("~charmers/utopic/wordpress-42") purl := charm.MustParseReference("utopic/wordpress-42") err := s.client.UploadCharmWithRevision(curl, ch, 42) c.Assert(err, gc.IsNil) err = s.client.Put("/"+curl.Path()+"/meta/perm/read", []string{"bob"}) c.Assert(err, gc.IsNil) // Create a client without basic auth credentials client := csclient.New(csclient.Params{ URL: s.srv.URL, }) var result struct{ IdRevision struct{ Revision int } } // TODO 2015-01-23: once supported, rewrite the test using POST requests. _, err = client.Meta(purl, &result) c.Assert(err, gc.ErrorMatches, `cannot get "/utopic/wordpress-42/meta/any\?include=id-revision": cannot get discharge from ".*": third party refused discharge: cannot discharge: no discharge`) c.Assert(httpbakery.IsDischargeError(errgo.Cause(err)), gc.Equals, true) s.discharge = func(cond, arg string) ([]checkers.Caveat, error) { return []checkers.Caveat{checkers.DeclaredCaveat("username", "bob")}, nil } _, err = client.Meta(curl, &result) c.Assert(err, gc.IsNil) c.Assert(result.IdRevision.Revision, gc.Equals, curl.Revision) visitURL := "http://0.1.2.3/visitURL" s.discharge = func(cond, arg string) ([]checkers.Caveat, error) { return nil, &httpbakery.Error{ Code: httpbakery.ErrInteractionRequired, Message: "interaction required", Info: &httpbakery.ErrorInfo{ VisitURL: visitURL, WaitURL: "http://0.1.2.3/waitURL", }} } client = csclient.New(csclient.Params{ URL: s.srv.URL, VisitWebPage: func(vurl *neturl.URL) error { c.Check(vurl.String(), gc.Equals, visitURL) return fmt.Errorf("stopping interaction") }}) _, err = client.Meta(purl, &result) c.Assert(err, gc.ErrorMatches, `cannot get "/utopic/wordpress-42/meta/any\?include=id-revision": cannot get discharge from ".*": cannot start interactive session: stopping interaction`) c.Assert(result.IdRevision.Revision, gc.Equals, curl.Revision) c.Assert(httpbakery.IsInteractionError(errgo.Cause(err)), gc.Equals, true) }
// AddCharmWithAuthorization adds the given charm URL (which must include revision) to // the environment, if it does not exist yet. Local charms are not // supported, only charm store URLs. See also AddLocalCharm(). // // The authorization macaroon, args.CharmStoreMacaroon, may be // omitted, in which case this call is equivalent to AddCharm. func AddCharmWithAuthorization(st *state.State, args params.AddCharmWithAuthorization) error { charmURL, err := charm.ParseURL(args.URL) if err != nil { return err } if charmURL.Schema != "cs" { return fmt.Errorf("only charm store charm URLs are supported, with cs: schema") } if charmURL.Revision < 0 { return fmt.Errorf("charm URL must include revision") } // First, check if a pending or a real charm exists in state. stateCharm, err := st.PrepareStoreCharmUpload(charmURL) if err != nil { return err } if stateCharm.IsUploaded() { // Charm already in state (it was uploaded already). return nil } // Get the charm and its information from the store. envConfig, err := st.EnvironConfig() if err != nil { return err } csURL, err := url.Parse(csclient.ServerURL) if err != nil { return err } csParams := charmrepo.NewCharmStoreParams{ URL: csURL.String(), HTTPClient: httpbakery.NewHTTPClient(), } if args.CharmStoreMacaroon != nil { // Set the provided charmstore authorizing macaroon // as a cookie in the HTTP client. // TODO discharge any third party caveats in the macaroon. ms := []*macaroon.Macaroon{args.CharmStoreMacaroon} httpbakery.SetCookie(csParams.HTTPClient.Jar, csURL, ms) } repo := config.SpecializeCharmRepo( NewCharmStore(csParams), envConfig, ) downloadedCharm, err := repo.Get(charmURL) if err != nil { cause := errors.Cause(err) if httpbakery.IsDischargeError(cause) || httpbakery.IsInteractionError(cause) { return errors.NewUnauthorized(err, "") } return errors.Trace(err) } // Open it and calculate the SHA256 hash. downloadedBundle, ok := downloadedCharm.(*charm.CharmArchive) if !ok { return errors.Errorf("expected a charm archive, got %T", downloadedCharm) } archive, err := os.Open(downloadedBundle.Path) if err != nil { return errors.Annotate(err, "cannot read downloaded charm") } defer archive.Close() bundleSHA256, size, err := utils.ReadSHA256(archive) if err != nil { return errors.Annotate(err, "cannot calculate SHA256 hash of charm") } if _, err := archive.Seek(0, 0); err != nil { return errors.Annotate(err, "cannot rewind charm archive") } // Store the charm archive in environment storage. return StoreCharmArchive( st, charmURL, downloadedCharm, archive, size, bundleSHA256, ) }
// AddCharmWithAuthorization adds the given charm URL (which must include revision) to // the environment, if it does not exist yet. Local charms are not // supported, only charm store URLs. See also AddLocalCharm(). // // The authorization macaroon, args.CharmStoreMacaroon, may be // omitted, in which case this call is equivalent to AddCharm. func AddCharmWithAuthorization(st *state.State, args params.AddCharmWithAuthorization) error { charmURL, err := charm.ParseURL(args.URL) if err != nil { return err } if charmURL.Schema != "cs" { return fmt.Errorf("only charm store charm URLs are supported, with cs: schema") } if charmURL.Revision < 0 { return fmt.Errorf("charm URL must include revision") } // First, check if a pending or a real charm exists in state. stateCharm, err := st.PrepareStoreCharmUpload(charmURL) if err != nil { return err } if stateCharm.IsUploaded() { // Charm already in state (it was uploaded already). return nil } // Open a charm store client. repo, err := openCSRepo(args) if err != nil { return err } envConfig, err := st.ModelConfig() if err != nil { return err } repo = config.SpecializeCharmRepo(repo, envConfig).(*charmrepo.CharmStore) // Get the charm and its information from the store. downloadedCharm, err := repo.Get(charmURL) if err != nil { cause := errors.Cause(err) if httpbakery.IsDischargeError(cause) || httpbakery.IsInteractionError(cause) { return errors.NewUnauthorized(err, "") } return errors.Trace(err) } if err := checkMinVersion(downloadedCharm); err != nil { return errors.Trace(err) } // Open it and calculate the SHA256 hash. downloadedBundle, ok := downloadedCharm.(*charm.CharmArchive) if !ok { return errors.Errorf("expected a charm archive, got %T", downloadedCharm) } archive, err := os.Open(downloadedBundle.Path) if err != nil { return errors.Annotate(err, "cannot read downloaded charm") } defer archive.Close() bundleSHA256, size, err := utils.ReadSHA256(archive) if err != nil { return errors.Annotate(err, "cannot calculate SHA256 hash of charm") } if _, err := archive.Seek(0, 0); err != nil { return errors.Annotate(err, "cannot rewind charm archive") } // Store the charm archive in environment storage. return StoreCharmArchive( st, CharmArchive{ ID: charmURL, Charm: downloadedCharm, Data: archive, Size: size, SHA256: bundleSHA256, }, ) }