github.com/nats-io/nsc@v0.0.0-20221206222106-35db9400b257/cmd/generatediagram.go (about)

     1  /*
     2   * Copyright 2020-2020 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  	"fmt"
    20  	"os"
    21  	"strings"
    22  	"time"
    23  
    24  	cli "github.com/nats-io/cliprompts/v2"
    25  	"github.com/nats-io/jwt/v2"
    26  	"github.com/nats-io/nsc/cmd/store"
    27  	"github.com/spf13/cobra"
    28  )
    29  
    30  var outputFile string
    31  
    32  func init() {
    33  	diagram := &cobra.Command{
    34  		Use:          "diagram",
    35  		Short:        "Generate diagrams for this store",
    36  		Args:         MaxArgs(0),
    37  		SilenceUsage: true,
    38  	}
    39  	accDetail := false
    40  	comp := &cobra.Command{
    41  		Use:          "component",
    42  		Short:        "Generate a plantuml component diagram for this store",
    43  		Args:         MaxArgs(0),
    44  		SilenceUsage: true,
    45  		Example:      `nsc generate diagram component`,
    46  		RunE: func(cmd *cobra.Command, args []string) error {
    47  			return componentDiagram(accDetail)
    48  		},
    49  	}
    50  	comp.Flags().BoolVarP(&accDetail, "detail", "", false, "Include account descriptions")
    51  	diagram.AddCommand(comp)
    52  	showKeys, detail, users := false, false, false
    53  	object := &cobra.Command{
    54  		Use:          "object",
    55  		Short:        "Generate a plantuml object diagram for this store",
    56  		Args:         MaxArgs(0),
    57  		SilenceUsage: true,
    58  		Example:      `nsc generate diagram object`,
    59  		RunE: func(cmd *cobra.Command, args []string) error {
    60  			return objectDiagram(users, showKeys, detail)
    61  		},
    62  	}
    63  	object.Flags().BoolVarP(&showKeys, "show-keys", "", false, "Include keys in diagram")
    64  	object.Flags().BoolVarP(&users, "users", "", false, "Include User")
    65  	object.Flags().BoolVarP(&detail, "detail", "", false, "Include empty/unlimited values")
    66  	diagram.AddCommand(object)
    67  	diagram.PersistentFlags().StringVarP(&outputFile, "output-file", "o", "--", "output file, '--' is stdout")
    68  	generateCmd.AddCommand(diagram)
    69  }
    70  
    71  const rename = "<&resize-width>"
    72  
    73  func accessMod(e *jwt.Export) string {
    74  	if e.TokenReq {
    75  		return "private"
    76  	}
    77  	return "public"
    78  }
    79  
    80  func expType(e *jwt.Export) string {
    81  	switch e.Type {
    82  	case jwt.Stream:
    83  		return "stream"
    84  	case jwt.Service:
    85  		return "service"
    86  	default:
    87  		return "n/a"
    88  	}
    89  }
    90  
    91  func expName(e *jwt.Export) string {
    92  	name := e.Name
    93  	if name == "" {
    94  		name = string(e.Subject)
    95  	}
    96  	return name
    97  }
    98  
    99  func expId(subject string, e *jwt.Export) string {
   100  	s := strings.ReplaceAll(expName(e), " ", "_")
   101  	s = strings.ReplaceAll(s, "*", "_A_")
   102  	s = strings.ReplaceAll(s, ">", "_G_")
   103  	s = strings.ReplaceAll(s, "$", "_D_")
   104  	s = strings.ReplaceAll(s, "-", "_M_")
   105  	return fmt.Sprintf("%s_%s", subject, s)
   106  }
   107  
   108  func impSubj(i *jwt.Import) (local string, remote string) {
   109  	if i.LocalSubject != "" {
   110  		local = string(i.LocalSubject)
   111  		remote = string(i.Subject)
   112  	} else {
   113  		local = i.GetTo()
   114  		if local == "" {
   115  			local = string(i.Subject)
   116  		}
   117  		remote = string(i.Subject)
   118  		if i.Type == jwt.Service {
   119  			local, remote = remote, local
   120  		}
   121  	}
   122  	return
   123  }
   124  
   125  func componentDiagram(accDetail bool) error {
   126  	s, err := GetStore()
   127  	if err != nil {
   128  		return err
   129  	}
   130  	op, err := s.ReadOperatorClaim()
   131  	if err != nil {
   132  		return err
   133  	}
   134  	f := os.Stdout
   135  	if !IsStdOut(outputFile) {
   136  		if f, err = os.Create(outputFile); err != nil {
   137  			return err
   138  		}
   139  	}
   140  	bldrPrntf := func(format string, args ...interface{}) {
   141  		fmt.Fprintln(f, fmt.Sprintf(format, args...))
   142  	}
   143  	addNote := func(ref string, i jwt.Info) {
   144  		if !accDetail {
   145  			return
   146  		}
   147  		if i.Description != "" || i.InfoURL != "" {
   148  			link := ""
   149  			if i.InfoURL != "" {
   150  				link = fmt.Sprintf("\n[[%s info]]", i.InfoURL)
   151  			}
   152  			bldrPrntf("note right of %s\n%s %s\nend note", ref, cli.WrapString(20, i.Description), link)
   153  		}
   154  	}
   155  	bldrPrntf(`@startuml
   156  skinparam component {
   157  	ArrowFontName Arial
   158  	ArrowFontColor #636363
   159      ArrowSize 10pt
   160  }
   161  skinparam interface {
   162      backgroundColor<<not-found public service>> Red
   163      backgroundColor<<not-found private service>> Red
   164      backgroundColor<<not-found public stream>> Red
   165      backgroundColor<<not-found private stream>> Red
   166  }
   167  `)
   168  	addValidationNote := func(id string, name string, vr *jwt.ValidationResults) {
   169  		if len(vr.Issues) == 0 {
   170  			return
   171  		}
   172  		if len(vr.Issues) == 1 && strings.HasPrefix(vr.Issues[0].Description, "the field to has been deprecated") {
   173  			return
   174  		}
   175  		bldrPrntf("note left of %s\n", id)
   176  		bldrPrntf("** Validation Issues by %s**\n", name)
   177  		for _, v := range vr.Issues {
   178  			if !strings.HasPrefix(v.Description, "the field to has been deprecated") {
   179  				bldrPrntf("* %s\n", v.Description)
   180  			}
   181  		}
   182  		bldrPrntf("end note")
   183  	}
   184  	escapeSubjectLabel := func(sub string) string {
   185  		// * is special notation in plantuml. (escape by adding a space)
   186  		if strings.HasPrefix(sub, "*") {
   187  			return fmt.Sprintf(" %s", sub)
   188  		}
   189  		return sub
   190  	}
   191  	bldrPrntf(`title Component Diagram of Accounts - Operator %s`, op.Name)
   192  	accs, _ := s.ListSubContainers(store.Accounts)
   193  	accBySubj := make(map[string]*jwt.AccountClaims)
   194  	for _, accName := range accs {
   195  		ac, err := s.ReadAccountClaim(accName)
   196  		if err != nil {
   197  			return err
   198  		}
   199  		accBySubj[ac.Subject] = ac
   200  		if len(ac.Imports)+len(ac.Exports) == 0 {
   201  			continue
   202  		}
   203  		bldrPrntf(`component [%s] as %s <<account>>`, ac.Name, ac.Subject)
   204  		addNote(ac.Subject, ac.Info)
   205  		for _, e := range ac.Exports {
   206  			eId := expId(ac.Subject, e)
   207  			bldrPrntf(`interface "%s" << %s %s >> as %s`, expName(e), accessMod(e), expType(e), eId)
   208  			bldrPrntf(`%s -- %s : ""%s"""`, expId(ac.Subject, e), ac.Subject, escapeSubjectLabel(string(e.Subject)))
   209  			addNote(eId, e.Info)
   210  
   211  			vr := jwt.ValidationResults{}
   212  			e.Validate(&vr)
   213  			addValidationNote(eId, ac.Name, &vr)
   214  		}
   215  		bldrPrntf("")
   216  	}
   217  	for _, accSubj := range accs {
   218  		ac, err := s.ReadAccountClaim(accSubj)
   219  		if err != nil {
   220  			return err
   221  		}
   222  		for _, i := range ac.Imports {
   223  			local, remote := impSubj(i)
   224  			foundExport := false
   225  			tokenReq := false
   226  			if i.Token != "" {
   227  				tokenReq = true
   228  			}
   229  			matchingExport := &jwt.Export{Subject: jwt.Subject(remote), Type: i.Type, TokenReq: tokenReq} // dummy
   230  			impAcc, foundExporter := accBySubj[i.Account]
   231  			if foundExporter {
   232  				for _, e := range impAcc.Exports {
   233  					if i.Type == e.Type && jwt.Subject(remote).IsContainedIn(e.Subject) {
   234  						matchingExport = e
   235  						foundExport = true
   236  						break
   237  					}
   238  				}
   239  			}
   240  			id := expId(i.Account, matchingExport)
   241  			if !foundExport {
   242  				bldrPrntf(`interface " " << not-found %s %s >> as %s`, accessMod(matchingExport), expType(matchingExport), id)
   243  			}
   244  			if local != remote {
   245  				bldrPrntf(`%s "%s%s" ..> %s : "%s"`, ac.Subject, rename, local, id, escapeSubjectLabel(remote))
   246  			} else {
   247  				bldrPrntf(`%s ..> %s : "%s"`, ac.Subject, id, escapeSubjectLabel(remote))
   248  			}
   249  			vr := jwt.ValidationResults{}
   250  			i.Validate(ac.Subject, &vr)
   251  			if matchingExport.TokenReq && i.Token == "" {
   252  				vr.AddError("Export is private but no activation token")
   253  			} else if !matchingExport.TokenReq && i.Token != "" {
   254  				vr.AddError("Export is public but import has activation token")
   255  			}
   256  			if !foundExporter {
   257  				vr.AddError("Exporting account not present: %s", i.Account)
   258  			}
   259  			addValidationNote(id, ac.Name, &vr)
   260  		}
   261  	}
   262  	bldrPrntf("legend\n\"%sX\", the imported subject is rewritten to X\nend legend", rename)
   263  	bldrPrntf(`footer generated by nsc - store dir: %s - date: %s `, s.Dir, time.Now().Format("2006-01-02 15:04:05"))
   264  	bldrPrntf("@enduml")
   265  
   266  	return f.Close()
   267  }
   268  
   269  func objectDiagram(users bool, showKeys bool, detail bool) error {
   270  	s, err := GetStore()
   271  	if err != nil {
   272  		return err
   273  	}
   274  	ctx, err := s.GetContext()
   275  	if err != nil {
   276  		return err
   277  	}
   278  	op, err := s.ReadOperatorClaim()
   279  	if err != nil {
   280  		return err
   281  	}
   282  	f := os.Stdout
   283  	if !IsStdOut(outputFile) {
   284  		if f, err = os.Create(outputFile); err != nil {
   285  			return err
   286  		}
   287  	}
   288  	bldrPrntf := func(format string, args ...interface{}) {
   289  		fmt.Fprintln(f, fmt.Sprintf(format, args...))
   290  	}
   291  	addNote := func(ref string, i jwt.Info) {
   292  		if i.Description != "" || i.InfoURL != "" {
   293  			link := ""
   294  			if i.InfoURL != "" {
   295  				link = fmt.Sprintf("\n[[%s info]]", i.InfoURL)
   296  			}
   297  			bldrPrntf("note right of %s\n%s %s\nend note", ref, cli.WrapString(20, i.Description), link)
   298  		}
   299  	}
   300  	addValue := func(name string, format string, args ...interface{}) {
   301  		value := fmt.Sprintf(format, args...)
   302  		if value != "" || detail {
   303  			bldrPrntf(`%s = %s`, name, value)
   304  		}
   305  	}
   306  	addList := func(name string, list []string) {
   307  		if len(list) != 0 || detail {
   308  			addValue(name, strings.Trim(fmt.Sprintf("%q", list), " []"))
   309  		}
   310  	}
   311  	addTime := func(name string, when int64) {
   312  		if when != 0 {
   313  			bldrPrntf(`%s = %s`, name, time.Unix(when, 0).Format("2006-01-02 15:04:05"))
   314  		} else if detail {
   315  			bldrPrntf(`%s = not set`, name)
   316  		}
   317  	}
   318  	addClaims := func(data jwt.ClaimsData, tags jwt.TagList) {
   319  		if showKeys {
   320  			addValue("Identity Key", data.Subject)
   321  			addValue("Identity Key Present", fmt.Sprintf("%t", ctx.KeyStore.HasPrivateKey(data.Subject)))
   322  		}
   323  		addList("Tags", tags)
   324  		addTime("Issued At", data.IssuedAt)
   325  		addTime("Valid From", data.NotBefore)
   326  		addTime("Expires", data.Expires)
   327  	}
   328  	addValidationResults := func(claims jwt.Claims) {
   329  		vr := jwt.ValidationResults{}
   330  		claims.Validate(&vr)
   331  		if len(vr.Issues) == 0 {
   332  			if !detail {
   333  				return
   334  			}
   335  			bldrPrntf("--- Validation (no issues) ---")
   336  		} else {
   337  			bldrPrntf("==**<color:red>Validation</color>**==")
   338  		}
   339  		addValue("Errors", strings.Trim(fmt.Sprintf("%q", vr.Errors()), " []"))
   340  		addList("Warnings", vr.Warnings())
   341  	}
   342  	addLimit := func(name string, limit int64) {
   343  		if limit == -1 {
   344  			addValue(name, "-1 (unlimited)")
   345  		} else if limit == 0 {
   346  			addValue(name, "0 (disabled)")
   347  		} else {
   348  			addValue(name, fmt.Sprintf("%d", limit))
   349  		}
   350  	}
   351  	addAccLimits := func(l jwt.AccountLimits) {
   352  		if l.IsUnlimited() {
   353  			if !detail {
   354  				return
   355  			}
   356  			bldrPrntf("--- Account Limits (unlimited)---")
   357  		} else {
   358  			bldrPrntf("--- Account Limits ---")
   359  		}
   360  		addLimit("Max Exports", l.Exports)
   361  		addLimit("Max Imports", l.Imports)
   362  		addLimit("Max Client Connections", l.Conn)
   363  		addLimit("Max Leaf Node Connections", l.LeafNodeConn)
   364  		addValue("Allow Wildcard Exports", fmt.Sprintf("%t", l.WildcardExports))
   365  		addValue("Disallow bearer token", fmt.Sprintf("%t", l.DisallowBearer))
   366  	}
   367  	addNatsLimits := func(l jwt.NatsLimits) {
   368  		if l.IsUnlimited() {
   369  			if !detail {
   370  				return
   371  			}
   372  			bldrPrntf("--- Nats Limits (unlimited)---")
   373  		} else {
   374  			bldrPrntf("--- Nats Limits ---")
   375  		}
   376  		addLimit("Max Payload", l.Payload)
   377  		addLimit("Max Subscriber", l.Subs)
   378  		addLimit("Max Number of bytes", l.Data)
   379  	}
   380  	addJSLimits := func(l jwt.JetStreamLimits) {
   381  		if l.IsUnlimited() {
   382  			bldrPrntf("--- Jetstream Limits (unlimited) ---")
   383  		} else if l.DiskStorage == 0 && l.MemoryStorage == 0 {
   384  			if !detail {
   385  				return
   386  			}
   387  			bldrPrntf("--- Jetstream Limits (disabled) ---")
   388  		} else {
   389  			bldrPrntf("--- Jetstream Limits ---")
   390  		}
   391  		addLimit("Max Memory Storage", l.MemoryStorage)
   392  		addLimit("Max Disk Storage", l.DiskStorage)
   393  		addLimit("Max Streams", l.Streams)
   394  		addLimit("Max Consumer", l.Consumer)
   395  	}
   396  	addUserLimits := func(l jwt.UserLimits) {
   397  		if l.IsUnlimited() {
   398  			if !detail {
   399  				return
   400  			}
   401  			bldrPrntf("--- User Limits (unlimited)---")
   402  		} else {
   403  			bldrPrntf("--- User Limits ---")
   404  		}
   405  		addList("Permitted CIDR blocks", l.Src)
   406  
   407  		bldr := strings.Builder{}
   408  		for _, t := range l.Times {
   409  			bldr.WriteString(fmt.Sprintf(" [%s-%s]", t.Start, t.End))
   410  		}
   411  		addValue("Permitted Times to Connect", l.Locale+bldr.String())
   412  	}
   413  	addSigningKeys := func(subject string, subjName string, permissionsType string, keys jwt.StringList) {
   414  		if !showKeys {
   415  			return
   416  		}
   417  		if len(keys) == 0 {
   418  			return
   419  		}
   420  		permId := fmt.Sprintf("%s_sk", subject)
   421  		if permissionsType != "" {
   422  			permissionsType += " "
   423  		}
   424  		bldrPrntf(`map "%s" as %s << %ssigning keys >> {`, subjName, permId, permissionsType)
   425  		bldrPrntf(`key => stored`)
   426  		for _, k := range keys {
   427  			bldrPrntf(`%s => %t`, k, ctx.KeyStore.HasPrivateKey(k))
   428  		}
   429  		bldrPrntf(`}`)
   430  		bldrPrntf(`%s *-- %s `, subject, permId)
   431  	}
   432  	permissionsSet := func(p jwt.Permissions) bool {
   433  		return !(len(p.Pub.Allow)+len(p.Pub.Deny)+len(p.Sub.Allow)+len(p.Sub.Deny) == 0 && p.Resp == nil)
   434  	}
   435  	addPermissions := func(subject string, subjName string, permissionsType string, p jwt.Permissions) string {
   436  		addSubjects := func(name string, list jwt.StringList) {
   437  			if len(list) == 0 && !detail {
   438  				return
   439  			}
   440  			bldrPrntf("--- %s ---", name)
   441  			for _, sub := range list {
   442  				bldrPrntf(` ""%s""`, sub)
   443  			}
   444  		}
   445  		permId := fmt.Sprintf("%s_permissions", subject)
   446  		if permissionsType != "" {
   447  			permissionsType += " "
   448  		}
   449  		bldrPrntf(`object "%s" as %s << %spermissions >> {`, subjName, permId, permissionsType)
   450  		addSubjects("Publish Deny", p.Pub.Deny)
   451  		addSubjects("Publish Allow", p.Pub.Allow)
   452  		addSubjects("Subscribe Deny", p.Sub.Deny)
   453  		addSubjects("Subscribe Allow", p.Sub.Allow)
   454  		if p.Resp == nil {
   455  			if detail {
   456  				bldrPrntf("--- Response Permissions (server default)---")
   457  			}
   458  		} else {
   459  			bldrPrntf("--- Response Permissions ---")
   460  			addValue("Expiration", p.Resp.Expires.String())
   461  			addLimit("Max Messages", int64(p.Resp.MaxMsgs))
   462  		}
   463  		bldrPrntf(`}`)
   464  		return permId
   465  	}
   466  	connectSigned := func(signer jwt.ClaimsData, signee jwt.ClaimsData) {
   467  		if signee.Issuer == signee.Subject {
   468  			bldrPrntf(`%s -- %s : "self signed >"`, signee.Issuer, signee.Subject)
   469  		} else if !showKeys {
   470  			bldrPrntf(`%s -- %s : "signed >"`, signer.Subject, signee.Subject)
   471  		} else if signee.Issuer == signer.Subject {
   472  			bldrPrntf(`%s -- %s : "signed >"`, signee.Issuer, signee.Subject)
   473  		} else {
   474  			bldrPrntf(`%s_sk::%s -- %s : "signed >"`, signer.Subject, signee.Issuer, signee.Subject)
   475  		}
   476  	}
   477  	bldrPrntf(`@startuml`)
   478  	bldrPrntf(`title Object Diagram`)
   479  	bldrPrntf(`object "%s" as %s << operator >> {`, op.Name, op.Subject)
   480  	addClaims(op.ClaimsData, op.Tags)
   481  	addValue("JWT Version", "%d", op.Version)
   482  	addValue("account server", op.AccountServerURL)
   483  	addValue("Strict signing key usage", "%t", op.StrictSigningKeyUsage)
   484  	addValidationResults(op)
   485  	bldrPrntf("}")
   486  
   487  	addSigningKeys(op.Subject, op.Name, "operator", op.SigningKeys)
   488  
   489  	accs, _ := s.ListSubContainers(store.Accounts)
   490  	for _, accName := range accs {
   491  		ac, err := s.ReadAccountClaim(accName)
   492  		if err != nil {
   493  			return err
   494  		}
   495  		tp := "account"
   496  		if ac.Subject == op.SystemAccount {
   497  			tp = "system account"
   498  		}
   499  		bldrPrntf(`object "%s" as %s << %s >> {`, ac.Name, ac.Subject, tp)
   500  		addClaims(ac.ClaimsData, ac.Tags)
   501  
   502  		addAccLimits(ac.Limits.AccountLimits)
   503  		addNatsLimits(ac.Limits.NatsLimits)
   504  		addJSLimits(ac.Limits.JetStreamLimits)
   505  		addValidationResults(ac)
   506  		bldrPrntf("}")
   507  
   508  		defPermId := ""
   509  		if permissionsSet(ac.DefaultPermissions) {
   510  			defPermId = addPermissions(ac.Subject, ac.Name, "default", ac.DefaultPermissions)
   511  			bldrPrntf(`%s *-- %s`, ac.Subject, defPermId)
   512  		}
   513  		addSigningKeys(ac.Subject, ac.Name, "account", ac.SigningKeys.Keys())
   514  
   515  		connectSigned(op.ClaimsData, ac.ClaimsData)
   516  		addNote(ac.Subject, ac.Info)
   517  		if !users {
   518  			continue
   519  		}
   520  		usrs, _ := s.ListEntries(store.Accounts, accName, store.Users)
   521  		for _, usrName := range usrs {
   522  			uc, err := s.ReadUserClaim(accName, usrName)
   523  			if err != nil {
   524  				return err
   525  			}
   526  			bldrPrntf(`object "%s" as %s << user >> {`, uc.Name, uc.Subject)
   527  			addClaims(uc.ClaimsData, uc.Tags)
   528  			addValue("Bearer Token", fmt.Sprintf("%t", uc.BearerToken))
   529  			addList("Allowed Connection Types", uc.AllowedConnectionTypes)
   530  			addNatsLimits(uc.NatsLimits)
   531  			addUserLimits(uc.UserLimits)
   532  			addValidationResults(uc)
   533  			bldrPrntf("}")
   534  
   535  			if permissionsSet(uc.Permissions) {
   536  				permId := addPermissions(uc.Subject, uc.Name, "user", uc.Permissions)
   537  				bldrPrntf(`%s *-- %s : "API restricted by >"`, uc.Subject, permId)
   538  			} else if defPermId != "" {
   539  				bldrPrntf(`%s -- %s : "API restricted by >"`, uc.Subject, defPermId)
   540  			}
   541  			connectSigned(ac.ClaimsData, uc.ClaimsData)
   542  		}
   543  	}
   544  	bldrPrntf(`footer generated by nsc - store dir: %s - date: %s `, s.Dir, time.Now().Format("2006-01-02 15:04:05"))
   545  	bldrPrntf("@enduml")
   546  
   547  	return f.Close()
   548  }