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

     1  // Copyright 2022 The NATS Authors
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package cmd
    15  
    16  import (
    17  	"fmt"
    18  	"net/url"
    19  	"os"
    20  	"strings"
    21  
    22  	"github.com/nats-io/nsc/v2/cmd/store"
    23  	"github.com/nats-io/nsc/v2/home"
    24  
    25  	"github.com/nats-io/jsm.go/natscontext"
    26  	"github.com/nats-io/jwt/v2"
    27  	"github.com/nats-io/nkeys"
    28  
    29  	"github.com/spf13/cobra"
    30  )
    31  
    32  func createLoadCmd() *cobra.Command {
    33  	var params LoadParams
    34  	cmd := &cobra.Command{
    35  		Use:   "load",
    36  		Short: "install entities for an operator, account and key",
    37  		RunE: func(cmd *cobra.Command, args []string) error {
    38  			// Note: Need to initialize the operator and environment
    39  			// before the RunAction command since that is dependent
    40  			// on an operator being available.
    41  			if err := params.setupHomeDir(); err != nil {
    42  				return err
    43  			}
    44  			if err := params.addOperator(); err != nil {
    45  				switch err.Error() {
    46  				case "operator not found":
    47  					cmd.Printf("Unable to find operator %q\n\n", params.operatorName)
    48  					cmd.Printf("If you have used this operator, please enter:")
    49  					cmd.Printf("`nsc env -s /path/to/storedir`\n")
    50  					cmd.Printf("or define the operator endpoint environment:\n")
    51  					cmd.Printf("`NSC_<OPERATOR_NAME>_OPERATOR=http/s://host:port/jwt/v1/operator`\n\n")
    52  				case "bad operator version":
    53  					_ = JWTUpgradeBannerJWT(1)
    54  				default:
    55  				}
    56  				return err
    57  			}
    58  			err := RunAction(cmd, args, &params)
    59  			if err != nil {
    60  				return err
    61  			}
    62  			return err
    63  		},
    64  	}
    65  
    66  	cmd.Flags().StringVarP(&params.resourceURL, "profile", "", "", "Profile URL to initialize NSC and NATS CLI env")
    67  	cmd.Flags().StringVarP(&params.accountServerURL, "url", "", "", "URL of the account server")
    68  	cmd.Flags().StringVarP(&params.accountSeed, "seed", "", "", "Seed of the account used to create users")
    69  	cmd.Flags().StringVarP(&params.username, "user", "", "default", "Default username")
    70  	return cmd
    71  }
    72  
    73  func init() {
    74  	rootCmd.AddCommand(createLoadCmd())
    75  }
    76  
    77  // LoadParams prepares an NSC environment and NATS Cli context based
    78  // on the URL of and Account seed.  Requires NATS CLI to be in PATH
    79  // to complete the setup of a user profile.
    80  type LoadParams struct {
    81  	r                *store.Report
    82  	resourceURL      string
    83  	operatorName     string
    84  	operator         *jwt.OperatorClaims
    85  	accountName      string
    86  	accountSeed      string
    87  	accountPublicKey string
    88  	account          *jwt.AccountClaims
    89  	accountKeyPair   nkeys.KeyPair
    90  	accountServerURL string
    91  	username         string
    92  	contextName      string
    93  	userCreds        string
    94  }
    95  
    96  func (p *LoadParams) setupHomeDir() error {
    97  	tc := GetConfig()
    98  	sr := tc.StoreRoot
    99  	if sr == "" {
   100  		sr = home.NscDataHome(home.StoresSubDirName)
   101  	}
   102  
   103  	sr, err := Expand(sr)
   104  	if err != nil {
   105  		return err
   106  	}
   107  	if err := MaybeMakeDir(sr); err != nil {
   108  		return err
   109  	}
   110  	if err := tc.ContextConfig.setStoreRoot(sr); err != nil {
   111  		return err
   112  	}
   113  	if err := tc.Save(); err != nil {
   114  		return err
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  func (p *LoadParams) addOperator() error {
   121  	if p.resourceURL != "" {
   122  		u, err := url.Parse(p.resourceURL)
   123  		if err != nil {
   124  			return err
   125  		}
   126  		p.operatorName = u.Hostname()
   127  		qparams := u.Query()
   128  		p.accountSeed = qparams.Get("secret")
   129  		username := qparams.Get("user")
   130  		if username != "" {
   131  			p.username = username
   132  		}
   133  		ko, err := FindKnownOperator(p.operatorName)
   134  		if err != nil {
   135  			return err
   136  		}
   137  		if ko == nil {
   138  			return fmt.Errorf("operator not found")
   139  		}
   140  		p.accountServerURL = ko.AccountServerURL
   141  	}
   142  	if p.accountServerURL == "" {
   143  		return fmt.Errorf("missing account server URL")
   144  	}
   145  
   146  	// Fetch the Operator JWT from the URL to get its claims
   147  	// and setup the local store.
   148  	data, err := LoadFromURL(p.accountServerURL)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	opJWT, err := jwt.ParseDecoratedJWT(data)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	operator, err := jwt.DecodeOperatorClaims(opJWT)
   157  	if err != nil {
   158  		return err
   159  	}
   160  	p.operator = operator
   161  
   162  	if operator.AccountServerURL == "" {
   163  		return fmt.Errorf("error importing operator %q - it doesn't define an account server url", p.operatorName)
   164  	}
   165  
   166  	// Store the Operator locally.
   167  	var (
   168  		onk store.NamedKey
   169  		s   *store.Store
   170  	)
   171  	onk.Name = p.operatorName
   172  	ts, err := GetConfig().LoadStore(onk.Name)
   173  	if err == nil {
   174  		tso, err := ts.ReadOperatorClaim()
   175  		if err == nil {
   176  			if tso.Subject == operator.Subject {
   177  				s = ts
   178  			} else {
   179  				return fmt.Errorf("error a different operator named %q already exists -- specify --dir to create at a different location", onk.Name)
   180  			}
   181  		}
   182  	}
   183  	if s == nil {
   184  		s, err = store.CreateStore(onk.Name, GetConfig().StoreRoot, &onk)
   185  		if err != nil {
   186  			return err
   187  		}
   188  	}
   189  	if err := s.StoreRaw([]byte(opJWT)); err != nil {
   190  		return err
   191  	}
   192  	return nil
   193  }
   194  
   195  func (p *LoadParams) addAccount(ctx ActionCtx) error {
   196  	r := p.r
   197  	// Take the seed and generate the public key for the Account then store it.
   198  	// The key is needed to be able to create local user creds as well to configure
   199  	// the context for the NATS CLI.
   200  	operator := p.operator
   201  	seed := p.accountSeed
   202  	if !strings.HasPrefix(seed, "SA") {
   203  		return fmt.Errorf("expected account seed to initialize")
   204  	}
   205  	akp, err := nkeys.FromSeed([]byte(seed))
   206  	if err != nil {
   207  		return fmt.Errorf("failed to parse account name as an nkey: %w", err)
   208  	}
   209  	p.accountKeyPair = akp
   210  	publicAccount, err := akp.PublicKey()
   211  	if err != nil {
   212  		return err
   213  	}
   214  	p.accountPublicKey = publicAccount
   215  
   216  	// Fetch Account JWT from URL.
   217  	accountURL := fmt.Sprintf("%s/accounts/%s", operator.AccountServerURL, publicAccount)
   218  	data, err := LoadFromURL(accountURL)
   219  	if err != nil {
   220  		return err
   221  	}
   222  	accJWT, err := jwt.ParseDecoratedJWT(data)
   223  	if err != nil {
   224  		return err
   225  	}
   226  	account, err := jwt.DecodeAccountClaims(accJWT)
   227  	if err != nil {
   228  		return err
   229  	}
   230  	p.account = account
   231  	p.accountName = account.Name
   232  
   233  	current := GetConfig()
   234  	err = current.ContextConfig.Update(current.StoreRoot, p.operatorName, "")
   235  	if err != nil {
   236  		return err
   237  	}
   238  	// Store the key and JWT.
   239  	_, err = store.StoreKey(akp)
   240  	if err != nil {
   241  		return err
   242  	}
   243  	sctx := ctx.StoreCtx()
   244  	err = sctx.SetContext(p.accountName, p.accountPublicKey)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	ts, err := current.LoadStore(p.operatorName)
   249  	if err != nil {
   250  		return err
   251  	}
   252  	rs, err := ts.StoreClaim([]byte(accJWT))
   253  	if rs != nil {
   254  		r.Add(rs)
   255  	}
   256  	if err != nil {
   257  		r.AddFromError(err)
   258  	}
   259  	return nil
   260  }
   261  
   262  func (p *LoadParams) addUser(ctx ActionCtx) error {
   263  	current := GetConfig()
   264  	err := current.ContextConfig.Update(current.StoreRoot, p.operatorName, p.accountName)
   265  	if err != nil {
   266  		return err
   267  	}
   268  
   269  	sctx := ctx.StoreCtx()
   270  	err = sctx.SetContext(p.accountName, p.accountPublicKey)
   271  	if err != nil {
   272  		return err
   273  	}
   274  	ts, err := current.LoadStore(p.operatorName)
   275  	if err != nil {
   276  		return err
   277  	}
   278  	r := p.r
   279  
   280  	// NOTE: Stamp the KeyStore env to use the Operator that is being setup.
   281  	sctx.KeyStore.Env = p.operatorName
   282  
   283  	// Check if username is already setup, if so then just find the creds instead.
   284  	userFields := []string{store.Accounts, p.accountName, store.Users, store.JwtName(p.username)}
   285  	if ts.Has(userFields...) {
   286  		r.AddWarning("the user %q already exists", p.username)
   287  		creds := sctx.KeyStore.CalcUserCredsPath(p.accountName, p.username)
   288  		if _, err := os.Stat(creds); os.IsNotExist(err) {
   289  			r.AddFromError(fmt.Errorf("user %q credentials not found at %q", p.username, creds))
   290  			return err
   291  		} else {
   292  			p.userCreds = creds
   293  		}
   294  		return nil
   295  	}
   296  
   297  	kp, err := nkeys.CreatePair(nkeys.PrefixByteUser)
   298  	if err != nil {
   299  		return err
   300  	}
   301  	pub, err := kp.PublicKey()
   302  	if err != nil {
   303  		return err
   304  	}
   305  	uc := jwt.NewUserClaims(pub)
   306  	uc.Name = p.username
   307  	uc.SetScoped(signerKeyIsScoped(ctx, p.accountName, p.accountKeyPair))
   308  	userJWT, err := uc.Encode(p.accountKeyPair)
   309  	if err != nil {
   310  		return err
   311  	}
   312  	st, err := ts.StoreClaim([]byte(userJWT))
   313  	if st != nil {
   314  		r.Add(st)
   315  	}
   316  	if err != nil {
   317  		p.r.AddFromError(err)
   318  		return err
   319  	}
   320  	_, err = sctx.KeyStore.Store(kp)
   321  	if err != nil {
   322  		return err
   323  	}
   324  	r.AddOK("generated and stored user key %q", uc.Subject)
   325  
   326  	// Store user credentials.
   327  	userSeed, err := kp.Seed()
   328  	if err != nil {
   329  		return err
   330  	}
   331  	creds, err := jwt.FormatUserConfig(userJWT, userSeed)
   332  	if err != nil {
   333  		return err
   334  	}
   335  	ks := sctx.KeyStore
   336  	path, err := ks.MaybeStoreUserCreds(p.accountName, p.username, creds)
   337  	if err != nil {
   338  		return err
   339  	}
   340  	p.userCreds = path
   341  	r.AddOK("generated and stored user credentials at %q", path)
   342  	return nil
   343  }
   344  
   345  func (p *LoadParams) configureNSCEnv() error {
   346  	// Change the current context to the one from load.
   347  	current := GetConfig()
   348  
   349  	if err := current.ContextConfig.Update(current.StoreRoot, p.operatorName, p.accountName); err != nil {
   350  		return err
   351  	}
   352  	if err := current.Save(); err != nil {
   353  		return err
   354  	}
   355  	return nil
   356  }
   357  
   358  func (p *LoadParams) configureNATSCLI() error {
   359  	p.contextName = fmt.Sprintf("%s_%s_%s", p.operatorName, p.accountName, p.username)
   360  
   361  	// Replace this to use instead use the natscontext library.
   362  	var servers string
   363  	if len(p.operator.OperatorServiceURLs) > 0 {
   364  		servers = strings.Join(p.operator.OperatorServiceURLs, ",")
   365  	}
   366  	// Finally, store the NATS context used by the NATS CLI.
   367  	config, err := natscontext.New(p.contextName, false,
   368  		natscontext.WithServerURL(servers),
   369  		natscontext.WithCreds(p.userCreds),
   370  		natscontext.WithDescription(fmt.Sprintf("%s (%s)", p.operatorName, p.operator.Name)),
   371  	)
   372  	if err != nil {
   373  		return err
   374  	}
   375  	config.Save(p.contextName)
   376  
   377  	// Switch to use that context as well.
   378  	err = natscontext.SelectContext(p.contextName)
   379  	if err != nil {
   380  		return err
   381  	}
   382  	return nil
   383  }
   384  
   385  // Run executes the load profile command.
   386  func (p *LoadParams) Run(ctx ActionCtx) (store.Status, error) {
   387  	r := store.NewDetailedReport(false)
   388  	p.r = r
   389  	if err := p.configureNSCEnv(); err != nil {
   390  		return nil, err
   391  	}
   392  	if err := p.addAccount(ctx); err != nil {
   393  		return nil, err
   394  	}
   395  	if err := p.addUser(ctx); err != nil {
   396  		return nil, err
   397  	}
   398  	if err := p.configureNATSCLI(); err != nil {
   399  		return nil, err
   400  	}
   401  	r.AddOK("created nats context %q", p.contextName)
   402  	return r, nil
   403  }
   404  
   405  func (p *LoadParams) SetDefaults(ctx ActionCtx) error     { return nil }
   406  func (p *LoadParams) PreInteractive(ctx ActionCtx) error  { return nil }
   407  func (p *LoadParams) Load(ctx ActionCtx) error            { return nil }
   408  func (p *LoadParams) PostInteractive(ctx ActionCtx) error { return nil }
   409  func (p *LoadParams) Validate(ctx ActionCtx) error        { return nil }