Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tiup/mirror: add ability to change owner of a component #1676

Merged
merged 2 commits into from
Dec 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion cmd/mirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ of components or the repository itself.`,
newMirrorRenewCmd(),
newMirrorGrantCmd(),
newMirrorRotateCmd(),
newTransferOwnerCmd(),
)

return cmd
Expand Down Expand Up @@ -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 <component> <new-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"
Expand All @@ -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
}
Expand Down
24 changes: 19 additions & 5 deletions pkg/repository/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}
Expand All @@ -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}
Expand Down Expand Up @@ -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
})
}
Expand Down
7 changes: 7 additions & 0 deletions pkg/repository/model/publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type ComponentInfo interface {
Standalone() *bool
Yanked() *bool
Hidden() *bool
OwnerID() string
}

// PublishInfo implements ComponentInfo
Expand All @@ -38,6 +39,7 @@ type PublishInfo struct {
Stand *bool
Yank *bool
Hide *bool
Owner string
}

// TarInfo implements ComponentData
Expand Down Expand Up @@ -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
}
100 changes: 100 additions & 0 deletions server/rotate/component.go
Original file line number Diff line number Diff line change
@@ -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
}
74 changes: 22 additions & 52 deletions server/rotate/rotate_server.go → server/rotate/root.go
Original file line number Diff line number Diff line change
@@ -1,75 +1,45 @@
// 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"
"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/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
Expand All @@ -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:
Expand All @@ -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)
Expand Down
Loading