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 }