github.com/nats-io/nsc/v2@v2.8.7-0.20240307184528-efd7023c6896/cmd/generateactivation.go (about) 1 /* 2 * Copyright 2018-2022 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 "net/url" 22 "path" 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 createGenerateActivationCmd() *cobra.Command { 32 var params GenerateActivationParams 33 cmd := &cobra.Command{ 34 Use: "activation", 35 Short: "Generate an export activation jwt token", 36 Args: MaxArgs(0), 37 SilenceUsage: true, 38 RunE: func(cmd *cobra.Command, args []string) error { 39 return RunAction(cmd, args, ¶ms) 40 }, 41 } 42 cmd.Flags().StringVarP(¶ms.subject, "subject", "s", "", "export subject") 43 cmd.Flags().StringVarP(¶ms.out, "output-file", "o", "--", "output file '--' is stdout") 44 cmd.Flags().BoolVarP(¶ms.push, "push", "", false, "push activation token to operator's account server (exclusive of output-file") 45 params.accountKey.BindFlags("target-account", "t", nkeys.PrefixByteAccount, cmd) 46 params.timeParams.BindFlags(cmd) 47 params.AccountContextParams.BindFlags(cmd) 48 49 return cmd 50 } 51 52 func init() { 53 generateCmd.AddCommand(createGenerateActivationCmd()) 54 } 55 56 type GenerateActivationParams struct { 57 AccountContextParams 58 SignerParams 59 accountKey PubKeyParams 60 activation *jwt.ActivationClaims 61 claims *jwt.AccountClaims 62 export jwt.Export 63 privateExports []AccountExportChoice 64 timeParams TimeParams 65 token string 66 out string 67 push bool 68 subject string 69 } 70 71 func (p *GenerateActivationParams) SetDefaults(ctx ActionCtx) error { 72 p.AccountContextParams.SetDefaults(ctx) 73 if err := p.accountKey.SetDefaults(ctx); err != nil { 74 return err 75 } 76 p.SignerParams.SetDefaults(nkeys.PrefixByteAccount, false, ctx) 77 return nil 78 } 79 80 func (p *GenerateActivationParams) PreInteractive(ctx ActionCtx) error { 81 var err error 82 if err = p.AccountContextParams.Edit(ctx); err != nil { 83 return err 84 } 85 return nil 86 } 87 88 func (p *GenerateActivationParams) Load(ctx ActionCtx) error { 89 var err error 90 if err = p.AccountContextParams.Validate(ctx); err != nil { 91 return err 92 } 93 94 p.claims, err = ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name) 95 if err != nil { 96 return err 97 } 98 99 if len(p.claims.Exports) == 0 { 100 return fmt.Errorf("account %q doesn't have exports", p.AccountContextParams.Name) 101 } 102 103 choices, err := GetAccountExports(p.claims) 104 if err != nil { 105 return err 106 } 107 for _, e := range choices { 108 if e.Selection.TokenReq { 109 p.privateExports = append(p.privateExports, e) 110 } 111 } 112 if len(p.privateExports) == 0 { 113 return fmt.Errorf("account %q doesn't have exports that require an activation token", p.AccountContextParams.Name) 114 } 115 116 if len(p.privateExports) == 1 && p.subject == "" { 117 p.subject = string(p.privateExports[0].Selection.Subject) 118 } 119 120 return nil 121 } 122 123 func (p *GenerateActivationParams) PostInteractive(ctx ActionCtx) error { 124 var err error 125 126 labels := AccountExportChoices(p.privateExports).String() 127 choice := "" 128 if len(labels) == 1 { 129 choice = labels[0] 130 } 131 i, err := cli.Select("select the export", choice, labels) 132 if err != nil { 133 return err 134 } 135 136 p.export = *p.privateExports[i].Selection 137 if p.subject == "" { 138 p.subject = string(p.export.Subject) 139 } 140 141 p.subject, err = cli.Prompt("subject", p.subject, cli.Val(func(v string) error { 142 t := jwt.Subject(v) 143 var vr jwt.ValidationResults 144 t.Validate(&vr) 145 if len(vr.Issues) > 0 { 146 return errors.New(vr.Issues[0].Description) 147 } 148 if t != p.export.Subject && !t.IsContainedIn(p.export.Subject) { 149 return fmt.Errorf("%q doesn't contain %q", string(p.export.Subject), string(t)) 150 } 151 return nil 152 })) 153 if err != nil { 154 return err 155 } 156 157 if err = p.accountKey.Edit(); err != nil { 158 return err 159 } 160 if err := p.timeParams.Edit(); err != nil { 161 return err 162 } 163 if err = p.SignerParams.Edit(ctx); err != nil { 164 return err 165 } 166 167 oc, err := ctx.StoreCtx().Store.ReadOperatorClaim() 168 if err != nil { 169 return err 170 } 171 if oc.AccountServerURL != "" && IsAccountServerURL(oc.AccountServerURL) { 172 m := fmt.Sprintf("push the activation to %q", oc.AccountServerURL) 173 p.push, err = cli.Confirm(m, false) 174 if err != nil { 175 return err 176 } 177 } 178 179 return nil 180 } 181 182 func (p *GenerateActivationParams) Validate(ctx ActionCtx) error { 183 var err error 184 185 if p.subject == "" { 186 ctx.CurrentCmd().SilenceUsage = false 187 return errors.New("a subject is required") 188 } 189 190 if err = p.timeParams.Validate(); err != nil { 191 return err 192 } 193 194 if err = p.accountKey.Valid(); err != nil { 195 return err 196 } 197 198 // validate the raw subject 199 sub := jwt.Subject(p.subject) 200 var vr jwt.ValidationResults 201 sub.Validate(&vr) 202 if len(vr.Issues) > 0 { 203 return errors.New(vr.Issues[0].Description) 204 } 205 206 for _, e := range p.privateExports { 207 if sub.IsContainedIn(e.Selection.Subject) { 208 p.export = *e.Selection 209 break 210 } 211 } 212 213 if p.export.Subject == "" { 214 return fmt.Errorf("a private export for %q was not found in account %q", p.subject, p.AccountContextParams.Name) 215 } 216 217 if err = p.SignerParams.Resolve(ctx); err != nil { 218 return err 219 } 220 return nil 221 } 222 223 func (p *GenerateActivationParams) Run(ctx ActionCtx) (store.Status, error) { 224 var err error 225 p.activation = jwt.NewActivationClaims(p.accountKey.publicKey) 226 p.activation.NotBefore, _ = p.timeParams.StartDate() 227 p.activation.Expires, _ = p.timeParams.ExpiryDate() 228 p.activation.Name = p.subject 229 // p.subject is subset of the export 230 p.activation.Activation.ImportSubject = jwt.Subject(p.subject) 231 p.activation.Activation.ImportType = p.export.Type 232 233 spub, err := p.signerKP.PublicKey() 234 if err != nil { 235 return nil, err 236 } 237 if p.claims.Subject != spub { 238 p.activation.IssuerAccount = p.claims.Subject 239 } 240 241 p.token, err = p.activation.Encode(p.signerKP) 242 if err != nil { 243 return nil, err 244 } 245 246 d, err := jwt.DecorateJWT(p.token) 247 if err != nil { 248 return nil, err 249 } 250 r := store.NewDetailedReport(true) 251 r.AddOK("generated %q activation for account %q", p.export.Name, p.accountKey.publicKey) 252 if p.activation.NotBefore > 0 { 253 r.AddOK("token valid %s - %s", UnixToDate(p.activation.NotBefore), HumanizedDate(p.activation.NotBefore)) 254 } 255 if p.activation.Expires > 0 { 256 r.AddOK("token expires %s - %s", UnixToDate(p.activation.Expires), HumanizedDate(p.activation.Expires)) 257 } 258 259 // if some command embeds, the output will be blank 260 // in that case don't generate the output 261 if p.out != "" { 262 if err := Write(p.out, d); err != nil { 263 return nil, err 264 } 265 if !IsStdOut(p.out) { 266 r.AddOK("wrote activation token to %#q", AbbrevHomePaths(p.out)) 267 } 268 } 269 270 if p.push { 271 oc, err := ctx.StoreCtx().Store.ReadOperatorClaim() 272 if err != nil { 273 return nil, err 274 } 275 if oc.AccountServerURL == "" { 276 return nil, fmt.Errorf("operator %s doesn't have an account server url configured", oc.Name) 277 } else if IsResolverURL(oc.AccountServerURL) { 278 return nil, fmt.Errorf("activation push is only supported for http base account server not nats-resover enabled nats-server") 279 } 280 u, err := url.Parse(oc.AccountServerURL) 281 if err != nil { 282 return nil, err 283 } 284 u.Path = path.Join(u.Path, "activations") 285 s, err := store.PushAccount(u.String(), []byte(p.token)) 286 if s != nil { 287 r.Add(s) 288 } 289 if err != nil { 290 return s, err 291 } 292 293 hid, err := p.activation.HashID() 294 if err != nil { 295 r.AddError("error calculating activation hash id: %v", err) 296 return r, err 297 } 298 299 gu, err := url.Parse(oc.AccountServerURL) 300 if err != nil { 301 return nil, err 302 } 303 304 u.Path = path.Join(gu.Path, "activations", hid) 305 r.AddOK("activation accessible at %q", u.String()) 306 } 307 308 return r, nil 309 } 310 311 func (p *GenerateActivationParams) Token() string { 312 return p.token 313 }