github.com/kbehouse/nsc@v0.0.6/cmd/migrate.go (about)

     1  /*
     2   * Copyright 2018-2019 The NATS Authors
     3   * Licensed under the Apache License, Version 2.0 (the "License");
     4   * you may not use this file except in compliance with the License.
     5   * You may obtain a copy of the License at
     6   *
     7   * http://www.apache.org/licenses/LICENSE-2.0
     8   *
     9   * Unless required by applicable law or agreed to in writing, software
    10   * distributed under the License is distributed on an "AS IS" BASIS,
    11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12   * See the License for the specific language governing permissions and
    13   * limitations under the License.
    14   */
    15  
    16  package cmd
    17  
    18  import (
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os"
    22  	"path/filepath"
    23  	"strings"
    24  
    25  	"github.com/kbehouse/nsc/cmd/store"
    26  	cli "github.com/nats-io/cliprompts/v2"
    27  	"github.com/nats-io/jwt/v2"
    28  	"github.com/nats-io/nkeys"
    29  	"github.com/spf13/cobra"
    30  )
    31  
    32  func createMigrateCmd() *cobra.Command {
    33  	var params MigrateCmdParams
    34  	var cmd = &cobra.Command{
    35  		Hidden: true,
    36  
    37  		Short:   "Migrate an account to the current operator",
    38  		Example: "migrate --url <path or url to account jwt>",
    39  		Use:     `migrate`,
    40  		Args:    MaxArgs(0),
    41  		RunE: func(cmd *cobra.Command, args []string) error {
    42  			if err := RunAction(cmd, args, &params); err != nil {
    43  				return err
    44  			}
    45  			return nil
    46  		},
    47  	}
    48  	cmd.Flags().StringVarP(&params.url, "url", "u", "", "path or url to import jwt from")
    49  	cmd.Flags().StringVarP(&params.storeDir, "operator-dir", "", "", "path to an operator dir - all accounts are migrated")
    50  	cmd.Flags().BoolVarP(&params.overwrite, "force", "F", false, "overwrite accounts with the same name")
    51  	return cmd
    52  }
    53  
    54  func init() {
    55  	GetRootCmd().AddCommand(createMigrateCmd())
    56  }
    57  
    58  type MigrateCmdParams struct {
    59  	url       string
    60  	storeDir  string
    61  	overwrite bool
    62  	Jobs      []*MigrateJob
    63  }
    64  
    65  func (p *MigrateCmdParams) SetDefaults(ctx ActionCtx) error {
    66  	if p.url != "" && p.storeDir != "" {
    67  		return fmt.Errorf("specify one of --url or --store-dir")
    68  	}
    69  	return nil
    70  }
    71  
    72  func (p *MigrateCmdParams) PreInteractive(ctx ActionCtx) error {
    73  	ok, err := cli.Confirm("migrate all accounts under a particular operator", true)
    74  	if err != nil {
    75  		return err
    76  	}
    77  	if ok {
    78  		p.storeDir, err = cli.Prompt("specify the directory for the operator", "", cli.Val(func(v string) error {
    79  			_, err := store.LoadStore(v)
    80  			return err
    81  		}))
    82  		if err != nil {
    83  			return err
    84  		}
    85  	} else {
    86  		p.url, err = cli.Prompt("account jwt url/or path ", p.url, cli.Val(func(v string) error {
    87  			// we expect either a file or url
    88  			if IsURL(v) {
    89  				return nil
    90  			}
    91  			v, err := Expand(v)
    92  			if err != nil {
    93  				return err
    94  			}
    95  			_, err = os.Stat(v)
    96  			return err
    97  		}))
    98  		if err != nil {
    99  			return err
   100  		}
   101  	}
   102  	return nil
   103  }
   104  
   105  func (p *MigrateCmdParams) Load(ctx ActionCtx) error {
   106  	var err error
   107  	if p.storeDir != "" {
   108  		p.storeDir, err = Expand(p.storeDir)
   109  		if err != nil {
   110  			return err
   111  		}
   112  
   113  		s, err := store.LoadStore(p.storeDir)
   114  		if err != nil {
   115  			return fmt.Errorf("error loading operator %#q: %v", p.storeDir, err)
   116  		}
   117  		names, err := s.ListSubContainers(store.Accounts)
   118  		if err != nil {
   119  			return fmt.Errorf("error listing accounts in %#q: %v", p.storeDir, err)
   120  		}
   121  		for _, n := range names {
   122  			mj := NewMigrateJob(filepath.Join(p.storeDir, store.Accounts, n, store.JwtName(n)), p.overwrite)
   123  			p.Jobs = append(p.Jobs, &mj)
   124  		}
   125  	} else {
   126  		mj := NewMigrateJob(p.url, p.overwrite)
   127  		p.Jobs = append(p.Jobs, &mj)
   128  	}
   129  
   130  	for _, j := range p.Jobs {
   131  		j.Load(ctx)
   132  	}
   133  	return nil
   134  }
   135  
   136  func (p *MigrateCmdParams) PostInteractive(ctx ActionCtx) error {
   137  	for _, j := range p.Jobs {
   138  		if j.OK() {
   139  			j.PostInteractive(ctx)
   140  		}
   141  	}
   142  	return nil
   143  }
   144  
   145  func (p *MigrateCmdParams) Validate(ctx ActionCtx) error {
   146  	for _, j := range p.Jobs {
   147  		if j.OK() {
   148  			j.Validate(ctx)
   149  		}
   150  	}
   151  	return nil
   152  }
   153  
   154  func (p *MigrateCmdParams) Run(ctx ActionCtx) (store.Status, error) {
   155  	var jobs store.MultiJob
   156  	for _, j := range p.Jobs {
   157  		if j.OK() {
   158  			j.Run(ctx)
   159  		}
   160  		jobs = append(jobs, j.status)
   161  	}
   162  	m, err := jobs.Summary()
   163  	if m != "" {
   164  		ctx.CurrentCmd().Println(m)
   165  	}
   166  	return jobs, err
   167  }
   168  
   169  type MigrateJob struct {
   170  	accountToken  string
   171  	claim         *jwt.AccountClaims
   172  	url           string
   173  	isFileImport  bool
   174  	operator      string
   175  	migratedUsers []*jwt.UserClaims
   176  	overwrite     bool
   177  
   178  	status store.Status
   179  }
   180  
   181  func NewMigrateJob(url string, overwrite bool) MigrateJob {
   182  	return MigrateJob{url: url, overwrite: overwrite, status: &store.Report{}}
   183  }
   184  
   185  func (j *MigrateJob) OK() bool {
   186  	code := j.status.Code()
   187  	return code == store.OK || code == store.NONE
   188  }
   189  
   190  func (j *MigrateJob) getAccountKeys() []string {
   191  	var keys []string
   192  	keys = append(keys, j.claim.Subject)
   193  	keys = append(keys, j.claim.SigningKeys.Keys()...)
   194  	return keys
   195  }
   196  
   197  func (j *MigrateJob) Load(ctx ActionCtx) {
   198  	if j.url == "" {
   199  		j.status = store.ErrorStatus("an url or path to the account jwt is required")
   200  		return
   201  	}
   202  	data, err := LoadFromFileOrURL(j.url)
   203  	if err != nil {
   204  		j.status = store.ErrorStatus(fmt.Sprintf("error loading from %#q: %v", j.url, err))
   205  		return
   206  	}
   207  	j.isFileImport = !IsURL(j.url)
   208  
   209  	j.accountToken, err = jwt.ParseDecoratedJWT(data)
   210  	if err != nil {
   211  		j.status = store.ErrorStatus(fmt.Sprintf("error parsing JWT: %v", err))
   212  		return
   213  	}
   214  	j.claim, err = jwt.DecodeAccountClaims(j.accountToken)
   215  	if err != nil {
   216  		j.status = store.ErrorStatus(fmt.Sprintf("error decoding JWT: %v", err))
   217  		return
   218  	}
   219  }
   220  
   221  func (j *MigrateJob) PostInteractive(ctx ActionCtx) {
   222  	if ctx.StoreCtx().Store.HasAccount(j.claim.Name) && !j.overwrite {
   223  		aac, err := ctx.StoreCtx().Store.ReadAccountClaim(j.claim.Name)
   224  		if err != nil {
   225  
   226  			j.status = store.ErrorStatus(fmt.Sprintf("error reading account JWT: %v", err))
   227  			return
   228  		}
   229  		j.overwrite = aac.Subject == j.claim.Subject
   230  		if !j.overwrite {
   231  			j.overwrite, err = cli.Confirm("account %q already exists under the current operator, replace it", false)
   232  			if err != nil {
   233  				j.status = store.ErrorStatus(fmt.Sprintf("%v", err))
   234  				return
   235  			}
   236  		}
   237  	}
   238  }
   239  
   240  func (j *MigrateJob) Validate(ctx ActionCtx) {
   241  	if j.isFileImport {
   242  		parent := ctx.StoreCtx().Store.Dir
   243  		// it is already determined to be a file
   244  		fp, err := Expand(j.url)
   245  		if err != nil {
   246  			j.status = store.ErrorStatus(fmt.Sprintf("%v", err))
   247  			return
   248  		}
   249  		if strings.HasPrefix(fp, parent) {
   250  			j.status = store.ErrorStatus(fmt.Sprintf("cannot migrate %q onto itself", fp))
   251  			return
   252  		}
   253  	}
   254  
   255  	if !j.overwrite && ctx.StoreCtx().Store.HasAccount(j.claim.Name) {
   256  		j.status = store.ErrorStatus(fmt.Sprintf("account %q already exists, specify --force to overwrite", j.claim.Name))
   257  		return
   258  	}
   259  
   260  	keys := j.getAccountKeys()
   261  	var hasOne bool
   262  	for _, k := range keys {
   263  		kp, _ := ctx.StoreCtx().KeyStore.GetKeyPair(k)
   264  		if kp != nil {
   265  			hasOne = true
   266  			break
   267  		}
   268  	}
   269  	if !hasOne {
   270  		j.status = store.ErrorStatus(fmt.Sprintf("unable to find an account key for %q - need one of %s", j.claim.Name, strings.Join(keys, ", ")))
   271  		return
   272  	}
   273  }
   274  
   275  func (j *MigrateJob) Run(ctx ActionCtx) {
   276  	ctx.CurrentCmd().SilenceUsage = true
   277  	j.operator = ctx.StoreCtx().Operator.Name
   278  
   279  	token, err := jwt.ParseDecoratedJWT([]byte(j.accountToken))
   280  	if err != nil {
   281  		j.status = store.ErrorStatus(fmt.Sprintf("%v", err))
   282  		return
   283  	}
   284  	ac, err := jwt.DecodeAccountClaims(token)
   285  	if err != nil {
   286  		j.status = store.ErrorStatus(fmt.Sprintf("%v", err))
   287  		return
   288  	}
   289  
   290  	if ctx.StoreCtx().Store.IsManaged() {
   291  		var keys []string
   292  		keys = append(keys, ac.Subject)
   293  		keys = append(keys, ac.SigningKeys.Keys()...)
   294  
   295  		// need to sign it with any key we can get
   296  		var kp nkeys.KeyPair
   297  		for _, k := range keys {
   298  			kp, _ = ctx.StoreCtx().KeyStore.GetKeyPair(k)
   299  			if kp != nil {
   300  				break
   301  			}
   302  		}
   303  		if kp == nil {
   304  			j.status = store.ErrorStatus(fmt.Sprintf("unable to find any account keys - need any of %s", strings.Join(keys, ", ")))
   305  			return
   306  		}
   307  		j.accountToken, err = ac.Encode(kp)
   308  		if err != nil {
   309  			j.status = store.ErrorStatus(fmt.Sprintf("%v", err))
   310  			return
   311  		}
   312  	}
   313  
   314  	remote, err := ctx.StoreCtx().Store.StoreClaim([]byte(j.accountToken))
   315  	if err != nil {
   316  		j.status = store.ErrorStatus(fmt.Sprintf("failed to migrate %q: %v", ac.Name, err))
   317  		return
   318  	}
   319  
   320  	if j.isFileImport {
   321  		udir := filepath.Join(filepath.Dir(j.url), store.Users)
   322  		fi, err := os.Stat(udir)
   323  		if err == nil && fi.IsDir() {
   324  			infos, err := ioutil.ReadDir(udir)
   325  			if err != nil {
   326  				j.status = store.ErrorStatus(fmt.Sprintf("%v", err))
   327  				return
   328  			}
   329  			for _, v := range infos {
   330  				n := v.Name()
   331  				if !v.IsDir() && filepath.Ext(n) == ".jwt" {
   332  					up := filepath.Join(udir, n)
   333  					d, err := Read(up)
   334  					if err != nil {
   335  						j.status = store.ErrorStatus(fmt.Sprintf("%v", err))
   336  						return
   337  					}
   338  					s, err := jwt.ParseDecoratedJWT(d)
   339  					if err != nil {
   340  						j.status = store.ErrorStatus(fmt.Sprintf("%v", err))
   341  						return
   342  					}
   343  					uc, err := jwt.DecodeUserClaims(s)
   344  					if err != nil {
   345  						j.status = store.ErrorStatus(fmt.Sprintf("%v", err))
   346  						return
   347  					}
   348  					if err := ctx.StoreCtx().Store.StoreRaw([]byte(s)); err != nil {
   349  						j.status = store.ErrorStatus(fmt.Sprintf("%v", err))
   350  						return
   351  					}
   352  					j.migratedUsers = append(j.migratedUsers, uc)
   353  				}
   354  			}
   355  		}
   356  	}
   357  
   358  	m := fmt.Sprintf("migrated %q to operator %q", j.claim.Name, j.operator)
   359  	um := fmt.Sprintf("%d users migrated", len(j.migratedUsers))
   360  	if len(j.migratedUsers) == 0 {
   361  		um = "no users migrated"
   362  	}
   363  	if !j.isFileImport {
   364  		um = ""
   365  	}
   366  
   367  	j.status = store.OKStatus(fmt.Sprintf("%s [%s]", m, um))
   368  	if remote != nil {
   369  		si, ok := j.status.(*store.Report)
   370  		if ok {
   371  			si.Details = append(si.Details, remote)
   372  		}
   373  	}
   374  }