From 0ce15197848d1f3b79d10c78a472a8cafdc491cd Mon Sep 17 00:00:00 2001 From: hperl <34397+hperl@users.noreply.github.com> Date: Tue, 9 Aug 2022 13:57:38 +0200 Subject: [PATCH] feat: configure subject-set rewrites The subject-set rewrites can now be configured through the Ory Permission Language (OPL), which is a subset of TypeScript. The OPL config is referenced in the central configuration under namespaces as such: [...] namespaces: location: [...] The can be any valid file, directory or URI. --- .gitignore | 3 +- embedx/config.schema.json | 25 ++- internal/driver/config/namespace_memory.go | 59 ++++--- internal/driver/config/namespace_watcher.go | 163 +++++++++++------- .../config/opl_config_namespace_watcher.go | 99 +++++++++++ internal/driver/config/provider.go | 92 +++++++--- internal/driver/config/provider_test.go | 158 ++++++++++++----- 7 files changed, 450 insertions(+), 149 deletions(-) create mode 100644 internal/driver/config/opl_config_namespace_watcher.go diff --git a/.gitignore b/.gitignore index 44fb6d370..3cb5dd17a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ dist/ **/*.sqlite **/*.sqlite-journal .vscode/ -.fuzzer/ \ No newline at end of file +.fuzzer/ +keto \ No newline at end of file diff --git a/embedx/config.schema.json b/embedx/config.schema.json index 80677441a..bae192e61 100644 --- a/embedx/config.schema.json +++ b/embedx/config.schema.json @@ -29,7 +29,7 @@ } }, "additionalProperties": false, - "required": ["name", "id"] + "required": ["name"] }, "tlsxSource": { "type": "object", @@ -287,16 +287,35 @@ "default": "file://./keto_namespaces", "oneOf": [ { - "title": "Namespace Repo URI", - "description": "URI that points to a directory of namespace files, a single file with all namespaces, or a websocket connection that provides former via `github.com/ory/x/watcherx.WatchAndServeWS`", + "title": "Legacy namespace Repo URI", + "description": "A URI that points to a directory of namespace files, a single file with all namespaces, or a websocket connection that provides former via `github.com/ory/x/watcherx.WatchAndServeWS`", "type": "string", "format": "uri" }, { "type": "array", + "title": "Legacy namespace configuration", "items": { "$ref": "#/definitions/namespace" } + }, + { + "type": "object", + "title": "Namespace configuration", + "properties": { + "location": { + "type": "string", + "title": "Ory Permission Language config file URI", + "description": "A URI that points to a directory of namespace files, a single file with all namespaces, or a websocket connection that provides former via `github.com/ory/x/watcherx.WatchAndServeWS`", + "format": "uri", + "examples": [ + "file://./keto_namespaces.ts", + "file:///etc/configs/keto_namespaces.ts", + "ws://my.websocket.server/keto_namespaces.ts" + ] + } + }, + "required": ["location"] } ] }, diff --git a/internal/driver/config/namespace_memory.go b/internal/driver/config/namespace_memory.go index 2bd1e184b..00a10a1e8 100644 --- a/internal/driver/config/namespace_memory.go +++ b/internal/driver/config/namespace_memory.go @@ -3,6 +3,7 @@ package config import ( "context" "reflect" + "sync" "github.com/ory/herodot" "github.com/pkg/errors" @@ -11,35 +12,37 @@ import ( ) type ( - memoryNamespaceManager []*namespace.Namespace + memoryNamespaceManager struct { + byName map[string]*namespace.Namespace + sync.RWMutex + } ) var _ namespace.Manager = &memoryNamespaceManager{} func NewMemoryNamespaceManager(nn ...*namespace.Namespace) *memoryNamespaceManager { - nm := make(memoryNamespaceManager, len(nn)) - - for i, np := range nn { - n := *np - nm[i] = &n - } - - return &nm + s := &memoryNamespaceManager{} + s.set(nn) + return s } func (s *memoryNamespaceManager) GetNamespaceByName(_ context.Context, name string) (*namespace.Namespace, error) { - for _, n := range *s { - if n.Name == name { - return n, nil - } + s.RLock() + defer s.RUnlock() + + if n, ok := s.byName[name]; ok { + return n, nil } return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("Unknown namespace with name %q.", name)) } func (s *memoryNamespaceManager) GetNamespaceByConfigID(_ context.Context, id int32) (*namespace.Namespace, error) { - for _, n := range *s { - if n.ID == id { + s.RLock() + defer s.RUnlock() + + for _, n := range s.byName { + if n.ID == id { // nolint ignore deprecated method return n, nil } } @@ -48,16 +51,32 @@ func (s *memoryNamespaceManager) GetNamespaceByConfigID(_ context.Context, id in } func (s *memoryNamespaceManager) Namespaces(_ context.Context) ([]*namespace.Namespace, error) { - nn := make([]*namespace.Namespace, 0, len(*s)) + s.RLock() + defer s.RUnlock() - for _, n := range *s { - nc := *n - nn = append(nn, &nc) + nn := make([]*namespace.Namespace, 0, len(s.byName)) + for _, n := range s.byName { + nn = append(nn, n) } return nn, nil } func (s *memoryNamespaceManager) ShouldReload(newValue interface{}) bool { - return !reflect.DeepEqual(newValue, []*namespace.Namespace(*s)) + s.RLock() + defer s.RUnlock() + + nn, _ := s.Namespaces(context.Background()) + + return !reflect.DeepEqual(newValue, nn) +} + +func (s *memoryNamespaceManager) set(nn []*namespace.Namespace) { + s.Lock() + defer s.Unlock() + + s.byName = make(map[string]*namespace.Namespace, len(nn)) + for _, n := range nn { + s.byName[n.Name] = n + } } diff --git a/internal/driver/config/namespace_watcher.go b/internal/driver/config/namespace_watcher.go index 9f82b6403..ad0195353 100644 --- a/internal/driver/config/namespace_watcher.go +++ b/internal/driver/config/namespace_watcher.go @@ -36,129 +36,172 @@ type ( NamespaceWatcher struct { sync.RWMutex namespaces map[string]*NamespaceFile - ec watcherx.EventChannel - l *logrusx.Logger + logger *logrusx.Logger target string - w watcherx.Watcher + } + + eventHandler interface { + handleRemove(*watcherx.RemoveEvent) + handleChange(*watcherx.ChangeEvent) + handleError(*watcherx.ErrorEvent) } ) var _ namespace.Manager = (*NamespaceWatcher)(nil) func NewNamespaceWatcher(ctx context.Context, l *logrusx.Logger, target string) (*NamespaceWatcher, error) { - u, err := urlx.Parse(target) - if err != nil { - return nil, errors.WithStack(err) - } - nw := NamespaceWatcher{ - ec: make(watcherx.EventChannel), - l: l, + logger: l, target: target, namespaces: make(map[string]*NamespaceFile), } - info, err := os.Stat(u.Path) - if err != nil { - return nil, errors.WithStack(err) + return &nw, watchTarget(ctx, target, &nw, l) +} + +func (nw *NamespaceWatcher) handleRemove(e *watcherx.RemoveEvent) { + nw.Lock() + defer nw.Unlock() + + delete(nw.namespaces, e.Source()) +} + +func (nw *NamespaceWatcher) handleChange(e *watcherx.ChangeEvent) { + // the lock is acquired before parsing to ensure that the getters are + // waiting for the updated values + nw.Lock() + defer nw.Unlock() + + n := nw.readNamespaceFile(e.Reader(), e.Source()) + if n == nil { + return + } else if n.namespace == nil { + // parse failed, rolling back to previous working version + if existing, ok := nw.namespaces[e.Source()]; ok { + existing.Contents = n.Contents + } else { + nw.namespaces[e.Source()] = n + } + } else { + nw.namespaces[e.Source()] = n } +} + +func (nw *NamespaceWatcher) handleError(e *watcherx.ErrorEvent) { + nw.logger. + WithError(e). + Errorf("Received error while watching namespace files at target %s.", nw.target) +} +func watchTarget(ctx context.Context, target string, handler eventHandler, log *logrusx.Logger) error { + var ( + eventCh = make(watcherx.EventChannel) + watcher watcherx.Watcher + ) + + targetUrl, err := urlx.Parse(target) + if err != nil { + return errors.WithStack(err) + } + info, err := os.Stat(targetUrl.Path) + if err != nil { + return errors.WithStack(err) + } if info.IsDir() { - nw.w, err = watcherx.WatchDirectory(ctx, u.Path, nw.ec) + watcher, err = watcherx.WatchDirectory(ctx, targetUrl.Path, eventCh) } else { - nw.w, err = watcherx.Watch(ctx, u, nw.ec) + watcher, err = watcherx.Watch(ctx, targetUrl, eventCh) } // this handles the watcher init error if err != nil { - return nil, err + return err } // trigger initial load - done, err := nw.w.DispatchNow() + done, err := watcher.DispatchNow() if err != nil { - return nil, errors.WithStack(err) + return errors.WithStack(err) } initialEventsProcessed := make(chan struct{}) - go eventHandler(ctx, &nw, done, initialEventsProcessed) + go startEventHandler(ctx, eventCh, handler, done, initialEventsProcessed, log) // wait for initial load to be done <-initialEventsProcessed - return &nw, nil + return nil } -func eventHandler(ctx context.Context, nw *NamespaceWatcher, done <-chan int, initialEventsProcessed chan<- struct{}) { +func startEventHandler(ctx context.Context, + eventCh watcherx.EventChannel, + handler eventHandler, + done <-chan int, + initialEventsProcessed chan<- struct{}, + log *logrusx.Logger) { + initalDone := false for { select { - // because we use an unbuffered chan we can be sure that at least all initial events are handled + // because we use an unbuffered chan we can be sure that at least all + // initial events are handled case <-done: initalDone = true close(initialEventsProcessed) + case <-ctx.Done(): return - case e, open := <-nw.ec: + + case e, open := <-eventCh: if !open { return } if initalDone { - nw.l.WithField("file", e.Source()).WithField("event_type", fmt.Sprintf("%T", e)).Info("A change to a namespace file was detected.") + log. + WithField("file", e.Source()). + WithField("event_type", fmt.Sprintf("%T", e)). + Info("A change to a namespace file was detected.") } - switch etyped := e.(type) { + switch e := e.(type) { case *watcherx.RemoveEvent: - func() { - nw.Lock() - defer nw.Unlock() - - delete(nw.namespaces, e.Source()) - }() + handler.handleRemove(e) case *watcherx.ChangeEvent: - // the lock is acquired before parsing to ensure that the getters are waiting for the updated values - func() { - nw.Lock() - defer nw.Unlock() - - n := readNamespaceFile(nw.l, e.Reader(), e.Source()) - if n == nil { - return - } else if n.namespace == nil { - // parse failed, rolling back to previous working version - if existing, ok := nw.namespaces[e.Source()]; ok { - existing.Contents = n.Contents - } else { - nw.namespaces[e.Source()] = n - } - } else { - nw.namespaces[e.Source()] = n - } - }() + handler.handleChange(e) case *watcherx.ErrorEvent: - nw.l.WithError(etyped).Errorf("Received error while watching namespace files at target %s.", nw.target) + handler.handleError(e) + default: + log.Warnf("Ignored unknown event %T", e) } } } } -func readNamespaceFile(l *logrusx.Logger, r io.Reader, source string) *NamespaceFile { - var parse Parser +func (nw *NamespaceWatcher) readNamespaceFile(r io.Reader, source string) *NamespaceFile { parse, err := GetParser(source) if err != nil { - l.WithError(err).WithField("file_name", source).Warn("could not infer format from file extension") + nw.logger. + WithError(err). + WithField("file_name", source). + Warn("could not infer format from file extension") return nil } raw, err := ioutil.ReadAll(r) if err != nil { - l.WithError(errors.WithStack(err)).WithField("file_name", source).Error("could not read namespace file") + nw.logger. + WithError(errors.WithStack(err)). + WithField("file_name", source). + Error("could not read namespace file") return nil } n := namespace.Namespace{} if err := parse(raw, &n); err != nil { - l.WithError(errors.WithStack(err)).WithField("file_name", source).Error("could not parse namespace file") + nw.logger. + WithError(errors.WithStack(err)). + WithField("file_name", source). + Error("could not parse namespace file") return &NamespaceFile{Name: source, Contents: raw, Parser: parse} } @@ -175,7 +218,8 @@ func (n *NamespaceWatcher) GetNamespaceByName(_ context.Context, name string) (* } } - return nil, errors.WithStack(herodot.ErrNotFound.WithErrorf("Unknown namespace with name %s", name)) + return nil, errors.WithStack(herodot.ErrNotFound.WithErrorf( + "Unknown namespace with name %s", name)) } func (n *NamespaceWatcher) GetNamespaceByConfigID(_ context.Context, id int32) (*namespace.Namespace, error) { @@ -183,12 +227,13 @@ func (n *NamespaceWatcher) GetNamespaceByConfigID(_ context.Context, id int32) ( defer n.RUnlock() for _, nspace := range n.namespaces { - if nspace.namespace.ID == id { + if nspace.namespace.ID == id { // nolint ignore deprecated ID return nspace.namespace, nil } } - return nil, errors.WithStack(herodot.ErrNotFound.WithErrorf("Unknown namespace with ID %d", id)) + return nil, errors.WithStack(herodot.ErrNotFound.WithErrorf( + "Unknown namespace with ID %d", id)) } func (n *NamespaceWatcher) Namespaces(_ context.Context) ([]*namespace.Namespace, error) { diff --git a/internal/driver/config/opl_config_namespace_watcher.go b/internal/driver/config/opl_config_namespace_watcher.go new file mode 100644 index 000000000..40c3372cf --- /dev/null +++ b/internal/driver/config/opl_config_namespace_watcher.go @@ -0,0 +1,99 @@ +package config + +import ( + "context" + "io" + "sync" + + "github.com/ory/x/logrusx" + "github.com/ory/x/watcherx" + + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/schema" +) + +type ( + configFiles struct { + byPath map[string]io.Reader + sync.Mutex + } + + oplConfigWatcher struct { + logger *logrusx.Logger + target string + files configFiles + + memoryNamespaceManager + } +) + +var _ namespace.Manager = (*oplConfigWatcher)(nil) + +func newOPLConfigWatcher(ctx context.Context, l *logrusx.Logger, target string) (*oplConfigWatcher, error) { + + nw := &oplConfigWatcher{ + logger: l, + target: target, + files: configFiles{byPath: make(map[string]io.Reader)}, + memoryNamespaceManager: *NewMemoryNamespaceManager(), + } + + return nw, watchTarget(ctx, target, nw, l) +} + +func (nw *oplConfigWatcher) handleChange(e *watcherx.ChangeEvent) { + // the lock is acquired before parsing to ensure that the getters are + // waiting for the updated values + nw.files.Lock() + defer nw.files.Unlock() + nw.files.byPath[e.Source()] = e.Reader() + nw.parseFiles() +} + +func (nw *oplConfigWatcher) handleRemove(e *watcherx.RemoveEvent) { + nw.files.Lock() + defer nw.files.Unlock() + delete(nw.files.byPath, e.Source()) + nw.parseFiles() +} + +func (nw *oplConfigWatcher) handleError(e *watcherx.ErrorEvent) { + nw.logger. + WithError(e). + Errorf("Received error while watching OPL config files at target %s.", + nw.target) +} + +// parseFiles loops through all files, parsing each and getting the namespaces. +// It then sets the namespaces only if there were no errors. +// +// The caller must hold the lock to nw.files. +func (nw *oplConfigWatcher) parseFiles() { + var ( + namespaces = make([]*namespace.Namespace, 0) + errs []error + ) + for _, reader := range nw.files.byPath { + content, err := io.ReadAll(reader) + if err != nil { + errs = append(errs, err) + continue + } + nn, ee := schema.Parse(string(content)) + errs = append(errs, ee...) + for _, n := range nn { + n := n // alias because we want a reference + namespaces = append(namespaces, &n) + } + } + if len(errs) > 0 { + for _, err := range errs { + nw.logger. + WithError(err). + Errorf("Failed to parse OPL config files at target %s.", + nw.target) + } + return + } + nw.set(namespaces) +} diff --git a/internal/driver/config/provider.go b/internal/driver/config/provider.go index 886a96be4..c78b9c132 100644 --- a/internal/driver/config/provider.go +++ b/internal/driver/config/provider.go @@ -114,12 +114,12 @@ func (k *Config) watcher(_ watcherx.Event, err error) { return } - nn, err := k.getNamespaces() + nnCfg, err := k.namespaceConfig() if err != nil { k.l.WithError(err).Error("could not get namespaces from config") return } - if nm.ShouldReload(nn) { + if nm.ShouldReload(nnCfg.value()) { k.resetNamespaceManager() } } @@ -138,7 +138,7 @@ func (k *Config) resetNamespaceManager() { k.nm, k.cancelNamespaceManager = nil, nil } -func (k *Config) Set(key string, v interface{}) error { +func (k *Config) Set(key string, v any) error { if err := k.p.Set(key, v); err != nil { return err } @@ -212,36 +212,83 @@ func (k *Config) NamespaceManager() (namespace.Manager, error) { var ctx context.Context ctx, k.cancelNamespaceManager = context.WithCancel(k.ctx) - nn, err := k.getNamespaces() + nnCfg, err := k.namespaceConfig() if err != nil { return nil, err } - switch nTyped := nn.(type) { - case string: - var err error - k.nm, err = NewNamespaceWatcher(ctx, k.l, nTyped) - if err != nil { - return nil, err - } - case []*namespace.Namespace: - k.nm = NewMemoryNamespaceManager(nTyped...) - default: - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("got unexpected namespaces type %T", nn)) + k.nm, err = nnCfg.newManager()(ctx, k.l) + if err != nil { + return nil, err } } return k.nm, nil } -// getNamespaces returns string or []*namespace.Namespace -func (k *Config) getNamespaces() (interface{}, error) { +type ( + buildNamespaceFn func(context.Context, *logrusx.Logger) (namespace.Manager, error) + + namespaceConfig interface { + // newManager builds a new namespace manager. + newManager() buildNamespaceFn + // value returns the wrapped value (for comparing if we should reload) + value() any + } + + legacyURINamespaceConfig string + literalNamespaceConfig []*namespace.Namespace + oplNamespaceConfig map[string]any +) + +func (uri legacyURINamespaceConfig) newManager() buildNamespaceFn { + return func(ctx context.Context, l *logrusx.Logger) (namespace.Manager, error) { + return NewNamespaceWatcher(ctx, l, string(uri)) + } +} +func (uri legacyURINamespaceConfig) value() any { + return string(uri) +} + +func (namespaces literalNamespaceConfig) newManager() buildNamespaceFn { + return func(ctx context.Context, l *logrusx.Logger) (namespace.Manager, error) { + return NewMemoryNamespaceManager(namespaces...), nil + } +} +func (namespaces literalNamespaceConfig) value() any { + return []*namespace.Namespace(namespaces) +} + +func (oplConfig oplNamespaceConfig) newManager() buildNamespaceFn { + return func(ctx context.Context, l *logrusx.Logger) (namespace.Manager, error) { + entry, ok := oplConfig["location"] + if !ok { + return nil, errors.New("location key not found") + } + target, ok := entry.(string) + if !ok { + return nil, fmt.Errorf("config value must be string, was %T", entry) + } + return newOPLConfigWatcher(ctx, l, target) + } +} +func (oplConfig oplNamespaceConfig) value() any { + return map[string]any(oplConfig) +} + +// namespaceConfig returns a namespace config, which can be either a URI (in +// which case we want to watch that URI), or a literal list of namespaces (in +// which case we just load them into memory), or a list of URIs referencing OPL +// definitions (in which case we want to watch each URI and parse the content). +func (k *Config) namespaceConfig() (namespaceConfig, error) { switch nTyped := k.p.GetF(KeyNamespaces, "file://./keto_namespaces").(type) { case string: - return nTyped, nil + return legacyURINamespaceConfig(nTyped), nil + case []*namespace.Namespace: - return nTyped, nil - case []interface{}: + return literalNamespaceConfig(nTyped), nil + + case []any: nEnc, err := json.Marshal(nTyped) if err != nil { return nil, errors.WithStack(err) @@ -252,8 +299,11 @@ func (k *Config) getNamespaces() (interface{}, error) { if err := json.Unmarshal(nEnc, &nn); err != nil { return nil, errors.WithStack(err) } + return literalNamespaceConfig(nn), nil + + case map[string]any: + return oplNamespaceConfig(nTyped), nil - return nn, nil default: return nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("could not infer namespaces for type %T", nTyped)) } diff --git a/internal/driver/config/provider_test.go b/internal/driver/config/provider_test.go index bc2f0c38f..029006db8 100644 --- a/internal/driver/config/provider_test.go +++ b/internal/driver/config/provider_test.go @@ -3,54 +3,96 @@ package config import ( "context" "encoding/json" + "fmt" + "os" "testing" - "github.com/ory/keto/embedx" - "github.com/ory/x/configx" - "github.com/ory/x/logrusx" "github.com/sirupsen/logrus/hooks/test" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/ory/keto/embedx" "github.com/ory/keto/internal/namespace" ) -func TestKoanfNamespaceManager(t *testing.T) { - setup := func(t *testing.T) (*test.Hook, *Config) { - hook := test.Hook{} - l := logrusx.New("test", "today", logrusx.WithHook(&hook)) +// createFile writes the content to a temporary file, returning the path. +// Good for testing config files. +func createFile(t *testing.T, content string) (path string) { + t.Helper() - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) + f, err := os.CreateTemp(t.TempDir(), "config-*.yaml") + if err != nil { + t.Fatal(err) + } - p, err := NewDefault(ctx, pflag.NewFlagSet("test", pflag.ContinueOnError), l, configx.SkipValidation()) - require.NoError(t, err) + t.Cleanup(func() { os.Remove(f.Name()) }) - return &hook, p + n, err := f.WriteString(content) + if err != nil { + t.Fatal(err) + } + if n != len(content) { + t.Fatal("failed to write the complete content") } - assertNamespaces := func(t *testing.T, p *Config, nn ...*namespace.Namespace) { - nm, err := p.NamespaceManager() - require.NoError(t, err) + return f.Name() +} - actualNamespaces, err := nm.Namespaces(context.Background()) - require.NoError(t, err) - assert.Equal(t, len(nn), len(actualNamespaces)) +// createFileF writes the content format string with the applied args to a +// temporary file, returning the path. Good for testing config files. +func createFileF(t *testing.T, contentF string, args ...any) (path string) { + return createFile(t, fmt.Sprintf(contentF, args...)) +} - for _, n := range nn { - assert.Contains(t, actualNamespaces, n) - } - } +func setup(t *testing.T, configFile string) (*test.Hook, *Config) { + t.Helper() + hook := test.Hook{} + l := logrusx.New("test", "today", logrusx.WithHook(&hook)) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + config, err := NewDefault( + ctx, + pflag.NewFlagSet("test", pflag.ContinueOnError), + l, + configx.WithConfigFiles(configFile), + ) + require.NoError(t, err) + + return &hook, config +} + +func assertNamespaces(t *testing.T, p *Config, nn ...*namespace.Namespace) { + t.Helper() + nm, err := p.NamespaceManager() + require.NoError(t, err) + actualNamespaces, err := nm.Namespaces(context.Background()) + require.NoError(t, err) + assert.Equal(t, len(nn), len(actualNamespaces)) + assert.ElementsMatch(t, nn, actualNamespaces) +} + +// The new way to configure namespaces is through the Ory Permissions Language. +// We check here that we still support enumerating the namespaces directly in +// the config or through a file reference, in which case there should be no +// rewrites configured. +func TestLegacyNamespaceConfig(t *testing.T) { t.Run("case=creates memory namespace manager when namespaces are set", func(t *testing.T) { - run := func(namespaces []*namespace.Namespace, value interface{}) func(*testing.T) { + config := createFile(t, ` +dsn: memory +namespaces: + - name: n0 + - name: n1 + - name: n2`) + + run := func(namespaces []*namespace.Namespace) func(*testing.T) { return func(t *testing.T) { - _, p := setup(t) - - require.NoError(t, p.Set(KeyNamespaces, value)) + _, p := setup(t, config) assertNamespaces(t, p, namespaces...) @@ -63,15 +105,9 @@ func TestKoanfNamespaceManager(t *testing.T) { } nn := []*namespace.Namespace{ - { - Name: "n0", - }, - { - Name: "n1", - }, - { - Name: "n2", - }, + {Name: "n0"}, + {Name: "n1"}, + {Name: "n2"}, } nnJson, err := json.Marshal(nn) require.NoError(t, err) @@ -80,17 +116,12 @@ func TestKoanfNamespaceManager(t *testing.T) { t.Run( "type=[]*namespace.Namespace", - run(nn, nn), - ) - - t.Run( - "type=[]interface{}", - run(nn, nnValue), + run(nn), ) }) t.Run("case=reloads namespace manager when namespaces are updated using Set()", func(t *testing.T) { - _, p := setup(t) + _, p := setup(t, createFile(t, "dsn: memory")) n0 := &namespace.Namespace{ Name: "n0", @@ -107,9 +138,10 @@ func TestKoanfNamespaceManager(t *testing.T) { }) t.Run("case=creates watcher manager when namespaces is string URL", func(t *testing.T) { - _, p := setup(t) - - require.NoError(t, p.Set(KeyNamespaces, "file://"+t.TempDir())) + _, p := setup(t, createFileF(t, ` +dsn: memory +namespaces: file://%s`, + t.TempDir())) nm, err := p.NamespaceManager() require.NoError(t, err) @@ -127,3 +159,39 @@ func TestKoanfNamespaceManager(t *testing.T) { assert.Same(t, cp, p.p) }) } + +// Test that the namespaces can be configured through the Ory Permission +// Language. +func TestRewritesNamespaceConfig(t *testing.T) { + t.Run("case=one file", func(t *testing.T) { + oplConfig := createFile(t, ` +class User implements Namespace { + related: { + manager: User[] + } +} + +class Group implements Namespace { + related: { + members: (User | Group)[] + } +} + `) + config := createFileF(t, ` +dsn: memory +namespaces: + location: file://%s`, oplConfig) + + _, p := setup(t, config) + nm, err := p.NamespaceManager() + require.NoError(t, err) + namespaces, err := nm.Namespaces(context.Background()) + require.NoError(t, err) + require.Len(t, namespaces, 2) + + names, relationNames := []string{namespaces[0].Name, namespaces[1].Name}, []string{namespaces[0].Relations[0].Name, namespaces[1].Relations[0].Name} + + assert.ElementsMatch(t, names, []string{"User", "Group"}) + assert.ElementsMatch(t, relationNames, []string{"manager", "members"}) + }) +}