Repository: nats-io/nsc
Pull request #295 (2020-09-09 20:35:15)

Generate system account and user on init and add operator
Named SYS and sys

Signed-off-by: Matthias Hanel <mh@synadia.com>


```
16:32:01  ~/repos/nsc  system_account *9 ?1  nsc init
? enter a configuration directory /Users/matthiashanel/nsc-test
? Select an operator Create Operator
? name your operator, account and user crazy_brahmagupta
[ OK ] created operator crazy_brahmagupta
[ OK ] created system_account: name:SYS id:ADLPUC4CEHJY2NLBV3ILXYGVHR42ZVHCX7Q2OEMCFCNYUNPFETXI2DAG
[ OK ] created system account user: name:sys id:UALD37FFZD7HNO7IKBW7KOGAW6BONFQAWKQI54UAHNUUBT4QNSGPUG4F
[ OK ] system account user creds file stored in `~/.nkeys/creds/crazy_brahmagupta/SYS/sys.creds`
[ OK ] created account crazy_brahmagupta
[ OK ] created user "crazy_brahmagupta"
[ OK ] project jwt files created in `~/nsc-test`
[ OK ] user creds file stored in `~/.nkeys/creds/crazy_brahmagupta/crazy_brahmagupta/crazy_brahmagupta.creds`
> to run a local server using this configuration, enter:
>   nsc generate config --mem-resolver --config-file <path/server.conf>
> then start a nats-server using the generated config:
>   nats-server -c <path/server.conf>
all jobs succeeded
 16:32:15  ~/repos/nsc  system_account *9 ?1  nsc describe operator crazy_brahmagupta
╭──────────────────────────────────────────────────────────────────────────────────╮
│                                 Operator Details                                 │
├───────────────────────┬──────────────────────────────────────────────────────────┤
│ Name                  │ crazy_brahmagupta                                        │
│ Operator ID           │ ODI6CWZB53RMWHBXHPGOW6VALIFE7B3YPKPTNF3MEF3OTTTCZXFG2WN5 │
│ Issuer ID             │ ODI6CWZB53RMWHBXHPGOW6VALIFE7B3YPKPTNF3MEF3OTTTCZXFG2WN5 │
│ Issued                │ 2020-09-09 20:32:05 UTC                                  │
│ Expires               │                                                          │
│ Operator Service URLs │ nats://localhost:4222                                    │
│ System Account        │ ADLPUC4CEHJY2NLBV3ILXYGVHR42ZVHCX7Q2OEMCFCNYUNPFETXI2DAG │
╰───────────────────────┴──────────────────────────────────────────────────────────╯
 16:32:53  ✘ 1  ~/repos/nsc  system_account *9 ?1  nsc add operator -n test3 -s
[ OK ] generated and stored operator key "ODIIETQPK7RV3IQK3675Y553RZ7ST2PU4ICPYPIOVGAA5GHT7R7RVOWM"
[ OK ] added operator "test3"
[ OK ] created system_account: name:SYS id:ABBYA7BW3OCTRL76JFP4BRWIRB2G53XUAHTK5JG4VW34VAB4HOZGFND7
[ OK ] created system account user: name:sys id:UADSO5EEDMUNTAQIAHOSCDZM7GDREGXYDQBRMJVKGJXSTSSKBP2G4CEK
[ OK ] system account user creds file stored in `~/.nkeys/creds/test3/SYS/sys.creds`
 16:32:58  ~/repos/nsc  system_account *9 ?1  nsc describe operator test3
╭───────────────────────────────────────────────────────────────────────────╮
│                             Operator Details                              │
├────────────────┬──────────────────────────────────────────────────────────┤
│ Name           │ test3                                                    │
│ Operator ID    │ ODIIETQPK7RV3IQK3675Y553RZ7ST2PU4ICPYPIOVGAA5GHT7R7RVOWM │
│ Issuer ID      │ ODIIETQPK7RV3IQK3675Y553RZ7ST2PU4ICPYPIOVGAA5GHT7R7RVOWM │
│ Issued         │ 2020-09-09 20:32:53 UTC                                  │
│ Expires        │                                                          │
│ System Account │ ABBYA7BW3OCTRL76JFP4BRWIRB2G53XUAHTK5JG4VW34VAB4HOZGFND7 │
╰────────────────┴──────────────────────────────────────────────────────────╯
 16:32:59  ~/repos/nsc  system_account *9 ?1 
```


Patch:
diff --git a/cmd/addoperator.go b/cmd/addoperator.go
index 3e37f08..9acb3e6 100644
--- a/cmd/addoperator.go
+++ b/cmd/addoperator.go
@@ -44,6 +44,7 @@ func createAddOperatorCmd() *cobra.Command {
 	}
 	cmd.Flags().StringVarP(&params.name, "name", "n", "", "operator name")
 	cmd.Flags().StringVarP(&params.jwtPath, "url", "u", "", "import from a jwt server url, file, or well known operator")
+	cmd.Flags().BoolVarP(&params.sysAcc, "sys", "s", false, "generate system account with the operator")
 	params.TimeParams.BindFlags(cmd)
 
 	return cmd
@@ -60,6 +61,7 @@ type AddOperatorParams struct {
 	token    string
 	name     string
 	generate bool
+	sysAcc   bool
 	keyPath  string
 }
 
@@ -83,6 +85,7 @@ func (p *AddOperatorParams) PreInteractive(ctx ActionCtx) error {
 		return err
 	}
 	if ok {
+		p.sysAcc = false
 		p.jwtPath, err = cli.Prompt("path or url for operator jwt", p.jwtPath, cli.Val(func(v string) error {
 			// is it is an URL or path
 			pv := cli.PathOrURLValidator()
@@ -109,6 +112,9 @@ func (p *AddOperatorParams) PreInteractive(ctx ActionCtx) error {
 		if err = p.TimeParams.Edit(); err != nil {
 			return err
 		}
+		if p.sysAcc, err = cli.Confirm("Generate system account?", true); err != nil {
+			return err
+		}
 	}
 
 	return nil
@@ -239,6 +245,10 @@ func (p *AddOperatorParams) Validate(ctx ActionCtx) error {
 		}
 	}
 
+	if p.sysAcc && p.signerKP == nil {
+		return fmt.Errorf("generating system account requires a key")
+	}
+
 	if err := p.Resolve(ctx); err != nil {
 		return err
 	}
@@ -253,6 +263,9 @@ func (p *AddOperatorParams) Run(_ ActionCtx) (store.Status, error) {
 		return nil, err
 	}
 
+	var sAcc *keys
+	var sUsr *keys
+
 	if p.token == "" {
 		ctx, err := s.GetContext()
 		if err != nil {
@@ -265,28 +278,35 @@ func (p *AddOperatorParams) Run(_ ActionCtx) (store.Status, error) {
 			}
 		}
 
-		if p.Start != "" || p.Expiry != "" {
-			oc, err := ctx.Store.ReadOperatorClaim()
+		oc, err := ctx.Store.ReadOperatorClaim()
+		if err != nil {
+			return nil, err
+		}
+		if p.Start != "" {
+			oc.NotBefore, err = p.TimeParams.StartDate()
 			if err != nil {
 				return nil, err
 			}
-			if p.Start != "" {
-				oc.NotBefore, err = p.TimeParams.StartDate()
-				if err != nil {
-					return nil, err
-				}
-			}
-			if p.Expiry != "" {
-				oc.Expires, err = p.TimeParams.ExpiryDate()
-				if err != nil {
-					return nil, err
-				}
-			}
-			p.token, err = oc.Encode(p.signerKP)
+		}
+		if p.Expiry != "" {
+			oc.Expires, err = p.TimeParams.ExpiryDate()
 			if err != nil {
 				return nil, err
 			}
 		}
+
+		if p.sysAcc {
+			if sAcc, sUsr, err = createSystemAccount(ctx, p.signerKP); err != nil {
+				return nil, err
+			}
+			oc.SystemAccount = sAcc.PubKey
+		}
+
+		p.token, err = oc.Encode(p.signerKP)
+		if err != nil {
+			return nil, err
+		}
+
 	}
 
 	r := store.NewDetailedReport(false)
@@ -308,6 +328,11 @@ func (p *AddOperatorParams) Run(_ ActionCtx) (store.Status, error) {
 			verb = "imported"
 		}
 		r.AddOK("%s operator %q", verb, p.name)
+		if sAcc != nil && sUsr != nil {
+			r.AddOK("created system_account: name:SYS id:%s", sAcc.PubKey)
+			r.AddOK("created system account user: name:sys id:%s", sUsr.PubKey)
+			r.AddOK("system account user creds file stored in %#q", AbbrevHomePaths(sUsr.CredsPath))
+		}
 	}
 	return r, err
 }
diff --git a/cmd/describer.go b/cmd/describer.go
index ef20f01..8fccc28 100644
--- a/cmd/describer.go
+++ b/cmd/describer.go
@@ -425,6 +425,10 @@ func (o *OperatorDescriber) Describe() string {
 
 	AddListValues(table, "Operator Service URLs", o.OperatorServiceURLs)
 
+	if o.SystemAccount != "" {
+		table.AddRow("System Account", o.SystemAccount)
+	}
+
 	if len(o.Identities) > 0 {
 		table.AddSeparator()
 		for _, v := range o.Identities {
diff --git a/cmd/init.go b/cmd/init.go
index 2903275..2bf4010 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -75,6 +75,8 @@ type InitCmdParams struct {
 	ManagedOperatorName string
 	CreateOperator      bool
 	Operator            keys
+	SystemAccount       keys
+	SystemUser          keys
 	Account             keys
 	User                keys
 	OperatorJwtURL      string
@@ -299,13 +301,67 @@ func (p *InitCmdParams) createStore(cmd *cobra.Command) error {
 	return GetConfig().Save()
 }
 
+func createSystemAccount(s *store.Context, opKp nkeys.KeyPair) (*keys, *keys, error) {
+	var acc keys
+	var usr keys
+	var err error
+	// create system account, signed by this operator
+	if acc.KP, err = nkeys.CreateAccount(); err != nil {
+		return nil, nil, err
+	} else if acc.PubKey, err = acc.KP.PublicKey(); err != nil {
+		return nil, nil, err
+	}
+	sysAccClaim := jwt.NewAccountClaims(acc.PubKey)
+	sysAccClaim.Name = "SYS"
+	if sysAccJwt, err := sysAccClaim.Encode(opKp); err != nil {
+		return nil, nil, err
+	} else if _, err := s.Store.StoreClaim([]byte(sysAccJwt)); err != nil {
+		return nil, nil, err
+	} else if acc.KeyPath, err = s.KeyStore.Store(acc.KP); err != nil {
+		return nil, nil, err
+	}
+	// create system account user and creds
+	if usr.KP, err = nkeys.CreateUser(); err != nil {
+		return nil, nil, err
+	} else if usr.PubKey, err = usr.KP.PublicKey(); err != nil {
+		return nil, nil, err
+	}
+	sysUsrClaim := jwt.NewUserClaims(usr.PubKey)
+	sysUsrClaim.Name = "sys"
+	if sysUsrJwt, err := sysUsrClaim.Encode(acc.KP); err != nil {
+		return nil, nil, err
+	} else if _, err := s.Store.StoreClaim([]byte(sysUsrJwt)); err != nil {
+		return nil, nil, err
+	} else if usr.KeyPath, err = s.KeyStore.Store(usr.KP); err != nil {
+		return nil, nil, err
+	}
+	config, err := GenerateConfig(s.Store, sysAccClaim.Name, sysUsrClaim.Name, usr.KP)
+	if err != nil {
+		return nil, nil, err
+	}
+	if usr.CredsPath, err = s.KeyStore.MaybeStoreUserCreds(sysAccClaim.Name, sysUsrClaim.Name, config); err != nil {
+		return nil, nil, err
+	}
+	return &acc, &usr, nil
+}
+
 func (p *InitCmdParams) setOperatorDefaults(ctx ActionCtx) error {
+	ctx.StoreCtx()
 	if p.CreateOperator {
+		if acc, usr, err := createSystemAccount(ctx.StoreCtx(), p.Operator.KP); err != nil {
+			return err
+		} else {
+			p.SystemAccount = *acc
+			p.SystemUser = *usr
+		}
+
+		// read/create operator
 		oc, err := ctx.StoreCtx().Store.ReadOperatorClaim()
 		if err != nil {
 			return err
 		}
 		oc.OperatorServiceURLs.Add("nats://localhost:4222")
+		oc.SystemAccount = p.SystemAccount.PubKey
 		token, err := oc.Encode(p.Operator.KP)
 		if err != nil {
 			return err
@@ -396,6 +452,9 @@ func (p *InitCmdParams) Run(ctx ActionCtx) (store.Status, error) {
 			return nil, err
 		}
 		r.AddOK("created operator %s", p.Name)
+		r.AddOK("created system_account: name:SYS id:%s", p.SystemAccount.PubKey)
+		r.AddOK("created system account user: name:sys id:%s", p.SystemUser.PubKey)
+		r.AddOK("system account user creds file stored in %#q", AbbrevHomePaths(p.SystemUser.CredsPath))
 	} else {
 		r.AddOK("add managed operator %s", GetConfig().Operator)
 	}


Test patch:
diff --git a/cmd/addoperator_test.go b/cmd/addoperator_test.go
index 4b0d4a7..b41a51c 100644
--- a/cmd/addoperator_test.go
+++ b/cmd/addoperator_test.go
@@ -40,11 +40,14 @@ func Test_AddOperator(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	_, _, err = ExecuteCmd(createAddOperatorCmd(), "--name", "O")
+	_, _, err = ExecuteCmd(createAddOperatorCmd(), "--name", "O", "--sys")
 	require.NoError(t, err)
 
 	require.FileExists(t, filepath.Join(ts.Dir, "store", "O", ".nsc"))
 	require.FileExists(t, filepath.Join(ts.Dir, "store", "O", "O.jwt"))
+
+	require.FileExists(t, filepath.Join(ts.Dir, "store", "O", "accounts", "SYS", "SYS.jwt"))
+	require.FileExists(t, filepath.Join(ts.Dir, "store", "O", "accounts", "SYS", "users", "sys.jwt"))
 }
 
 func TestImportOperator(t *testing.T) {
@@ -83,7 +86,7 @@ func TestAddOperatorInteractive(t *testing.T) {
 	ts := NewEmptyStore(t)
 	defer ts.Done(t)
 
-	_, _, err := ExecuteInteractiveCmd(createAddOperatorCmd(), []interface{}{false, "O", "2019-12-01", "2029-12-01", true})
+	_, _, err := ExecuteInteractiveCmd(createAddOperatorCmd(), []interface{}{false, "O", "2019-12-01", "2029-12-01", true, true})
 	require.NoError(t, err)
 	d, err := Read(filepath.Join(ts.Dir, "store", "O", "O.jwt"))
 	require.NoError(t, err)
@@ -99,6 +102,10 @@ func TestAddOperatorInteractive(t *testing.T) {
 	require.Equal(t, 2029, expiry.Year())
 	require.Equal(t, time.Month(12), expiry.Month())
 	require.Equal(t, 1, expiry.Day())
+	require.NotEmpty(t, oc.SystemAccount)
+
+	require.FileExists(t, filepath.Join(ts.Dir, "store", "O", "accounts", "SYS", "SYS.jwt"))
+	require.FileExists(t, filepath.Join(ts.Dir, "store", "O", "accounts", "SYS", "users", "sys.jwt"))
 }
 
 func TestImportOperatorInteractive(t *testing.T) {
@@ -178,7 +185,7 @@ func Test_AddOperatorWithKeyInteractive(t *testing.T) {
 	cmd := createAddOperatorCmd()
 	HoistRootFlags(cmd)
 
-	args := []interface{}{false, "T", "0", "0", false, string(seed)}
+	args := []interface{}{false, "T", "0", "0", false, false, string(seed)}
 	_, _, err := ExecuteInteractiveCmd(cmd, args)
 	require.NoError(t, err)
 
diff --git a/cmd/init_test.go b/cmd/init_test.go
index 9dcb313..f9d62ae 100644
--- a/cmd/init_test.go
+++ b/cmd/init_test.go
@@ -33,6 +33,8 @@ func Test_InitLocal(t *testing.T) {
 	ts.VerifyOperator(t, "O", false)
 	ts.VerifyAccount(t, "O", "O", true)
 	ts.VerifyUser(t, "O", "O", "O", true)
+	ts.VerifyAccount(t, "O", "SYS", true)
+	ts.VerifyUser(t, "O", "SYS", "sys", true)
 }
 
 func Test_InitExists(t *testing.T) {
@@ -217,6 +219,8 @@ func Test_InitLocalInteractive(t *testing.T) {
 	ts.VerifyOperator(t, "O", false)
 	ts.VerifyAccount(t, "O", "O", true)
 	ts.VerifyUser(t, "O", "O", "O", true)
+	ts.VerifyAccount(t, "O", "SYS", true)
+	ts.VerifyUser(t, "O", "SYS", "sys", true)
 }
 
 func Test_InitCustomInteractive(t *testing.T) {