diff --git a/cmd/mirror.go b/cmd/mirror.go index 9790e1475d..171ca15295 100644 --- a/cmd/mirror.go +++ b/cmd/mirror.go @@ -74,6 +74,7 @@ of components or the repository itself.`, newMirrorRenewCmd(), newMirrorGrantCmd(), newMirrorRotateCmd(), + newTransferOwnerCmd(), ) return cmd @@ -388,6 +389,58 @@ func newMirrorRenewCmd() *cobra.Command { return cmd } +// the `mirror transfer-owner` sub command +func newTransferOwnerCmd() *cobra.Command { + addr := "0.0.0.0:8080" + + cmd := &cobra.Command{ + Use: "transfer-owner ", + Short: "Transfer component to another owner", + Long: "Transfer component to another owner, this must be done on the server.", + RunE: func(cmd *cobra.Command, args []string) error { + teleCommand = cmd.CommandPath() + if len(args) != 2 { + return cmd.Help() + } + + component := args[0] + newOwnerName := args[1] + env := environment.GlobalEnv() + + // read current manifests + index, err := env.V1Repository().FetchIndexManifest() + if err != nil { + return err + } + newOwner, found := index.Owners[newOwnerName] + if !found { + return fmt.Errorf("new owner '%s' is not in the available owner list", newOwnerName) + } + + m, err := env.V1Repository().FetchComponentManifest(component, true) + if err != nil { + return err + } + v1manifest.RenewManifest(m, time.Now()) + + // validate new owner's authorization + newCompManifest, err := rotate.ServeComponent(addr, &newOwner, m) + if err != nil { + return err + } + + // update owner info + return env.V1Repository().Mirror().Publish(newCompManifest, &model.PublishInfo{ + Owner: newOwnerName, + }) + }, + } + + cmd.Flags().StringVarP(&addr, "addr", "", addr, "listen address:port when starting the temp server for signing") + + return cmd +} + // the `mirror rotate` sub command func newMirrorRotateCmd() *cobra.Command { addr := "0.0.0.0:8080" @@ -403,7 +456,7 @@ func newMirrorRotateCmd() *cobra.Command { return err } - manifest, err := rotate.Serve(addr, root) + manifest, err := rotate.ServeRoot(addr, root) if err != nil { return err } diff --git a/pkg/repository/model/model.go b/pkg/repository/model/model.go index 5f900c0900..5024cdc74c 100644 --- a/pkg/repository/model/model.go +++ b/pkg/repository/model/model.go @@ -170,7 +170,7 @@ func (m *model) Rotate(manifest *v1manifest.Manifest) error { func (m *model) Publish(manifest *v1manifest.Manifest, info ComponentInfo) error { signed := manifest.Signed.(*v1manifest.Component) initTime := time.Now() - return utils.RetryUntil(func() error { + pf := func() error { // Write the component manifest (component.json) if err := m.updateComponentManifest(manifest); err != nil { return err @@ -186,7 +186,8 @@ func (m *model) Publish(manifest *v1manifest.Manifest, info ComponentInfo) error var owner *v1manifest.Owner if err := m.updateIndexManifest(initTime, func(im *v1manifest.Manifest) (*v1manifest.Manifest, error) { // We only update index.json when it's a new component - // or the yanked, standalone, hidden fileds changed + // or the yanked, standalone, hidden fileds changed, + // or the owner of component changed var ( compItem v1manifest.ComponentItem compExist bool @@ -196,9 +197,17 @@ func (m *model) Publish(manifest *v1manifest.Manifest, info ComponentInfo) error signed := im.Signed.(*v1manifest.Index) if compItem, compExist = signed.Components[componentName]; compExist { // Find the owner of target component - o := signed.Owners[compItem.Owner] + var o v1manifest.Owner + if info.OwnerID() != "" { + o = signed.Owners[info.OwnerID()] + } else { + o = signed.Owners[compItem.Owner] + } owner = &o - if info.Yanked() == nil && info.Hidden() == nil && info.Standalone() == nil { + if info.Yanked() == nil && + info.Hidden() == nil && + info.Standalone() == nil && + info.OwnerID() == "" { // No changes on index.json return nil, nil } @@ -224,6 +233,9 @@ func (m *model) Publish(manifest *v1manifest.Manifest, info ComponentInfo) error if info.Standalone() != nil { compItem.Standalone = *info.Standalone() } + if info.OwnerID() != "" { + compItem.Owner = info.OwnerID() + } signed.Components[componentName] = compItem indexFileVersion = &v1manifest.FileVersion{Version: signed.Version + 1} @@ -276,7 +288,9 @@ func (m *model) Publish(manifest *v1manifest.Manifest, info ComponentInfo) error } } return m.txn.Commit() - }, func(err error) bool { + } + + return utils.RetryUntil(pf, func(err error) bool { return err == store.ErrorFsCommitConflict && m.txn.ResetManifest() == nil }) } diff --git a/pkg/repository/model/publish.go b/pkg/repository/model/publish.go index fa174299ae..48dcc9c7bc 100644 --- a/pkg/repository/model/publish.go +++ b/pkg/repository/model/publish.go @@ -30,6 +30,7 @@ type ComponentInfo interface { Standalone() *bool Yanked() *bool Hidden() *bool + OwnerID() string } // PublishInfo implements ComponentInfo @@ -38,6 +39,7 @@ type PublishInfo struct { Stand *bool Yank *bool Hide *bool + Owner string } // TarInfo implements ComponentData @@ -73,3 +75,8 @@ func (i *PublishInfo) Yanked() *bool { func (i *PublishInfo) Hidden() *bool { return i.Hide } + +// OwnerID implements ComponentInfo +func (i *PublishInfo) OwnerID() string { + return i.Owner +} diff --git a/server/rotate/component.go b/server/rotate/component.go new file mode 100644 index 0000000000..6b45e15626 --- /dev/null +++ b/server/rotate/component.go @@ -0,0 +1,100 @@ +// Copyright 2021 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package rotate + +import ( + "context" + "fmt" + "net/http" + + cjson "github.com/gibson042/canonicaljson-go" + "github.com/gorilla/mux" + "github.com/pingcap/errors" + "github.com/pingcap/fn" + logprinter "github.com/pingcap/tiup/pkg/logger/printer" + "github.com/pingcap/tiup/pkg/repository/v1manifest" + "github.com/pingcap/tiup/pkg/utils" +) + +// ServeComponent starts a temp server for receiving component signatures from owner +func ServeComponent(addr string, owner *v1manifest.Owner, comp *v1manifest.Component) (*v1manifest.Manifest, error) { + r := mux.NewRouter() + uri := fmt.Sprintf("/rotate/%s", utils.Base62Tag()) + + r.Handle(uri, fn.Wrap(func() (*v1manifest.Manifest, error) { + return &v1manifest.Manifest{Signed: comp}, nil + })).Methods("GET") + + sigCh := make(chan v1manifest.Signature) + r.Handle(uri, fn.Wrap(func(m *v1manifest.RawManifest) (*v1manifest.Manifest /* always nil */, error) { + for _, sig := range m.Signatures { + if err := verifyComponentSig(sig, owner, comp); err != nil { + return nil, err + } + sigCh <- sig + } + return nil, nil + })).Methods("POST") + + srv := &http.Server{Addr: addr, Handler: r} + go func() { + if err := srv.ListenAndServe(); err != nil { + logprinter.Errorf("server closed: %s", err.Error()) + } + close(sigCh) + }() + + manifest := &v1manifest.Manifest{Signed: comp} + status := newStatusRender(owner.Keys, addr, uri) + defer status.stop() + +SIGLOOP: + for sig := range sigCh { + for _, s := range manifest.Signatures { + if s.KeyID == sig.KeyID { + // Duplicate signature + continue SIGLOOP + } + } + manifest.Signatures = append(manifest.Signatures, sig) + status.render(manifest) + if len(manifest.Signatures) == len(owner.Keys) { + _ = srv.Shutdown(context.Background()) + break + } + } + + if len(manifest.Signatures) != len(owner.Keys) { + return nil, errors.New("no enough signature collected before server shutdown") + } + return manifest, nil +} + +func verifyComponentSig(sig v1manifest.Signature, owner *v1manifest.Owner, comp *v1manifest.Component) error { + payload, err := cjson.Marshal(comp) + if err != nil { + return fn.ErrorWithStatusCode(errors.Annotate(err, "marshal component manifest"), http.StatusInternalServerError) + } + + k := owner.Keys[sig.KeyID] + if k == nil { + // Received a signature signed by an invalid key + return fn.ErrorWithStatusCode(errors.New("the key is not valid"), http.StatusNotAcceptable) + } + if err := k.Verify(payload, sig.Sig); err != nil { + // Received an invalid signature + return fn.ErrorWithStatusCode(errors.New("the signature is not valid"), http.StatusNotAcceptable) + } + return nil +} diff --git a/server/rotate/rotate_server.go b/server/rotate/root.go similarity index 51% rename from server/rotate/rotate_server.go rename to server/rotate/root.go index fc9bc6e6f7..2768f2050f 100644 --- a/server/rotate/rotate_server.go +++ b/server/rotate/root.go @@ -1,11 +1,22 @@ +// Copyright 2020 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + package rotate import ( "context" "fmt" - "net" "net/http" - "strings" cjson "github.com/gibson042/canonicaljson-go" "github.com/gorilla/mux" @@ -13,63 +24,22 @@ import ( "github.com/pingcap/fn" logprinter "github.com/pingcap/tiup/pkg/logger/printer" "github.com/pingcap/tiup/pkg/repository/v1manifest" - "github.com/pingcap/tiup/pkg/tui/progress" + "github.com/pingcap/tiup/pkg/utils" ) -type statusRender struct { - mbar *progress.MultiBar - bars map[string]*progress.MultiBarItem -} - -func newStatusRender(manifest *v1manifest.Manifest, addr string) *statusRender { - ss := strings.Split(addr, ":") - if strings.Trim(ss[0], " ") == "" || strings.Trim(ss[0], " ") == "0.0.0.0" { - addrs, _ := net.InterfaceAddrs() - for _, addr := range addrs { - if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && ip.IP.To4() != nil { - ss[0] = ip.IP.To4().String() - break - } - } - } - - status := &statusRender{ - mbar: progress.NewMultiBar(fmt.Sprintf("Waiting all administrators to sign http://%s/rotate/root.json", strings.Join(ss, ":"))), - bars: make(map[string]*progress.MultiBarItem), - } - root := manifest.Signed.(*v1manifest.Root) - for key := range root.Roles[v1manifest.ManifestTypeRoot].Keys { - status.bars[key] = status.mbar.AddBar(fmt.Sprintf(" - Waiting key %s", key)) - } - status.mbar.StartRenderLoop() - return status -} - -func (s *statusRender) render(manifest *v1manifest.Manifest) { - for _, sig := range manifest.Signatures { - s.bars[sig.KeyID].UpdateDisplay(&progress.DisplayProps{ - Prefix: fmt.Sprintf(" - Waiting key %s", sig.KeyID), - Mode: progress.ModeDone, - }) - } -} - -func (s *statusRender) stop() { - s.mbar.StopRenderLoop() -} - -// Serve starts a temp server for receiving signatures from administrators -func Serve(addr string, root *v1manifest.Root) (*v1manifest.Manifest, error) { +// ServeRoot starts a temp server for receiving root signatures from administrators +func ServeRoot(addr string, root *v1manifest.Root) (*v1manifest.Manifest, error) { r := mux.NewRouter() + uri := fmt.Sprintf("/rotate/%s", utils.Base62Tag()) - r.Handle("/rotate/root.json", fn.Wrap(func() (*v1manifest.Manifest, error) { + r.Handle(uri, fn.Wrap(func() (*v1manifest.Manifest, error) { return &v1manifest.Manifest{Signed: root}, nil })).Methods("GET") sigCh := make(chan v1manifest.Signature) - r.Handle("/rotate/root.json", fn.Wrap(func(m *v1manifest.RawManifest) (*v1manifest.Manifest /* always nil */, error) { + r.Handle(uri, fn.Wrap(func(m *v1manifest.RawManifest) (*v1manifest.Manifest /* always nil */, error) { for _, sig := range m.Signatures { - if err := verifySig(sig, root); err != nil { + if err := verifyRootSig(sig, root); err != nil { return nil, err } sigCh <- sig @@ -86,7 +56,7 @@ func Serve(addr string, root *v1manifest.Root) (*v1manifest.Manifest, error) { }() manifest := &v1manifest.Manifest{Signed: root} - status := newStatusRender(manifest, addr) + status := newStatusRender(root.Roles[v1manifest.ManifestTypeRoot].Keys, addr, uri) defer status.stop() SIGLOOP: @@ -111,7 +81,7 @@ SIGLOOP: return manifest, nil } -func verifySig(sig v1manifest.Signature, root *v1manifest.Root) error { +func verifyRootSig(sig v1manifest.Signature, root *v1manifest.Root) error { payload, err := cjson.Marshal(root) if err != nil { return fn.ErrorWithStatusCode(errors.Annotate(err, "marshal root manifest"), http.StatusInternalServerError) diff --git a/server/rotate/server.go b/server/rotate/server.go new file mode 100644 index 0000000000..462f8d0397 --- /dev/null +++ b/server/rotate/server.go @@ -0,0 +1,64 @@ +// Copyright 2020 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +package rotate + +import ( + "fmt" + "net" + "strings" + + "github.com/pingcap/tiup/pkg/repository/v1manifest" + "github.com/pingcap/tiup/pkg/tui/progress" +) + +type statusRender struct { + mbar *progress.MultiBar + bars map[string]*progress.MultiBarItem +} + +func newStatusRender(keys map[string]*v1manifest.KeyInfo, addr, uri string) *statusRender { + ss := strings.Split(addr, ":") + if strings.Trim(ss[0], " ") == "" || strings.Trim(ss[0], " ") == "0.0.0.0" { + addrs, _ := net.InterfaceAddrs() + for _, addr := range addrs { + if ip, ok := addr.(*net.IPNet); ok && !ip.IP.IsLoopback() && ip.IP.To4() != nil { + ss[0] = ip.IP.To4().String() + break + } + } + } + + status := &statusRender{ + mbar: progress.NewMultiBar(fmt.Sprintf("Waiting all key holders to sign http://%s%s", strings.Join(ss, ":"), uri)), + bars: make(map[string]*progress.MultiBarItem), + } + for key := range keys { + status.bars[key] = status.mbar.AddBar(fmt.Sprintf(" - Waiting key %s", key)) + } + status.mbar.StartRenderLoop() + return status +} + +func (s *statusRender) render(manifest *v1manifest.Manifest) { + for _, sig := range manifest.Signatures { + s.bars[sig.KeyID].UpdateDisplay(&progress.DisplayProps{ + Prefix: fmt.Sprintf(" - Waiting key %s", sig.KeyID), + Mode: progress.ModeDone, + }) + } +} + +func (s *statusRender) stop() { + s.mbar.StopRenderLoop() +}