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, &params)
    40  		},
    41  	}
    42  	cmd.Flags().StringVarP(&params.subject, "subject", "s", "", "export subject")
    43  	cmd.Flags().StringVarP(&params.out, "output-file", "o", "--", "output file '--' is stdout")
    44  	cmd.Flags().BoolVarP(&params.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  }