Skip to content

Commit

Permalink
tiup/mirror: add ability to change owner of a component (#1676)
Browse files Browse the repository at this point in the history
  • Loading branch information
AstroProfundis authored Dec 16, 2021
1 parent 3523106 commit 60ae371
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 58 deletions.
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

0 comments on commit 60ae371

Please sign in to comment.