Skip to content
Open
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
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type OPAConfig struct {
MatcherOp string
MatcherSkipTenants string
MatcherAdminGroups string
MatchersConfig string
SSAR bool
ViaQToOTELMigration bool
}
Expand Down Expand Up @@ -110,6 +111,7 @@ func ParseFlags() (*Config, error) {
flag.StringVar(&cfg.Opa.MatcherOp, "opa.matcher-op", "", "When several matchers are supplied (coma-separated string), this is the logical operation to perform. Allowed values: 'and', 'or'.") //nolint:lll
flag.StringVar(&cfg.Opa.MatcherSkipTenants, "opa.skip-tenants", "", "Tenants for which the label matcher should not be set as comma-separated values.")
flag.StringVar(&cfg.Opa.MatcherAdminGroups, "opa.admin-groups", "", "Groups which should be treated as admins and cause the matcher to be omitted.")
flag.StringVar(&cfg.Opa.MatchersConfig, "opa.matchersConfig", "", "Matchers JSON object as string. When specified, this will override all others matcher options.") //nolint:lll
flag.BoolVar(&cfg.Opa.SSAR, "opa.ssar", false, "Use SelftSubjectAccessReview instead of SubjectAccessReview.")
flag.BoolVar(&cfg.Opa.ViaQToOTELMigration, "opa.viaq-to-otel-migration", false, "Enable the ViaQ to OTel migration.")

Expand Down
81 changes: 44 additions & 37 deletions internal/config/matcher.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package config

import (
"maps"
"encoding/json"
"slices"
"strings"
)
Expand All @@ -14,58 +14,69 @@ const (
matchersSeparator = ","
)

type Matchers struct {
ByGroup map[string]Matcher `json:"byGroup,omitempty"`
ByTenant map[string]Matcher `json:"byTenant,omitempty"`
Default Matcher `json:"default,omitempty"`
}

type Matcher struct {
Keys []string
MatcherOp MatcherOp
skipTenants map[string]struct{}
adminGroups map[string]struct{}
Keys []string `json:"keys,omitempty"`
MatcherOp MatcherOp `json:"op,omitempty"`
}

// Return a clone for request-specific modifications
func (m *Matcher) Clone() *Matcher {
return &Matcher{
Keys: slices.Clone(m.Keys),
MatcherOp: m.MatcherOp,
skipTenants: maps.Clone(m.skipTenants),
adminGroups: maps.Clone(m.adminGroups),
Keys: slices.Clone(m.Keys),
MatcherOp: m.MatcherOp,
}
}

func (c *OPAConfig) ToMatcher() Matcher {
matcherKeys := c.Matcher
matcherOp := MatcherOp(c.MatcherOp)
skipTenants := prepareMap(c.MatcherSkipTenants)
adminGroups := prepareMap(c.MatcherAdminGroups)
func (c *OPAConfig) ToMatchers() (*Matchers, error) {
var matchers Matchers
if c.MatchersConfig != "" {
err := json.Unmarshal([]byte(c.MatchersConfig), &matchers)
if err != nil {
return nil, err
}
return &matchers, nil
}

matcher := Matcher{
MatcherOp: matcherOp,
skipTenants: skipTenants,
adminGroups: adminGroups,
var defaultKeys []string
if keys := strings.Split(c.Matcher, matchersSeparator); len(keys) > 0 && keys[0] != "" {
defaultKeys = keys
}

if keys := strings.Split(matcherKeys, matchersSeparator); len(keys) > 0 && keys[0] != "" {
matcher.Keys = keys
matchers = Matchers{
ByGroup: emptyMatchers(c.MatcherAdminGroups),
ByTenant: emptyMatchers(c.MatcherSkipTenants),
Default: Matcher{
Keys: defaultKeys,
MatcherOp: MatcherOp(c.MatcherOp),
},
}

return matcher
return &matchers, nil
}

func prepareMap(csvInput string) map[string]struct{} {
func emptyMatchers(csvInput string) map[string]Matcher {
if csvInput == "" {
return nil
}

tokens := strings.Split(csvInput, ",")

skipMap := make(map[string]struct{}, len(tokens))
matcherMap := make(map[string]Matcher, len(tokens))
for _, token := range tokens {
if token == "" {
continue
}

skipMap[token] = struct{}{}
matcherMap[token] = *EmptyMatcher()
}

return skipMap
return matcherMap
}

func (m *Matcher) IsEmpty() bool {
Expand All @@ -80,22 +91,18 @@ func EmptyMatcher() *Matcher {
return &Matcher{}
}

func (m *Matcher) ForRequest(tenant string, groups []string) *Matcher {
if m.IsEmpty() {
return m
}

if _, skip := m.skipTenants[tenant]; skip {
return EmptyMatcher()
}

func (m *Matchers) ForRequest(tenant string, groups []string) *Matcher {
for _, group := range groups {
if _, admin := m.adminGroups[group]; admin {
return EmptyMatcher()
if m, found := m.ByGroup[group]; found {
return m.Clone()
}
}

return m.Clone() // Return a clone for request-specific modifications
if m, found := m.ByTenant[tenant]; found {
return m.Clone()
}

return m.Default.Clone()
}

func (m *Matcher) ViaQToOTELMigration(selectors map[string][]string) {
Expand Down
111 changes: 109 additions & 2 deletions internal/config/matcher_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"fmt"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -84,11 +85,117 @@ func TestMatcherForRequest(t *testing.T) {
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()

matcher := tc.opaConfig.ToMatcher()
matchers, err := tc.opaConfig.ToMatchers()
require.NoError(t, err)

matcherForRequest := matcher.ForRequest(tc.tenant, tc.groups)
matcherForRequest := matchers.ForRequest(tc.tenant, tc.groups)

require.Equal(t, tc.wantMatcher, matcherForRequest.Keys)
})
}
}

func TestMatchersConfigForRequest(t *testing.T) {
matchersConfigStr := `
{
"byGroup": {
"admin-group": {},
"other-admin-group": {}
},
"byTenant": {
"tenantA": {
"keys": [
"a-test-matcher"
],
"op": "or"
},
"tenantB": {
"keys": [
"b-test-matcher"
],
"op": "and"
},
"tenantC": {
"keys": [
"another-test-matcher"
]
}
},
"default": {
"keys": [
"test-matcher"
]
}
}`

tt := []struct {
desc string
opaConfig OPAConfig
tenant string
groups []string
wantMatcher []string
err error
}{
{
desc: "Matchers config from JSON string",
opaConfig: OPAConfig{
MatchersConfig: matchersConfigStr,
},
tenant: "defaultTenant",
groups: []string{"authenticated"},
wantMatcher: []string{"test-matcher"},
},
{
desc: "Matchers config from JSON string for tenantA",
opaConfig: OPAConfig{
MatchersConfig: matchersConfigStr,
},
tenant: "tenantA",
groups: []string{"authenticated"},
wantMatcher: []string{"a-test-matcher"},
},
{
desc: "Matchers config from JSON string for tenantB",
opaConfig: OPAConfig{
MatchersConfig: matchersConfigStr,
},
tenant: "tenantB",
groups: []string{"authenticated"},
wantMatcher: []string{"b-test-matcher"},
},
{
desc: "Matchers config from JSON string for admin group",
opaConfig: OPAConfig{
MatchersConfig: matchersConfigStr,
},
tenant: "anyTenant",
groups: []string{"admin-group"},
wantMatcher: nil,
},
{
desc: "Invalid config",
opaConfig: OPAConfig{
MatchersConfig: "{ invalid json }",
},
err: fmt.Errorf("invalid character 'i' looking for beginning of object key string"),
},
}

for _, tc := range tt {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()

matchers, err := tc.opaConfig.ToMatchers()
if tc.err != nil {
require.Equal(t, err.Error(), tc.err.Error())
} else {
require.NoError(t, err)

matcherForRequest := matchers.ForRequest(tc.tenant, tc.groups)

require.Equal(t, tc.wantMatcher, matcherForRequest.Keys)
}
})
}
}
12 changes: 9 additions & 3 deletions internal/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,16 @@ func New(l log.Logger, c cache.Cacher, wt transport.WrapperFunc, cfg *config.Con
kubeconfigPath := cfg.KubeconfigPath
tenantAPIGroups := cfg.Mappings
debugToken := cfg.DebugToken
matcher := cfg.Opa.ToMatcher()
matchers, matchersErr := cfg.Opa.ToMatchers()
if matchersErr != nil {
level.Error(l).Log("msg", "failed to parse matchers configuration", "err", matchersErr)
}

return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
if matchers == nil {
http.Error(w, "failed to parse matchers configuration. Check error logs for details", http.StatusInternalServerError)
return //nolint:nlreturn
} else if r.Method != http.MethodPost {
http.Error(w, "request must be a POST", http.StatusBadRequest)
return //nolint:nlreturn
}
Expand Down Expand Up @@ -119,7 +125,7 @@ func New(l log.Logger, c cache.Cacher, wt transport.WrapperFunc, cfg *config.Con
return
}

matcherForRequest := matcher.ForRequest(req.Input.Tenant, req.Input.Groups)
matcherForRequest := matchers.ForRequest(req.Input.Tenant, req.Input.Groups)
extras := req.Input.Extras
if extras.WildcardSelectors && !matcherForRequest.IsEmpty() {
// do not allow wildcards in namespaces for everyone that needs an explicit namespace match
Expand Down