diff --git a/cmd/schema.go b/cmd/schema.go index 83da5d64..e2b34f20 100644 --- a/cmd/schema.go +++ b/cmd/schema.go @@ -6,6 +6,7 @@ import ( v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" "github.com/spf13/cobra" + "github.com/spf13/viper" "go.infratographer.com/x/otelx" "go.infratographer.com/permissions-api/internal/config" @@ -29,6 +30,17 @@ func init() { rootCmd.AddCommand(schemaCmd) schemaCmd.Flags().BoolVar(&dryRun, "dry-run", false, "dry run: print the schema instead of applying it") + + schemaCmd.Flags().Bool("mermaid", false, "outputs the policy as a mermaid chart definition") + schemaCmd.Flags().Bool("mermaid-markdown", false, "outputs the policy as a markdown mermaid chart definition") + + if err := viper.BindPFlag("mermaid", schemaCmd.Flags().Lookup("mermaid")); err != nil { + panic(err) + } + + if err := viper.BindPFlag("mermaid-markdown", schemaCmd.Flags().Lookup("mermaid-markdown")); err != nil { + panic(err) + } } func writeSchema(ctx context.Context, dryRun bool, cfg *config.AppConfig) { @@ -57,6 +69,12 @@ func writeSchema(ctx context.Context, dryRun bool, cfg *config.AppConfig) { logger.Fatalw("failed to generate schema from policy", "error", err) } + if viper.GetBool("mermaid") || viper.GetBool("mermaid-markdown") { + outputPolicyMermaid(cfg.SpiceDB.PolicyFile, viper.GetBool("mermaid-markdown")) + + return + } + if dryRun { fmt.Printf("%s", schemaStr) return diff --git a/cmd/schema_mermaid.go b/cmd/schema_mermaid.go new file mode 100644 index 00000000..a041aea4 --- /dev/null +++ b/cmd/schema_mermaid.go @@ -0,0 +1,111 @@ +package cmd + +import ( + "bytes" + "fmt" + "os" + "text/template" + + "go.infratographer.com/permissions-api/internal/iapl" + "gopkg.in/yaml.v3" +) + +var ( + mermaidTemplate = `erDiagram +{{- range $resource := .ResourceTypes }} + {{ $resource.Name }} { + id_prefix {{ $resource.IDPrefix }} + {{- range $action := index $.Actions $resource.Name }} + perm {{ $action }} + {{- end }} + {{- range $relation, $actions := index $.RelatedActions $resource.Name }} + {{- range $action := $actions }} + {{ $relation }}_perm {{ $action }} + {{- end }} + {{- end }} + } + {{- range $rel := $resource.Relationships }} + {{- range $targetName := $rel.TargetTypeNames }} + {{ $resource.Name }} ||--o{ {{ $targetName }} : {{ $rel.Relation }} + {{- end }} + {{- end }} +{{- end }} +{{- range $union := .Unions }} + {{ $union.Name }} { + {{- range $action := index $.Actions $union.Name }} + perm {{ $action }} + {{- end }} + } + {{- range $typ := $union.ResourceTypeNames }} + {{ $union.Name }} }o--|| {{ $typ }} : alias + {{- end }} +{{- end }}` + + mermaidTmpl = template.Must(template.New("mermaid").Parse(mermaidTemplate)) +) + +type mermaidContext struct { + ResourceTypes []iapl.ResourceType + Unions []iapl.Union + Actions map[string][]string + RelatedActions map[string]map[string][]string +} + +func outputPolicyMermaid(filePath string, markdown bool) { + var policy iapl.PolicyDocument + + if filePath != "" { + file, err := os.Open(filePath) + if err != nil { + logger.Fatalw("failed to open policy document file", "error", err) + } + + defer file.Close() + + if err := yaml.NewDecoder(file).Decode(&policy); err != nil { + logger.Fatalw("failed to load policy document file", "error", err) + } + } else { + policy = iapl.DefaultPolicyDocument() + } + + actions := map[string][]string{} + relatedActions := map[string]map[string][]string{} + + for _, binding := range policy.ActionBindings { + for _, cond := range binding.Conditions { + if cond.RoleBinding != nil { + actions[binding.TypeName] = append(actions[binding.TypeName], binding.ActionName) + } + + if cond.RelationshipAction != nil { + if _, ok := relatedActions[binding.TypeName]; !ok { + relatedActions[binding.TypeName] = make(map[string][]string) + } + + relatedActions[binding.TypeName][cond.RelationshipAction.Relation] = append(relatedActions[binding.TypeName][cond.RelationshipAction.Relation], cond.RelationshipAction.ActionName) + } + } + } + + ctx := mermaidContext{ + ResourceTypes: policy.ResourceTypes, + Unions: policy.Unions, + Actions: actions, + RelatedActions: relatedActions, + } + + var out bytes.Buffer + + if err := mermaidTmpl.Execute(&out, ctx); err != nil { + logger.Fatalw("failed to render mermaid chart for policy", "error", err) + } + + if markdown { + fmt.Printf("```mermaid\n%s\n```\n", out.String()) + + return + } + + fmt.Println(out.String()) +} diff --git a/diagram.md b/diagram.md new file mode 100644 index 00000000..ebf75ae8 --- /dev/null +++ b/diagram.md @@ -0,0 +1,39 @@ +```mermaid +erDiagram + role { + id_prefix permrol + } + role ||--o{ subject : subject + user { + id_prefix idntusr + } + client { + id_prefix idntcli + } + tenant { + id_prefix tnntten + } + tenant ||--o{ tenant : parent + loadbalancer { + id_prefix loadbal + perm loadbalancer_get + perm loadbalancer_update + perm loadbalancer_delete + owner_perm loadbalancer_get + owner_perm loadbalancer_update + owner_perm loadbalancer_delete + } + loadbalancer ||--o{ resourceowner : owner + subject { + } + subject }o--|| user : alias + subject }o--|| client : alias + resourceowner { + perm loadbalancer_create + perm loadbalancer_get + perm loadbalancer_update + perm loadbalancer_list + perm loadbalancer_delete + } + resourceowner }o--|| tenant : alias +```