github.com/nats-io/nsc/v2@v2.8.7-0.20240307184528-efd7023c6896/cmd/importuser.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  	"os"
    20  	"strings"
    21  
    22  	"github.com/nats-io/jwt/v2"
    23  	"github.com/nats-io/nkeys"
    24  	"github.com/spf13/cobra"
    25  
    26  	"github.com/nats-io/nsc/v2/cmd/store"
    27  )
    28  
    29  func createImportUserCmd() *cobra.Command {
    30  	var params ImportUser
    31  	cmd := &cobra.Command{
    32  		Use:          "user --file <user-jwt/user-creds>",
    33  		Short:        "Imports an user from a jwt or user and nkey from a creds file",
    34  		Example:      `nsc import user --file <account-jwt>`,
    35  		Args:         MaxArgs(0),
    36  		SilenceUsage: false,
    37  		RunE: func(cmd *cobra.Command, args []string) error {
    38  			if err := RunMaybeStorelessAction(cmd, args, &params); err != nil {
    39  				return err
    40  			}
    41  			return nil
    42  		},
    43  	}
    44  	cmd.Flags().BoolVarP(&params.skip, "skip", "", false, "skip validation issues")
    45  	cmd.Flags().BoolVarP(&params.overwrite, "overwrite", "", false, "overwrite existing user")
    46  	cmd.Flags().StringVarP(&params.file, "file", "f", "", "user jwt or creds to import")
    47  	return cmd
    48  }
    49  
    50  func init() {
    51  	importCmd.AddCommand(createImportUserCmd())
    52  }
    53  
    54  type ImportUser struct {
    55  	fileImport
    56  }
    57  
    58  func (p *ImportUser) SetDefaults(_ctx ActionCtx) error {
    59  	return nil
    60  }
    61  
    62  func (p *ImportUser) Validate(ctx ActionCtx) error {
    63  	return p.fileImport.Validate(ctx, ".jwt", ".creds")
    64  }
    65  
    66  // returns true when blocking and not skipped
    67  func validateAndReport(claim jwt.Claims, force bool, r *store.Report) bool {
    68  	vr := jwt.ValidationResults{}
    69  	claim.Validate(&vr)
    70  	for _, i := range vr.Issues {
    71  		if i.Blocking {
    72  			r.AddError("validation resulted in: %s", i.Description)
    73  		} else {
    74  			r.AddWarning("validation resulted in: %s", i.Description)
    75  		}
    76  	}
    77  	if vr.IsBlocking(true) && !force {
    78  		r.AddError("validation is blocking, skip with --skip")
    79  		return true
    80  	}
    81  	return false
    82  }
    83  
    84  func (p *ImportUser) Run(ctx ActionCtx) (store.Status, error) {
    85  	r := store.NewDetailedReport(true)
    86  	content, err := os.ReadFile(p.file)
    87  	if err != nil {
    88  		r.AddError("failed to import %#q: %v", p.file, err)
    89  		return r, err
    90  	}
    91  	theJWT := strings.TrimSpace(string(content))
    92  	var kp nkeys.KeyPair
    93  	if !strings.HasPrefix(theJWT, "ey") {
    94  		theJWT, err = jwt.ParseDecoratedJWT(content)
    95  		if err != nil {
    96  			r.AddError("failed to parse decorated JWT in %q: %v", p.file, err)
    97  			return r, err
    98  		}
    99  		if kp, err = jwt.ParseDecoratedUserNKey(content); err != nil {
   100  			r.AddWarning("failed to parse decorated key in %#q: %v", p.file, err)
   101  		}
   102  	}
   103  	claim, err := jwt.DecodeUserClaims(theJWT)
   104  	if err != nil {
   105  		r.AddError("failed to decode %#q: %v", p.file, err)
   106  		return r, err
   107  	}
   108  	if validateAndReport(claim, p.skip, r) {
   109  		return r, err
   110  	}
   111  	acc := claim.IssuerAccount
   112  	if acc == "" {
   113  		acc = claim.Issuer
   114  	}
   115  	var accClaim *jwt.AccountClaims
   116  	accs, _ := ctx.StoreCtx().Store.ListSubContainers(store.Accounts)
   117  	for _, accName := range accs {
   118  		accClaim, _ = ctx.StoreCtx().Store.ReadAccountClaim(accName)
   119  		if accClaim.Subject != acc {
   120  			accClaim = nil
   121  		} else {
   122  			break
   123  		}
   124  	}
   125  	if accClaim == nil {
   126  		r.AddError("referenced Account %s not found, import first", acc)
   127  		return r, nil
   128  	}
   129  	if ctx.StoreCtx().Store.Has(store.Accounts, accClaim.Name, store.Users, store.JwtName(claim.Name)) {
   130  		if !p.overwrite {
   131  			r.AddError("user already exists, overwrite with --overwrite")
   132  			return r, nil
   133  		}
   134  		if old, err := ctx.StoreCtx().Store.ReadUserClaim(accClaim.Name, claim.Name); err != nil {
   135  			r.AddError("existing User not found: %v", err)
   136  			return r, nil
   137  		} else if old.Subject != claim.Subject {
   138  			r.AddError("existing User has a name collision only and does not reference the same entity "+
   139  				"(Subject is %s and differs from %s). This problem needs to be resolved manually.",
   140  				old.Subject, claim.Subject)
   141  			return r, nil
   142  		}
   143  	}
   144  	sameAccount := false
   145  	keys := []string{accClaim.Subject}
   146  	keys = append(keys, accClaim.SigningKeys.Keys()...)
   147  	for _, key := range keys {
   148  		if key == claim.Issuer {
   149  			sameAccount = true
   150  			break
   151  		}
   152  	}
   153  	if !sameAccount {
   154  		r.AddError("can only import user signed by exiting account. Possibly update your account")
   155  		return r, nil
   156  	}
   157  	if kp != nil {
   158  		keyPath, err := ctx.StoreCtx().KeyStore.Store(kp)
   159  		if err != nil {
   160  			r.AddError("key could not be stored: %v", err)
   161  			return r, nil
   162  		}
   163  		r.AddOK("key stored %s", keyPath)
   164  	}
   165  	sub, err := ctx.StoreCtx().Store.StoreClaim([]byte(theJWT))
   166  	if err != nil {
   167  		r.AddError("Error when storing user: %v", err)
   168  	}
   169  	r.Add(sub)
   170  	if r.HasNoErrors() {
   171  		r.AddOK("user %s was successfully imported", claim.Name)
   172  	} else {
   173  		r.AddOK("user %s was not imported: %v", claim.Name, err)
   174  	}
   175  	return r, nil
   176  }