diff --git a/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=empty.json b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=empty.json new file mode 100644 index 000000000..f278cfaa7 --- /dev/null +++ b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=empty.json @@ -0,0 +1 @@ +"import { Namespace, SubjectSet, Context } from '@ory/keto-namespace-types'\n\n// Declare new namespaces as classes that implement `Namespace`" diff --git a/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=many.json b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=many.json new file mode 100644 index 000000000..428d7b68c --- /dev/null +++ b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=many.json @@ -0,0 +1 @@ +"import { Namespace, SubjectSet, Context } from '@ory/keto-namespace-types'\n\n// Declare new namespaces as classes that implement `Namespace`\nclass one implements Namespace {\n related: {\n // Define relations to other objects here.\n // Examples:\n //\n // parents: (File | Folder)[]\n // viewers: SubjectSet\u003cGroup, \"members\"\u003e[]\n }\n\n permits = {\n // Define permissions here. These can be derived from the relations above.\n // Examples:\n //\n // view: (ctx: Context): boolean =\u003e\n // this.related.viewers.includes(ctx.subject) ||\n // this.related.parents.traverse((p) =\u003e p.permits.view(ctx)),\n }\n}\n\nclass two implements Namespace {\n related: {\n // Define relations to other objects here.\n // Examples:\n //\n // parents: (File | Folder)[]\n // viewers: SubjectSet\u003cGroup, \"members\"\u003e[]\n }\n\n permits = {\n // Define permissions here. These can be derived from the relations above.\n // Examples:\n //\n // view: (ctx: Context): boolean =\u003e\n // this.related.viewers.includes(ctx.subject) ||\n // this.related.parents.traverse((p) =\u003e p.permits.view(ctx)),\n }\n}\n\nclass three implements Namespace {\n related: {\n // Define relations to other objects here.\n // Examples:\n //\n // parents: (File | Folder)[]\n // viewers: SubjectSet\u003cGroup, \"members\"\u003e[]\n }\n\n permits = {\n // Define permissions here. These can be derived from the relations above.\n // Examples:\n //\n // view: (ctx: Context): boolean =\u003e\n // this.related.viewers.includes(ctx.subject) ||\n // this.related.parents.traverse((p) =\u003e p.permits.view(ctx)),\n }\n}\n" diff --git a/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=one.json b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=one.json new file mode 100644 index 000000000..59cea5089 --- /dev/null +++ b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=one.json @@ -0,0 +1 @@ +"import { Namespace, SubjectSet, Context } from '@ory/keto-namespace-types'\n\n// Declare new namespaces as classes that implement `Namespace`\nclass one implements Namespace {\n related: {\n // Define relations to other objects here.\n // Examples:\n //\n // parents: (File | Folder)[]\n // viewers: SubjectSet\u003cGroup, \"members\"\u003e[]\n }\n\n permits = {\n // Define permissions here. These can be derived from the relations above.\n // Examples:\n //\n // view: (ctx: Context): boolean =\u003e\n // this.related.viewers.includes(ctx.subject) ||\n // this.related.parents.traverse((p) =\u003e p.permits.view(ctx)),\n }\n}\n" diff --git a/cmd/namespace/config_template/namespaces.ts.tmpl b/cmd/namespace/config_template/namespaces.ts.tmpl new file mode 100644 index 000000000..b06dbd832 --- /dev/null +++ b/cmd/namespace/config_template/namespaces.ts.tmpl @@ -0,0 +1,22 @@ +import { Namespace, SubjectSet, Context } from '@ory/keto-namespace-types' + +// Declare new namespaces as classes that implement `Namespace`{{ range .Namespaces }} +class {{ . }} implements Namespace { + related: { + // Define relations to other objects here. + // Examples: + // + // parents: (File | Folder)[] + // viewers: SubjectSet[] + } + + permits = { + // Define permissions here. These can be derived from the relations above. + // Examples: + // + // view: (ctx: Context): boolean => + // this.related.viewers.includes(ctx.subject) || + // this.related.parents.traverse((p) => p.permits.view(ctx)), + } +} +{{ end }} \ No newline at end of file diff --git a/cmd/namespace/opl_generate.go b/cmd/namespace/opl_generate.go new file mode 100644 index 000000000..2519685f0 --- /dev/null +++ b/cmd/namespace/opl_generate.go @@ -0,0 +1,25 @@ +package namespace + +import ( + "embed" + "io" + "text/template" + + "github.com/pkg/errors" +) + +//go:embed config_template/* +var configTemplate embed.FS + +// GenerateOPLConfig derives an Ory Permission Language config from the +// namespaces and writes it to out. The OPL config is functionally equivalent to +// the list of namespaces. +func GenerateOPLConfig(namespaces []string, out io.Writer) error { + t, err := template.New("config_template").ParseFS(configTemplate, "config_template/*") + if err != nil { + return errors.WithStack(err) + } + return errors.WithStack(t.ExecuteTemplate(out, + "namespaces.ts.tmpl", + struct{ Namespaces []string }{Namespaces: namespaces})) +} diff --git a/cmd/namespace/opl_generate_test.go b/cmd/namespace/opl_generate_test.go new file mode 100644 index 000000000..7cd95b6db --- /dev/null +++ b/cmd/namespace/opl_generate_test.go @@ -0,0 +1,35 @@ +package namespace_test + +import ( + "bytes" + "testing" + + "github.com/ory/x/snapshotx" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/cmd/namespace" +) + +func TestGenerateOPLConfig(t *testing.T) { + cases := []struct { + name string + namespaces []string + }{{ + name: "empty", + namespaces: []string{}, + }, { + name: "one", + namespaces: []string{"one"}, + }, { + name: "many", + namespaces: []string{"one", "two", "three"}, + }} + + for _, tc := range cases { + t.Run("case="+tc.name, func(t *testing.T) { + var out bytes.Buffer + require.NoError(t, namespace.GenerateOPLConfig(tc.namespaces, &out)) + snapshotx.SnapshotT(t, out.String()) + }) + } +}