github.com/nats-io/nsc/v2@v2.8.7-0.20240307184528-efd7023c6896/cmd/addoperator.go (about)

     1  /*
     2   * Copyright 2018-2023 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  	"errors"
    20  	"fmt"
    21  	"os"
    22  	"strings"
    23  
    24  	cli "github.com/nats-io/cliprompts/v2"
    25  	"github.com/nats-io/jwt/v2"
    26  	"github.com/nats-io/nkeys"
    27  	"github.com/nats-io/nsc/v2/cmd/store"
    28  	"github.com/spf13/cobra"
    29  )
    30  
    31  func createAddOperatorCmd() *cobra.Command {
    32  	var params AddOperatorParams
    33  	cmd := &cobra.Command{
    34  		Use:          "operator",
    35  		Short:        "Add an operator",
    36  		Args:         cobra.MaximumNArgs(1),
    37  		SilenceUsage: true,
    38  
    39  		RunE: func(cmd *cobra.Command, args []string) error {
    40  			if err := RunStoreLessAction(cmd, args, &params); err != nil {
    41  				return err
    42  			}
    43  			if NscCwdOnly {
    44  				return nil
    45  			}
    46  			return GetConfig().SetOperator(params.name)
    47  		},
    48  	}
    49  	cmd.Flags().StringVarP(&params.name, "name", "n", "", "operator name")
    50  	cmd.Flags().StringVarP(&params.jwtPath, "url", "u", "", "import from a jwt server url, file, or well known operator")
    51  	cmd.Flags().BoolVarP(&params.genSk, "generate-signing-key", "", false, "generate a signing key with the operator")
    52  	cmd.Flags().BoolVarP(&params.sysAcc, "sys", "s", false, "generate system account with the operator (if specified will be signed with signing key)")
    53  	cmd.Flags().BoolVarP(&params.force, "force", "", false, "on import, overwrite existing when already present")
    54  	params.TimeParams.BindFlags(cmd)
    55  
    56  	return cmd
    57  }
    58  
    59  func init() {
    60  	addCmd.AddCommand(createAddOperatorCmd())
    61  }
    62  
    63  func JWTUpgradeBannerJWT(ver int) error {
    64  	extra := ""
    65  	if ver == 1 {
    66  		extra = " - please downgrade to 0.5.0 by executing `nsc update --version 0.5.0`."
    67  	}
    68  	return fmt.Errorf(`the operator jwt (v%d) is incompatible this version of nsc%s`,
    69  		ver, extra)
    70  }
    71  
    72  type AddOperatorParams struct {
    73  	SignerParams
    74  	TimeParams
    75  	jwtPath  string
    76  	token    string
    77  	name     string
    78  	generate bool
    79  	sysAcc   bool
    80  	force    bool
    81  	genSk    bool
    82  	keyPath  string
    83  }
    84  
    85  func (p *AddOperatorParams) SetDefaults(ctx ActionCtx) error {
    86  	p.name = NameFlagOrArgument(p.name, ctx)
    87  	if p.name == "*" {
    88  		p.name = GetRandomName(0)
    89  	}
    90  	p.generate = KeyPathFlag == ""
    91  	p.keyPath = KeyPathFlag
    92  	p.SignerParams.SetDefaults(nkeys.PrefixByteOperator, false, ctx)
    93  
    94  	return nil
    95  }
    96  
    97  func (p *AddOperatorParams) PreInteractive(ctx ActionCtx) error {
    98  	var err error
    99  
   100  	ok, err := cli.Confirm("import operator from a JWT", true)
   101  	if err != nil {
   102  		return err
   103  	}
   104  	if ok {
   105  		p.sysAcc = false
   106  		p.jwtPath, err = cli.Prompt("path or url for operator jwt", p.jwtPath, cli.Val(func(v string) error {
   107  			// is it is an URL or path
   108  			pv := cli.PathOrURLValidator()
   109  			if perr := pv(v); perr != nil {
   110  				// if it doesn't exist - could it be the name of well known operator
   111  				if os.IsNotExist(perr) {
   112  					wko, _ := FindKnownOperator(v)
   113  					if wko != nil {
   114  						return nil
   115  					}
   116  				}
   117  				return perr
   118  			}
   119  			return nil
   120  		}))
   121  		if err != nil {
   122  			return err
   123  		}
   124  	} else {
   125  		p.name, err = cli.Prompt("operator name", p.name, cli.NewLengthValidator(1))
   126  		if err != nil {
   127  			return err
   128  		}
   129  		if err = p.TimeParams.Edit(); err != nil {
   130  			return err
   131  		}
   132  		if p.sysAcc, err = cli.Confirm("Generate system account?", true); err != nil {
   133  			return err
   134  		}
   135  		if p.genSk, err = cli.Confirm("Generate signing key?", true); err != nil {
   136  			return err
   137  		}
   138  	}
   139  
   140  	return nil
   141  }
   142  
   143  func (p *AddOperatorParams) Load(ctx ActionCtx) error {
   144  	// change the value of the source to be a well-known
   145  	// operator if that is what they gave us
   146  	if p.jwtPath != "" {
   147  		pv := cli.PathOrURLValidator()
   148  		if err := pv(p.jwtPath); os.IsNotExist(err) {
   149  			ko, _ := FindKnownOperator(p.jwtPath)
   150  			if ko != nil {
   151  				p.jwtPath = ko.AccountServerURL
   152  			}
   153  		}
   154  	}
   155  	if p.jwtPath != "" {
   156  		var err error
   157  		var data []byte
   158  		loadedFromURL := false
   159  
   160  		if IsURL(p.jwtPath) {
   161  			loadedFromURL = true
   162  			data, err = LoadFromURL(p.jwtPath)
   163  		} else {
   164  			data, err = Read(p.jwtPath)
   165  		}
   166  		if err != nil {
   167  			return fmt.Errorf("error reading %#q: %v", p.jwtPath, err)
   168  		}
   169  
   170  		token, err := jwt.ParseDecoratedJWT(data)
   171  		if err != nil {
   172  			return err
   173  		}
   174  		op, err := jwt.DecodeOperatorClaims(token)
   175  		if err != nil {
   176  			return fmt.Errorf("error importing operator jwt: %v", err)
   177  		}
   178  		if op.Version != 2 {
   179  			return JWTUpgradeBannerJWT(op.Version)
   180  		}
   181  		p.token = token
   182  		if p.name == "" {
   183  			p.name = op.Name
   184  			if loadedFromURL {
   185  				p.name = GetOperatorName(p.name, p.jwtPath)
   186  			}
   187  		}
   188  	}
   189  	return nil
   190  }
   191  
   192  func (p *AddOperatorParams) resolveOperatorNKey(s string) (nkeys.KeyPair, error) {
   193  	nk, err := store.ResolveKey(s)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  	if nk == nil {
   198  		return nil, fmt.Errorf("a key is required")
   199  	}
   200  	t, err := store.KeyType(nk)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  	if t != nkeys.PrefixByteOperator {
   205  		return nil, errors.New("specified key is not a valid operator nkey")
   206  	}
   207  	return nk, nil
   208  }
   209  
   210  func (p *AddOperatorParams) validateOperatorNKey(s string) error {
   211  	_, err := p.resolveOperatorNKey(s)
   212  	return err
   213  }
   214  
   215  func (p *AddOperatorParams) PostInteractive(ctx ActionCtx) error {
   216  	var err error
   217  
   218  	if p.token != "" {
   219  		// nothing to generate
   220  		return nil
   221  	}
   222  
   223  	if p.signerKP == nil {
   224  		p.generate, err = cli.Confirm("generate an operator nkey", true)
   225  		if err != nil {
   226  			return err
   227  		}
   228  		if !p.generate {
   229  			p.keyPath, err = cli.Prompt("path to an operator nkey or nkey", p.keyPath, cli.Val(p.validateOperatorNKey))
   230  			if err != nil {
   231  				return err
   232  			}
   233  		}
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  func (p *AddOperatorParams) Validate(ctx ActionCtx) error {
   240  	var err error
   241  	if p.token != "" {
   242  		if p.sysAcc {
   243  			return fmt.Errorf("importing an operator is not compatible with system account generation")
   244  		}
   245  		// validated on load
   246  		return nil
   247  	}
   248  	if p.force {
   249  		return fmt.Errorf("force only works with -u")
   250  	}
   251  	if p.name == "" {
   252  		ctx.CurrentCmd().SilenceUsage = false
   253  		return fmt.Errorf("operator name is required")
   254  	}
   255  	if strings.ContainsAny(p.name, "/\\") {
   256  		ctx.CurrentCmd().SilenceUsage = false
   257  		return fmt.Errorf("name cannot contain '/' or '\\'")
   258  	}
   259  
   260  	if err = p.TimeParams.Validate(); err != nil {
   261  		return err
   262  	}
   263  
   264  	if p.generate {
   265  		p.signerKP, err = nkeys.CreateOperator()
   266  		if err != nil {
   267  			return err
   268  		}
   269  		if p.keyPath, err = ctx.StoreCtx().KeyStore.Store(p.signerKP); err != nil {
   270  			return err
   271  		}
   272  	} else {
   273  		if p.genSk {
   274  			return fmt.Errorf("signing key can not be added when importing the operator")
   275  		}
   276  	}
   277  
   278  	if p.keyPath != "" {
   279  		p.signerKP, err = p.resolveOperatorNKey(p.keyPath)
   280  		if err != nil {
   281  			return err
   282  		}
   283  	}
   284  
   285  	if err := p.Resolve(ctx); err != nil {
   286  		return err
   287  	}
   288  
   289  	if p.sysAcc && p.signerKP == nil {
   290  		return fmt.Errorf("generating system account requires a key")
   291  	}
   292  
   293  	return nil
   294  }
   295  
   296  func (p *AddOperatorParams) Run(ctx ActionCtx) (store.Status, error) {
   297  	r := store.NewDetailedReport(false)
   298  	operator := &store.NamedKey{Name: p.name, KP: p.signerKP}
   299  	s, err := GetConfig().LoadStore(p.name)
   300  	if s == nil {
   301  		s, err = store.CreateStore(p.name, GetConfig().StoreRoot, operator)
   302  	} else if !p.force {
   303  		err = fmt.Errorf("operator named %s exists already", p.name)
   304  		r.AddError("%v please inspect and use --force to overwrite", err)
   305  	}
   306  	if err != nil {
   307  		return r, err
   308  	}
   309  
   310  	var sAcc *keys
   311  	var sUsr *keys
   312  	var skPub string
   313  
   314  	if p.token == "" {
   315  		ctx, err := s.GetContext()
   316  		if err != nil {
   317  			return nil, err
   318  		}
   319  		if p.generate {
   320  			p.keyPath, err = ctx.KeyStore.Store(p.signerKP)
   321  			if err != nil {
   322  				return nil, err
   323  			}
   324  		}
   325  		oc, err := ctx.Store.ReadOperatorClaim()
   326  		if err != nil {
   327  			return nil, err
   328  		}
   329  		if p.Start != "" {
   330  			oc.NotBefore, err = p.TimeParams.StartDate()
   331  			if err != nil {
   332  				return nil, err
   333  			}
   334  		}
   335  		if p.Expiry != "" {
   336  			oc.Expires, err = p.TimeParams.ExpiryDate()
   337  			if err != nil {
   338  				return nil, err
   339  			}
   340  		}
   341  		sysAccSigner := p.signerKP
   342  		if p.genSk {
   343  			sysAccSigner, err = nkeys.CreateOperator()
   344  			if err != nil {
   345  				return nil, err
   346  			}
   347  			if _, err := ctx.KeyStore.Store(sysAccSigner); err != nil {
   348  				return nil, err
   349  			}
   350  			skPub, err = sysAccSigner.PublicKey()
   351  			if err != nil {
   352  				return nil, err
   353  			}
   354  			oc.SigningKeys.Add(skPub)
   355  		}
   356  		if p.sysAcc {
   357  			if sAcc, sUsr, err = createSystemAccount(ctx, sysAccSigner); err != nil {
   358  				return nil, err
   359  			}
   360  			oc.SystemAccount = sAcc.PubKey
   361  		}
   362  
   363  		p.token, err = oc.Encode(p.signerKP)
   364  		if err != nil {
   365  			return nil, err
   366  		}
   367  	} else {
   368  		ctx, err := s.GetContext()
   369  		if err != nil {
   370  			return nil, err
   371  		}
   372  		oc, err := ctx.Store.ReadOperatorClaim()
   373  		if err == nil {
   374  			ocNew, err := jwt.DecodeOperatorClaims(p.token)
   375  			if err != nil {
   376  				return nil, err
   377  			}
   378  			if ocNew.Version != 2 {
   379  				return nil, JWTUpgradeBannerJWT(ocNew.Version)
   380  			}
   381  			if oc.Subject != ocNew.Subject {
   382  				err := fmt.Errorf("existing and new operator represent different entities")
   383  				if !p.force {
   384  					r.AddError(`%v resolve conflict first or provide "--force" to continue`, err)
   385  					return r, err
   386  				}
   387  				r.AddWarning("%v, forced to continue", err)
   388  			}
   389  		} else if err.(*store.ResourceErr).Err != store.ErrNotExist {
   390  			return nil, err
   391  		}
   392  	}
   393  
   394  	if p.generate && p.signerKP != nil {
   395  		pk, _ := p.signerKP.PublicKey()
   396  		r.AddOK("generated and stored operator key %q", pk)
   397  	}
   398  	// not in an action ctx - storing on self-created store
   399  	rs, err := s.StoreClaim([]byte(p.token))
   400  	if rs != nil {
   401  		r.Add(rs)
   402  	}
   403  	if err != nil {
   404  		r.AddFromError(err)
   405  	}
   406  	if r.HasNoErrors() {
   407  		verb := "added"
   408  		if p.jwtPath != "" {
   409  			verb = "imported"
   410  		}
   411  		r.AddOK("%s operator %q", verb, p.name)
   412  		r.AddOK("When running your own nats-server, make sure they run at least version 2.2.0")
   413  		if sAcc != nil && sUsr != nil {
   414  			if skPub != "" {
   415  				r.AddOK("created operator signing key: %s", skPub)
   416  			}
   417  			r.AddOK("created system_account: name:SYS id:%s", sAcc.PubKey)
   418  			r.AddOK("created system account user: name:sys id:%s", sUsr.PubKey)
   419  			r.AddOK("system account user creds file stored in %#q", AbbrevHomePaths(sUsr.CredsPath))
   420  		}
   421  	}
   422  	return r, err
   423  }