Skip to content

Commit e9c4aea

Browse files
committed
1 parent 89e8370 commit e9c4aea

3 files changed

Lines changed: 121 additions & 90 deletions

File tree

auth/rls.go

Lines changed: 118 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"sort"
8+
"strings"
79

810
"github.com/flanksource/commons/collections"
911
"github.com/flanksource/commons/logger"
@@ -13,6 +15,7 @@ import (
1315
"github.com/flanksource/duty/rbac/policy"
1416
"github.com/flanksource/duty/rls"
1517
"github.com/flanksource/duty/types"
18+
"github.com/google/uuid"
1619
"github.com/samber/lo"
1720
"go.opentelemetry.io/otel/trace"
1821
"gorm.io/gorm"
@@ -106,23 +109,31 @@ func buildRLSPayloadFromScopes(ctx context.Context) (*rls.Payload, error) {
106109
return nil, fmt.Errorf("failed to query permissions: %w", err)
107110
}
108111

109-
payload := &rls.Payload{}
112+
scopeIDs := map[uuid.UUID]struct{}{}
113+
wildcards := map[rls.WildcardResourceScope]struct{}{}
110114

111115
for _, perm := range permissions {
112-
if perm.ConfigID != nil {
113-
payload.Config = append(payload.Config, rls.Scope{ID: perm.ConfigID.String()})
116+
if !collections.MatchItems(policy.ActionRead, strings.Split(perm.Action, ",")...) {
117+
continue
114118
}
115119

116-
if perm.ComponentID != nil {
117-
payload.Component = append(payload.Component, rls.Scope{ID: perm.ComponentID.String()})
118-
}
120+
permScopeID := perm.ID
119121

120-
if perm.PlaybookID != nil {
121-
payload.Playbook = append(payload.Playbook, rls.Scope{ID: perm.PlaybookID.String()})
122+
if perm.ConfigID != nil || perm.ComponentID != nil || perm.PlaybookID != nil || perm.CanaryID != nil {
123+
scopeIDs[permScopeID] = struct{}{}
122124
}
123125

124-
if perm.CanaryID != nil {
125-
payload.Canary = append(payload.Canary, rls.Scope{ID: perm.CanaryID.String()})
126+
switch perm.Object {
127+
case policy.ObjectCatalog:
128+
wildcards[rls.WildcardResourceScopeConfig] = struct{}{}
129+
case policy.ObjectTopology:
130+
wildcards[rls.WildcardResourceScopeComponent] = struct{}{}
131+
case policy.ObjectCanary:
132+
wildcards[rls.WildcardResourceScopeCanary] = struct{}{}
133+
case policy.ObjectPlaybooks:
134+
wildcards[rls.WildcardResourceScopePlaybook] = struct{}{}
135+
case policy.ObjectViews:
136+
wildcards[rls.WildcardResourceScopeView] = struct{}{}
126137
}
127138

128139
if len(perm.ObjectSelector) == 0 {
@@ -137,34 +148,41 @@ func buildRLSPayloadFromScopes(ctx context.Context) (*rls.Payload, error) {
137148

138149
// Process scope references (indirect permissions)
139150
if len(selectors.Scopes) > 0 {
140-
if err := processScopeRefs(ctx, selectors.Scopes, payload); err != nil {
151+
if err := processScopeRefs(ctx, selectors.Scopes, scopeIDs, wildcards); err != nil {
141152
return nil, err
142153
}
143154
}
144155

145156
// Process direct resource selectors (configs, components, playbooks, etc.)
146-
// Only use tags, name, and agent_id as per requirements
147157
if len(selectors.Configs) > 0 {
148-
for _, selector := range selectors.Configs {
149-
payload.Config = append(payload.Config, convertResourceSelectorToRLSScope(selector))
158+
if hasWildcardSelector(selectors.Configs) {
159+
wildcards[rls.WildcardResourceScopeConfig] = struct{}{}
160+
} else {
161+
scopeIDs[permScopeID] = struct{}{}
150162
}
151163
}
152164

153165
if len(selectors.Components) > 0 {
154-
for _, selector := range selectors.Components {
155-
payload.Component = append(payload.Component, convertResourceSelectorToRLSScope(selector))
166+
if hasWildcardSelector(selectors.Components) {
167+
wildcards[rls.WildcardResourceScopeComponent] = struct{}{}
168+
} else {
169+
scopeIDs[permScopeID] = struct{}{}
156170
}
157171
}
158172

159173
if len(selectors.Playbooks) > 0 {
160-
for _, selector := range selectors.Playbooks {
161-
payload.Playbook = append(payload.Playbook, convertResourceSelectorToRLSScope(selector))
174+
if hasWildcardSelector(selectors.Playbooks) {
175+
wildcards[rls.WildcardResourceScopePlaybook] = struct{}{}
176+
} else {
177+
scopeIDs[permScopeID] = struct{}{}
162178
}
163179
}
164180

165181
if len(selectors.Views) > 0 {
166-
for _, viewRef := range selectors.Views {
167-
payload.View = append(payload.View, convertViewScopeRefToRLSScope(viewRef))
182+
if hasWildcardViewRef(selectors.Views) {
183+
wildcards[rls.WildcardResourceScopeView] = struct{}{}
184+
} else {
185+
scopeIDs[permScopeID] = struct{}{}
168186
}
169187
}
170188

@@ -176,11 +194,16 @@ func buildRLSPayloadFromScopes(ctx context.Context) (*rls.Payload, error) {
176194
// }
177195
}
178196

197+
payload := &rls.Payload{
198+
Scopes: setToSortedUUIDSlice(scopeIDs),
199+
WildcardScopes: setToSortedWildcardSlice(wildcards),
200+
}
201+
179202
return payload, nil
180203
}
181204

182-
// processScopeRefs fetches scopes from database and adds their targets to the payload
183-
func processScopeRefs(ctx context.Context, scopeRefs []dutyRBAC.NamespacedNameIDSelector, payload *rls.Payload) error {
205+
// processScopeRefs fetches scopes from database and adds their IDs and wildcard types
206+
func processScopeRefs(ctx context.Context, scopeRefs []dutyRBAC.NamespacedNameIDSelector, scopeIDs map[uuid.UUID]struct{}, wildcards map[rls.WildcardResourceScope]struct{}) error {
184207
for _, ref := range scopeRefs {
185208
var scope models.Scope
186209
err := ctx.DB().
@@ -194,8 +217,8 @@ func processScopeRefs(ctx context.Context, scopeRefs []dutyRBAC.NamespacedNameID
194217
return fmt.Errorf("failed to query scope %s/%s: %w", ref.Namespace, ref.Name, err)
195218
}
196219

197-
// Add scope UUID for view row-level grants
198-
payload.Scopes = append(payload.Scopes, scope.ID.String())
220+
// Always include scope UUID for view row-level grants
221+
scopeIDs[scope.ID] = struct{}{}
199222

200223
var targets []v1.ScopeTarget
201224
if err := json.Unmarshal([]byte(scope.Targets), &targets); err != nil {
@@ -204,93 +227,101 @@ func processScopeRefs(ctx context.Context, scopeRefs []dutyRBAC.NamespacedNameID
204227
}
205228

206229
for _, target := range targets {
207-
if target.Config != nil {
208-
rlsScope := convertToRLSScope(target.Config)
209-
payload.Config = append(payload.Config, rlsScope)
210-
}
211-
if target.Component != nil {
212-
rlsScope := convertToRLSScope(target.Component)
213-
payload.Component = append(payload.Component, rlsScope)
214-
}
215-
if target.Playbook != nil {
216-
rlsScope := convertToRLSScope(target.Playbook)
217-
payload.Playbook = append(payload.Playbook, rlsScope)
218-
}
219-
if target.Canary != nil {
220-
rlsScope := convertToRLSScope(target.Canary)
221-
payload.Canary = append(payload.Canary, rlsScope)
222-
}
223-
if target.View != nil {
224-
rlsScope := convertToRLSScope(target.View)
225-
payload.View = append(payload.View, rlsScope)
226-
}
227-
if target.Global != nil {
228-
rlsScope := convertToRLSScope(target.Global)
229-
payload.Config = append(payload.Config, rlsScope)
230-
payload.Component = append(payload.Component, rlsScope)
231-
payload.Playbook = append(payload.Playbook, rlsScope)
232-
payload.Canary = append(payload.Canary, rlsScope)
233-
payload.View = append(payload.View, rlsScope)
230+
switch {
231+
case target.Config != nil:
232+
if isWildcardScopeSelector(target.Config) {
233+
wildcards[rls.WildcardResourceScopeConfig] = struct{}{}
234+
}
235+
case target.Component != nil:
236+
if isWildcardScopeSelector(target.Component) {
237+
wildcards[rls.WildcardResourceScopeComponent] = struct{}{}
238+
}
239+
case target.Playbook != nil:
240+
if isWildcardScopeSelector(target.Playbook) {
241+
wildcards[rls.WildcardResourceScopePlaybook] = struct{}{}
242+
}
243+
case target.Canary != nil:
244+
if isWildcardScopeSelector(target.Canary) {
245+
wildcards[rls.WildcardResourceScopeCanary] = struct{}{}
246+
}
247+
case target.View != nil:
248+
if isWildcardScopeSelector(target.View) {
249+
wildcards[rls.WildcardResourceScopeView] = struct{}{}
250+
}
251+
case target.Global != nil:
252+
if isWildcardScopeSelector(target.Global) {
253+
wildcards[rls.WildcardResourceScopeConfig] = struct{}{}
254+
wildcards[rls.WildcardResourceScopeComponent] = struct{}{}
255+
wildcards[rls.WildcardResourceScopePlaybook] = struct{}{}
256+
wildcards[rls.WildcardResourceScopeCanary] = struct{}{}
257+
wildcards[rls.WildcardResourceScopeView] = struct{}{}
258+
}
234259
}
235260
}
236261
}
237262

238263
return nil
239264
}
240265

241-
func convertToRLSScope(selector *v1.ScopeResourceSelector) rls.Scope {
242-
rlsScope := rls.Scope{}
243-
244-
if selector.Agent != "" {
245-
rlsScope.Agents = []string{selector.Agent}
266+
func isWildcardScopeSelector(selector *v1.ScopeResourceSelector) bool {
267+
if selector == nil {
268+
return false
246269
}
247270

248-
if selector.Name != "" {
249-
rlsScope.Names = []string{selector.Name}
250-
}
271+
return selector.Name == "*" &&
272+
selector.Namespace == "" &&
273+
selector.Agent == "" &&
274+
selector.TagSelector == ""
275+
}
251276

252-
if selector.TagSelector != "" {
253-
rlsScope.Tags = collections.SelectorToMap(selector.TagSelector)
277+
func hasWildcardSelector(selectors []types.ResourceSelector) bool {
278+
for _, selector := range selectors {
279+
if selector.Wildcard() {
280+
return true
281+
}
254282
}
255-
256-
return rlsScope
283+
return false
257284
}
258285

259-
// convertResourceSelectorToRLSScope converts a types.ResourceSelector to rls.Scope
260-
// Only uses tags, name, and agent_id.
261-
func convertResourceSelectorToRLSScope(selector types.ResourceSelector) rls.Scope {
262-
rlsScope := rls.Scope{}
263-
264-
if selector.Agent != "" {
265-
rlsScope.Agents = []string{selector.Agent}
286+
func hasWildcardViewRef(selectors []dutyRBAC.ViewRef) bool {
287+
for _, selector := range selectors {
288+
if selector.Name == "*" && selector.Namespace == "" && selector.ID == "" {
289+
return true
290+
}
266291
}
292+
return false
293+
}
267294

268-
if selector.Name != "" {
269-
rlsScope.Names = []string{selector.Name}
295+
func setToSortedUUIDSlice(set map[uuid.UUID]struct{}) []uuid.UUID {
296+
if len(set) == 0 {
297+
return nil
270298
}
271299

272-
if selector.TagSelector != "" {
273-
rlsScope.Tags = collections.SelectorToMap(selector.TagSelector)
300+
out := make([]uuid.UUID, 0, len(set))
301+
for val := range set {
302+
out = append(out, val)
274303
}
275304

276-
return rlsScope
277-
}
305+
sort.Slice(out, func(i, j int) bool {
306+
return out[i].String() < out[j].String()
307+
})
278308

279-
// convertViewScopeRefToRLSScope converts a view ViewRef (namespace/name) to rls.Scope
280-
// Views only support id and name in match_scope (namespace is not supported)
281-
func convertViewScopeRefToRLSScope(viewRef dutyRBAC.ViewRef) rls.Scope {
282-
rlsScope := rls.Scope{}
309+
return out
310+
}
283311

284-
if viewRef.Name != "" {
285-
rlsScope.Names = []string{viewRef.Name}
312+
func setToSortedWildcardSlice(set map[rls.WildcardResourceScope]struct{}) []rls.WildcardResourceScope {
313+
if len(set) == 0 {
314+
return nil
286315
}
287316

288-
if viewRef.ID != "" {
289-
rlsScope.ID = viewRef.ID
317+
out := make([]rls.WildcardResourceScope, 0, len(set))
318+
for val := range set {
319+
out = append(out, val)
290320
}
291321

292-
// Note: namespace is not supported by match_scope for views
293-
// ID would be set if we have a direct ID reference, but ViewRef doesn't have ID field
322+
sort.Slice(out, func(i, j int) bool {
323+
return string(out[i]) < string(out[j])
324+
})
294325

295-
return rlsScope
326+
return out
296327
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/containrrr/shoutrrr v0.8.0
1313
github.com/fergusstrange/embedded-postgres v1.32.0 // indirect
1414
github.com/flanksource/commons v1.44.0
15-
github.com/flanksource/duty v1.0.1159
15+
github.com/flanksource/duty v1.0.1157-0.20260114123018-b5680731a586 // temporary from https://github.com/flanksource/duty/pull/1729
1616
github.com/flanksource/gomplate/v3 v3.24.66
1717
github.com/flanksource/kopper v1.0.14
1818
github.com/gomarkdown/markdown v0.0.0-20250810172220-2e2c11897d1a

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,8 @@ github.com/flanksource/commons v1.44.0 h1:u+4fARnBEpes/s9nM+Ll0vRl6grU7mjYY/1oYf
385385
github.com/flanksource/commons v1.44.0/go.mod h1:xCvBGp3f9N/Y4Iab9lgzjsN90zFiHvlaOgWtfH321Pg=
386386
github.com/flanksource/deps v1.0.20 h1:HHNSBxrtHgEuKuxXO5TPCI8KYlmIVjyCRzAMXxlPzAU=
387387
github.com/flanksource/deps v1.0.20/go.mod h1:74gGDlS75O9jAIAlKrKd+qSvY6sO5KBa5Y1VT9aYTPo=
388-
github.com/flanksource/duty v1.0.1159 h1:cTOSe6uJw+e+ISE05QmvqZ8/vxitB3UxaNzV26Tdg8E=
389-
github.com/flanksource/duty v1.0.1159/go.mod h1:OXLsjmkDrJaMjpPNYYbfAuoqVNZLoIeYn6JgclDGF1A=
388+
github.com/flanksource/duty v1.0.1157-0.20260114123018-b5680731a586 h1:fv+iP36hS4g+9Wy6uAJvXU5kPO3opFpwEpjg8XLeq4E=
389+
github.com/flanksource/duty v1.0.1157-0.20260114123018-b5680731a586/go.mod h1:pi6/7DTnpOWgsrNE+fYhOSTVFZJ789knV/H5XeJZkqA=
390390
github.com/flanksource/gomplate/v3 v3.24.66 h1:fTaN0s9t+YZCau+KlgcLn9pMcLTsSiMjBnZUbhGY/oY=
391391
github.com/flanksource/gomplate/v3 v3.24.66/go.mod h1:PiYJOAk971BpG/suhFP9YAZSjfz4KiRaqwYlQZZJp0Q=
392392
github.com/flanksource/is-healthy v1.0.82 h1:/hjq2hYWVph2Cr6F6qF4v/vTuEqOqgPVnZOh1kfDwvg=

0 commit comments

Comments
 (0)