From d79dfe246668125fc6628842f23faed6bad901c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Gro=C3=9Fmann?= Date: Fri, 12 Dec 2025 12:31:20 +0100 Subject: [PATCH] feat: scoping algorithm --- x/secrets/scope.go | 142 ++++++++++++++++++ x/secrets/scope_test.go | 309 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 x/secrets/scope.go create mode 100644 x/secrets/scope_test.go diff --git a/x/secrets/scope.go b/x/secrets/scope.go new file mode 100644 index 00000000..81765ee7 --- /dev/null +++ b/x/secrets/scope.go @@ -0,0 +1,142 @@ +package secrets + +import "strings" + +type Scope interface { + Forward(pattern Pattern) (Pattern, bool) +} + +//type ReversibleScope interface { +// Scope +// Reverse(id ID) ID +//} + +type dscope struct { + pattern Pattern +} + +func (d dscope) Forward(pattern Pattern) (Pattern, bool) { + components, _ := forward(split(d.pattern.String()), split(pattern.String())) + fwd, err := ParsePattern(strings.Join(components, "/")) + if err != nil { + return nil, false + } + return fwd, true +} + +func NewDynamicScope(s string) (Scope, error) { + p, err := ParsePattern(s) + if err != nil { + return nil, err + } + return dscope{ + pattern: p, + }, nil +} + +//nolint:gocyclo +func forward(pattern, path []string) ([]string, bool) { + var fwd []string + pi, si := 0, 0 + + for pi < len(pattern) && si < len(path) { + switch pattern[pi] { + case "**": + if pi+1 == len(pattern) { + return append(fwd, path[si:]...), true + } + + if path[si] == "**" { + offset := pi + // replace ** in path with the {*|**} mask coming from pattern + // eg ** could get replaced with **/*, **/*/**, **/*/**/*, etc + for offset+1 < len(pattern) && (pattern[offset+1] == "**" || pattern[offset+1] == "*") { + offset++ + } + next := pi + 1 + if offset > pi { + next = offset + } + + if fwdSub, ok := forward(pattern[next:], path[si:]); ok { + return append(append(fwd, pattern[pi:offset]...), fwdSub...), true + } + + // -> both act as ** + if fwdSub, ok := forward(pattern[pi+1:], path[si+1:]); ok { + return append(append(fwd, "**"), fwdSub...), true + } + + return nil, false + } + + for skip := 0; si+skip <= len(path); skip++ { + if fwdSub, ok := forward(pattern[pi+1:], path[si+skip:]); ok { + return append(append(fwd, path[si:si+skip]...), fwdSub...), true + } + } + return nil, false + case "*": + if path[si] == "**" { + if si+1 == len(path) { + fwd = append(fwd, "*") + pi++ + if pi == len(pattern) { + si++ + } + } else { + if fwdSub, ok := forward(pattern[pi:], append([]string{"*"}, path[si+1:]...)); ok { + return append(fwd, fwdSub...), true + } + if fwdSub, ok := forward(pattern[pi:], path[si+1:]); ok { + return append(fwd, fwdSub...), true + } + return nil, false + } + } else { + fwd = append(fwd, path[si]) + pi++ + si++ + } + default: + switch path[si] { + case "*", pattern[pi]: + pi++ + si++ + case "**": + if pi+1 == len(pattern) { + if si+1 != len(path) { + return nil, false + } + return append(fwd, path[si]), true + } + if si+1 == len(path) { + pi++ + } else { + if fwdSub, ok := forward(pattern[pi+1:], path[si:]); ok { + return append(fwd, fwdSub...), true + } + if fwdSub, ok := forward(pattern[pi:], path[si+1:]); ok { + return append(fwd, fwdSub...), true + } + if fwdSub, ok := forward(pattern[pi:], append([]string{"*"}, path[si+1:]...)); ok { + return append(fwd, fwdSub...), true + } + return nil, false + } + + default: + if pattern[pi] != path[si] { + return nil, false + } + } + + } + } + + if pi < len(pattern) || si < len(path) { + return nil, false + } + + return fwd, true +} diff --git a/x/secrets/scope_test.go b/x/secrets/scope_test.go new file mode 100644 index 00000000..6b49939c --- /dev/null +++ b/x/secrets/scope_test.go @@ -0,0 +1,309 @@ +package secrets + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestScope(t *testing.T) { + type query struct { + input string + fwd string + } + tests := []struct { + scope string + queries []query + }{ + { + scope: "docker/*/mcp/*", + queries: []query{ + { + input: "docker", + }, + { + input: "docker/proj1/mcp/foo", + fwd: "proj1/foo", + }, + { + input: "docker/proj1/mcp/**", + fwd: "proj1/*", + }, + { + input: "docker/*/mcp/*", + fwd: "*/*", + }, + { + input: "**", + fwd: "*/*", + }, + { + input: "docker/proj1/**", + fwd: "proj1/*", + }, + { + input: "docker/proj1/**/bar", + fwd: "proj1/bar", + }, + { + input: "docker/proj1/**/bar/foo", + }, + { + input: "docker/proj1/mcp/**", + fwd: "proj1/*", + }, + }, + }, + { + scope: "docker/**/mcp/*", + queries: []query{ + { + input: "docker", + }, + { + input: "docker/proj1/mcp/foo", + fwd: "proj1/foo", + }, + { + input: "docker/proj1/mcp", + }, + { + input: "docker/mcp/foo", + fwd: "foo", + }, + }, + }, + { + scope: "docker/*/mcp/**", + queries: []query{ + { + input: "docker/proj1/**", + fwd: "proj1/**", + }, + { + input: "docker/proj1/**/bar", + fwd: "proj1/**/bar", + }, + }, + }, + { + scope: "a/b/*", + queries: []query{ + { + input: "**/c", + fwd: "c", + }, + }, + }, + { + scope: "b/*", + queries: []query{ + { + input: "**", + fwd: "*", + }, + { + input: "**/b", + fwd: "b", + }, + }, + }, + { + scope: "*", + queries: []query{ + { + input: "**", + fwd: "*", + }, + { + input: "*", + fwd: "*", + }, + { + input: "a", + fwd: "a", + }, + + { + input: "a/b", + }, + }, + }, + { + scope: "baz", + queries: []query{ + { + input: "**", + fwd: "**", + }, + { + input: "**/baz", + }, + { + input: "foo", + }, + { + input: "**/bar", + }, + }, + }, + { + scope: "**/baz", + queries: []query{ + { + input: "**/baz", + fwd: "**", + }, + { + input: "**", + fwd: "**", + }, + { + input: "**/foo", + }, + }, + }, + { + scope: "**/*", + queries: []query{ + { + input: "**", + fwd: "**/*", + }, + { + input: "b/*", + fwd: "b/*", + }, + }, + }, + { + scope: "**/*/**/*", + queries: []query{ + { + input: "**", + fwd: "**/*/**/*", + }, + }, + }, + { + scope: "**/*/bar/*", + queries: []query{ + { + input: "**", + fwd: "**/*/*", + }, + }, + }, + { + scope: "bar/**/baz", + queries: []query{ + { + input: "**/baz", + fwd: "**", + }, + { + input: "**", + fwd: "**", + }, + { + input: "bar/**/baz", + fwd: "**", + }, + }, + }, + { + scope: "**/bar/**", + queries: []query{ + { + input: "**/baz", + fwd: "**/baz", + }, + { + input: "**/*/bar", + fwd: "**/*/bar", + }, + }, + }, + { + scope: "docker/*/mcp/**/baz", + queries: []query{ + { + input: "docker/proj1/**", + fwd: "proj1/**", + }, + { + input: "docker/proj1/**/bar", + }, + { + input: "docker/proj1/**/baz", + fwd: "proj1/**", + }, + }, + }, + { + scope: "docker/*/mcp/**/*", + queries: []query{ + { + input: "docker/proj1/**", + fwd: "proj1/**/*", + }, + { + input: "docker/proj1/**/*", + fwd: "proj1/**/*", + }, + { + input: "docker/proj1/**/bar", + fwd: "proj1/**/bar", + }, + }, + }, + { + scope: "docker/*/mcp/**/bar/**", + queries: []query{ + { + input: "docker/proj1/**", + fwd: "proj1/**", + }, + { + input: "docker/proj1/**/*", + fwd: "proj1/**/*", + }, + { + input: "docker/proj1/**/*/bar", + fwd: "proj1/**/*/bar", + }, + { + input: "docker/proj1/**/baz", + fwd: "proj1/**/baz", + }, + { + input: "docker/proj1/**/bar", + fwd: "proj1/**/bar", + }, + { + input: "docker/proj1/**/*/*", + fwd: "proj1/**/*/*", + }, + }, + }, + } + for idx, test := range tests { + t.Run(fmt.Sprintf("%d - %s", idx, test.scope), func(t *testing.T) { + s, err := NewDynamicScope(test.scope) + assert.NoError(t, err) + for inner, query := range test.queries { + t.Run(fmt.Sprintf("%d-%d %s %s", idx, inner, test.scope, query.input), func(t *testing.T) { + fwd, ok := s.Forward(MustParsePattern(query.input)) + if query.fwd == "" { + assert.False(t, ok) + return + } + require.True(t, ok, fmt.Sprintf("query: %s, expected out: %s", query.input, query.fwd)) + assert.Equal(t, query.fwd, fwd.String()) + }) + } + }) + } +}