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, ¶ms); err != nil { 39 return err 40 } 41 return nil 42 }, 43 } 44 cmd.Flags().BoolVarP(¶ms.skip, "skip", "", false, "skip validation issues") 45 cmd.Flags().BoolVarP(¶ms.overwrite, "overwrite", "", false, "overwrite existing user") 46 cmd.Flags().StringVarP(¶ms.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 }