Skip to content

Commit

Permalink
add mermaid generation to schema policy command
Browse files Browse the repository at this point in the history
This adds the ability to generate a mermaid chart from the schema policy
to help with visualizing the resource relationships.

Signed-off-by: Mike Mason <[email protected]>
  • Loading branch information
mikemrm committed Jul 24, 2023
1 parent ef884e6 commit 8409b3f
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 0 deletions.
13 changes: 13 additions & 0 deletions cmd/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -29,6 +30,12 @@ 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")

viper.BindPFlag("mermaid", schemaCmd.Flags().Lookup("mermaid"))
viper.BindPFlag("mermaid-markdown", schemaCmd.Flags().Lookup("mermaid-markdown"))
}

func writeSchema(ctx context.Context, dryRun bool, cfg *config.AppConfig) {
Expand Down Expand Up @@ -57,6 +64,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
Expand Down
109 changes: 109 additions & 0 deletions cmd/schema_mermaid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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())
}
39 changes: 39 additions & 0 deletions diagram.md
Original file line number Diff line number Diff line change
@@ -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
```

0 comments on commit 8409b3f

Please sign in to comment.