Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add mermaid generation to schema policy command #145

Merged
merged 1 commit into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 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,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) {
Expand Down Expand Up @@ -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
Expand Down
111 changes: 111 additions & 0 deletions cmd/schema_mermaid.go
Original file line number Diff line number Diff line change
@@ -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())
}
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
```