diff --git a/cmd/common.go b/cmd/common.go index 042a08e4..e5668755 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -458,6 +458,14 @@ func IsNatsUrl(url string) bool { return store.IsNatsUrl(url) } +func IsAccountServerURL(u string) bool { + return store.IsAccountServerURL(u) +} + +func IsResolverURL(u string) bool { + return store.IsResolverURL(u) +} + func ValidSigner(kp nkeys.KeyPair, signers []string) (bool, error) { pk, err := kp.PublicKey() if err != nil { diff --git a/cmd/editoperator.go b/cmd/editoperator.go index 233c000d..b67467a8 100644 --- a/cmd/editoperator.go +++ b/cmd/editoperator.go @@ -41,10 +41,10 @@ func createEditOperatorCmd() *cobra.Command { cmd.Flags().StringSliceVarP(¶ms.rmSigningKeys, "rm-sk", "", nil, "remove signing key - comma separated list or option can be specified multiple times") cmd.Flags().StringSliceVarP(¶ms.tags, "tag", "", nil, "add tags for user - comma separated list or option can be specified multiple times") cmd.Flags().StringSliceVarP(¶ms.rmTags, "rm-tag", "", nil, "remove tag - comma separated list or option can be specified multiple times") - cmd.Flags().StringVarP(¶ms.asu, "account-jwt-server-url", "u", "", "set account jwt server url for nsc sync (only http/https/nats urls supported if updating with nsc)") + cmd.Flags().StringVarP(¶ms.asu, "account-jwt-server-url", "u", "", "set account jwt server url for nsc sync (only http/https or nats service (nats/tls/ws/wss) urls supported if updating with nsc)") cmd.Flags().StringVarP(¶ms.sysAcc, "system-account", "", "", "set system account by account by public key or name") - cmd.Flags().StringSliceVarP(¶ms.serviceURLs, "service-url", "n", nil, "add an operator service url for nsc where clients can access the NATS service (only nats/tls urls supported)") - cmd.Flags().StringSliceVarP(¶ms.rmServiceURLs, "rm-service-url", "", nil, "remove an operator service url for nsc where clients can access the NATS service (only nats/tls urls supported)") + cmd.Flags().StringSliceVarP(¶ms.serviceURLs, "service-url", "n", nil, "add an operator service url for nsc where clients can access the NATS service (only nats/tls/ws/wss urls supported)") + cmd.Flags().StringSliceVarP(¶ms.rmServiceURLs, "rm-service-url", "", nil, "remove an operator service url for nsc where clients can access the NATS service (only nats/tls/ws/wss urls supported)") cmd.Flags().BoolVarP(¶ms.reqSk, "require-signing-keys", "", false, "require accounts/user to be signed with a signing key") cmd.Flags().BoolVarP(¶ms.rmAsu, "rm-account-jwt-server-url", "", false, "clear account server url") params.TimeParams.BindFlags(cmd) diff --git a/cmd/expirations_test.go b/cmd/expirations_test.go index 02e587ea..907cac01 100644 --- a/cmd/expirations_test.go +++ b/cmd/expirations_test.go @@ -17,9 +17,10 @@ package cmd import ( "encoding/json" - "github.com/stretchr/testify/require" "testing" "time" + + "github.com/stretchr/testify/require" ) func Test_ExpirationsNone(t *testing.T) { diff --git a/cmd/generateactivation.go b/cmd/generateactivation.go index feb14e10..1426f7c8 100644 --- a/cmd/generateactivation.go +++ b/cmd/generateactivation.go @@ -168,7 +168,7 @@ func (p *GenerateActivationParams) PostInteractive(ctx ActionCtx) error { if err != nil { return err } - if oc.AccountServerURL != "" && !IsNatsUrl(oc.AccountServerURL) { + if oc.AccountServerURL != "" && IsAccountServerURL(oc.AccountServerURL) { m := fmt.Sprintf("push the activation to %q", oc.AccountServerURL) p.push, err = cli.Confirm(m, false) if err != nil { @@ -274,7 +274,7 @@ func (p *GenerateActivationParams) Run(ctx ActionCtx) (store.Status, error) { } if oc.AccountServerURL == "" { return nil, fmt.Errorf("operator %s doesn't have an account server url configured", oc.Name) - } else if IsNatsUrl(oc.AccountServerURL) { + } else if IsResolverURL(oc.AccountServerURL) { return nil, fmt.Errorf("activation push is only supported for http base account server not nats-resover enabled nats-server") } u, err := url.Parse(oc.AccountServerURL) diff --git a/cmd/pull.go b/cmd/pull.go index d75600c7..1da28c27 100644 --- a/cmd/pull.go +++ b/cmd/pull.go @@ -159,7 +159,7 @@ func (p *PullParams) Validate(ctx ActionCtx) error { if oc.AccountServerURL == "" { return fmt.Errorf("operator %q doesn't set account server url - unable to pull", ctx.StoreCtx().Operator.Name) } - if IsNatsUrl(oc.AccountServerURL) && !p.All { + if IsResolverURL(oc.AccountServerURL) && !p.All { return fmt.Errorf("operator %q can only pull all jwt - unable to pull by account", ctx.StoreCtx().Operator.Name) } return nil @@ -253,7 +253,7 @@ func (p *PullParams) Run(ctx ActionCtx) (store.Status, error) { err := fmt.Errorf("operator has no account server url") r.AddError("operator %s: %v", op.Name, err) return r, err - } else if url := op.AccountServerURL; IsNatsUrl(url) { + } else if url := op.AccountServerURL; IsResolverURL(url) { subR := store.NewReport(store.OK, `pull from cluster using system account`) r.Add(subR) ib := nats.NewInbox() diff --git a/cmd/pull_test.go b/cmd/pull_test.go index 03baabe6..0877eea0 100644 --- a/cmd/pull_test.go +++ b/cmd/pull_test.go @@ -157,6 +157,7 @@ func Test_SyncNewerFromNatsResolver(t *testing.T) { require.NoError(t, err) dir := ts.AddSubDir(t, "resolver") data = bytes.ReplaceAll(data, []byte(`dir: './jwt'`), []byte(fmt.Sprintf(`dir: '%s'`, dir))) + t.Log(string(data)) err = os.WriteFile(serverconf, data, 0660) require.NoError(t, err) // Create a new account, only known to the nats-server. This account can be pulled @@ -190,6 +191,61 @@ func Test_SyncNewerFromNatsResolver(t *testing.T) { require.Equal(t, claimOrig.ID, claim2.ID) } +func Test_SyncNewerFromNatsResolverWs(t *testing.T) { + ts := NewEmptyStore(t) + defer ts.Done(t) + _, _, err := ExecuteCmd(createAddOperatorCmd(), "--name", "OP", "--sys") + require.NoError(t, err) + ts.SwitchOperator(t, "OP") // switch the operator so ts is in a usable state to obtain operator key + serverconf := filepath.Join(ts.Dir, "server.conf") + _, _, err = ExecuteCmd(createServerConfigCmd(), "--nats-resolver", "--config-file", serverconf) + require.NoError(t, err) + _, _, err = ExecuteCmd(CreateAddAccountCmd(), "--name", "AC1") + require.NoError(t, err) + // modify the generated file so testing becomes easier by knowing where the jwt directory is + data, err := os.ReadFile(serverconf) + require.NoError(t, err) + dir := ts.AddSubDir(t, "resolver") + ws := `websocket: { + port: -1 + no_tls: true +}` + data = append(data, ws...) + data = bytes.ReplaceAll(data, []byte(`dir: './jwt'`), []byte(fmt.Sprintf(`dir: '%s'`, dir))) + err = os.WriteFile(serverconf, data, 0660) + require.NoError(t, err) + // Create a new account, only known to the nats-server. This account can be pulled + opKey, err := ts.Store.GetRootPublicKey() + require.NoError(t, err) + opKp, err := ts.KeyStore.GetKeyPair(opKey) + require.NoError(t, err) + acKp, err := nkeys.CreateAccount() + require.NoError(t, err) + subj, err := acKp.PublicKey() + require.NoError(t, err) + claimOrig := jwt.NewAccountClaims(subj) + claimOrig.Name = "acc-name" + theJwtToPull, err := claimOrig.Encode(opKp) + require.NoError(t, err) + os.WriteFile(dir+string(os.PathSeparator)+subj+".jwt", []byte(theJwtToPull), 0660) + ports := ts.RunServerWithConfig(t, serverconf) + require.NotNil(t, ports) + // only after server start as ports are not yet known in tests + _, _, err = ExecuteCmd(createEditOperatorCmd(), "--account-jwt-server-url", ports.WebSocket[0]) + require.NoError(t, err) + + _, _, err = ExecuteCmd(createPullCmd(), "--all") + require.NoError(t, err) + // again, this time with system account and user specified + _, _, err = ExecuteCmd(createPullCmd(), "--all", "--system-account", "SYS", "--system-user", "sys") + require.NoError(t, err) + // claim now exists in nsc store + claim2, err := ts.Store.ReadAccountClaim("acc-name") + require.NoError(t, err) + require.NotEmpty(t, claimOrig.ID) + require.Equal(t, claimOrig.ID, claim2.ID) +} + func Test_V2OperatorDoesntFail(t *testing.T) { _, _, okp := CreateOperatorKey(t) as, m := RunTestAccountServerWithOperatorKP(t, okp, TasOpts{Vers: 2, OperatorOnlyIfV2: true}) diff --git a/cmd/push.go b/cmd/push.go index 076c540c..d4f902b8 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -246,7 +246,7 @@ func (p *PushCmdParams) validURL(s string) error { return err } scheme := strings.ToLower(u.Scheme) - supported := []string{"http", "https", "nats"} + supported := []string{"http", "https", "nats", "ws", "wss"} ok := false for _, v := range supported { @@ -271,7 +271,7 @@ func (p *PushCmdParams) PreInteractive(ctx ActionCtx) error { if p.ASU, err = cli.Prompt("Account Server URL or nats-resolver enabled nats-server URL", p.ASU, cli.Val(p.validURL)); err != nil { return err } - if IsNatsUrl(p.ASU) { + if IsResolverURL(p.ASU) { if p.sysAcc == "" { if p.sysAcc, err = ctx.StoreCtx().PickAccount(p.sysAcc); err != nil { return err @@ -307,7 +307,7 @@ func (p *PushCmdParams) Validate(ctx ActionCtx) error { if p.ASU == "" { return errors.New("no account server url or nats-server url was provided by the operator jwt") } - if !IsNatsUrl(p.ASU) && p.prune { + if !IsResolverURL(p.ASU) && p.prune { return errors.New("prune only works for nats based account resolver") } @@ -529,7 +529,7 @@ func (p *PushCmdParams) Run(ctx ActionCtx) (store.Status, error) { return nil, err } r := store.NewDetailedReport(true) - if !IsNatsUrl(p.ASU) { + if !IsResolverURL(p.ASU) { for _, v := range p.targeted { sub := store.NewReport(store.OK, "push %s to account server", v) sub.Opt = store.DetailsOnErrorOrWarning diff --git a/cmd/push_test.go b/cmd/push_test.go index bff272c8..f86376f4 100644 --- a/cmd/push_test.go +++ b/cmd/push_test.go @@ -319,3 +319,40 @@ func Test_SyncBadUrl(t *testing.T) { require.NoError(t, err) require.Equal(t, len(filesPre), 2) } + +func Test_SyncWs(t *testing.T) { + ts := NewEmptyStore(t) + defer ts.Done(t) + _, _, err := ExecuteCmd(createAddOperatorCmd(), "--name", "OP", "--sys") + require.NoError(t, err) + serverconf := filepath.Join(ts.Dir, "server.conf") + _, _, err = ExecuteCmd(createServerConfigCmd(), "--nats-resolver", "--config-file", serverconf) + require.NoError(t, err) + _, _, err = ExecuteCmd(CreateAddAccountCmd(), "--name", "AC1") + require.NoError(t, err) + // modify the generated file so testing becomes easier by knowing where the jwt directory is + data, err := os.ReadFile(serverconf) + require.NoError(t, err) + dir := ts.AddSubDir(t, "resolver") + + ws := `websocket: { + port: -1 + no_tls: true +}` + data = append(data, ws...) + data = bytes.ReplaceAll(data, []byte(`dir: './jwt'`), []byte(fmt.Sprintf(`dir: '%s'`, dir))) + err = os.WriteFile(serverconf, data, 0660) + require.NoError(t, err) + ports := ts.RunServerWithConfig(t, serverconf) + require.NotNil(t, ports) + _, _, err = ExecuteCmd(createEditOperatorCmd(), "--account-jwt-server-url", ports.WebSocket[0]) + require.NoError(t, err) + // Try again, thus also testing if the server is still around + // Provide explicit system account user to connect + _, _, err = ExecuteCmd(createPushCmd(), "--all", "--system-account", "SYS", "--system-user", "sys") + require.NoError(t, err) + // test to assure AC1/AC2/SYS where pushed + filesPre, err := filepath.Glob(dir + string(os.PathSeparator) + "*.jwt") + require.NoError(t, err) + require.Equal(t, len(filesPre), 2) +} diff --git a/cmd/store/store.go b/cmd/store/store.go index eaf18fe8..57709c3a 100644 --- a/cmd/store/store.go +++ b/cmd/store/store.go @@ -396,6 +396,19 @@ func IsNatsUrl(url string) bool { return strings.HasPrefix(url, "nats://") || strings.HasPrefix(url, ",nats://") } +func IsAccountServerURL(u string) bool { + u = strings.ToLower(u) + return strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://") +} + +func IsResolverURL(u string) bool { + u = strings.ToLower(u) + return strings.HasPrefix(u, "nats://") || + strings.HasPrefix(u, "tls://") || + strings.HasPrefix(u, "ws://") || + strings.HasPrefix(u, "wss://") +} + func (s *Store) handleManagedAccount(data []byte) (*Report, error) { ac, err := jwt.DecodeAccountClaims(string(data)) if err != nil { @@ -407,7 +420,7 @@ func (s *Store) handleManagedAccount(data []byte) (*Report, error) { return nil, fmt.Errorf("unable to push to the operator - failed to read operator claim: %w", err) } r := NewDetailedReport(false) - if oc.AccountServerURL == "" || IsNatsUrl(oc.AccountServerURL) { + if oc.AccountServerURL == "" || IsResolverURL(oc.AccountServerURL) { r.Label = "stored self signed account jwt" r.AddWarning("unable to push to %q - operator doesn't set an account server url or manual exchange necessary", oc.Name) return r, nil diff --git a/go.mod b/go.mod index b1000eb8..8bdff1f3 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/nats-io/cliprompts/v2 v2.0.0-20231014115920-801ca035562a github.com/nats-io/jsm.go v0.1.0 - github.com/nats-io/jwt/v2 v2.5.2 + github.com/nats-io/jwt/v2 v2.5.3 github.com/nats-io/nats-server/v2 v2.10.4 github.com/nats-io/nats.go v1.31.0 github.com/nats-io/nkeys v0.4.6 diff --git a/go.sum b/go.sum index 31d22de5..e302c9bf 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ github.com/nats-io/cliprompts/v2 v2.0.0-20231014115920-801ca035562a h1:28qvB6peS github.com/nats-io/cliprompts/v2 v2.0.0-20231014115920-801ca035562a/go.mod h1:oweZn7AeaVJYKlNHfCIhznJVsdySLSng55vfuINE/d0= github.com/nats-io/jsm.go v0.1.0 h1:H2gYCee/iyBDjUftPOr5fEPWAcG/+fyVl89IWiy6AC4= github.com/nats-io/jsm.go v0.1.0/go.mod h1:snnYORje42cEDCX5QygzeoVA2KiWVbiIJbLfGIvXW08= -github.com/nats-io/jwt/v2 v2.5.2 h1:DhGH+nKt+wIkDxM6qnVSKjokq5t59AZV5HRcFW0zJwU= -github.com/nats-io/jwt/v2 v2.5.2/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+vu/+ibI= +github.com/nats-io/jwt/v2 v2.5.3 h1:/9SWvzc6hTfamcgXJ3uYRpgj+QuY2aLNqRiqrKcrpEo= +github.com/nats-io/jwt/v2 v2.5.3/go.mod h1:iysuPemFcc7p4IoYots3IuELSI4EDe9Y0bQMe+I3Bf4= github.com/nats-io/nats-server/v2 v2.10.4 h1:uB9xcwon3tPXWAdmTJqqqC6cie3yuPWHJjjTBgaPNus= github.com/nats-io/nats-server/v2 v2.10.4/go.mod h1:eWm2JmHP9Lqm2oemB6/XGi0/GwsZwtWf8HIPUsh+9ns= github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E=