Skip to content

Commit 0f32be7

Browse files
committed
materialize wildcard scopes and set rls bypass role
1 parent e9c4aea commit 0f32be7

6 files changed

Lines changed: 54 additions & 280 deletions

File tree

api/v1/permission_types.go

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import (
88
"github.com/flanksource/duty/context"
99
"github.com/flanksource/duty/models"
1010
dutyRBAC "github.com/flanksource/duty/rbac"
11-
"github.com/flanksource/duty/rbac/policy"
12-
"github.com/flanksource/duty/types"
1311
"github.com/google/uuid"
1412
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1513
)
@@ -146,52 +144,9 @@ type PermissionObject struct {
146144
Scopes []dutyRBAC.NamespacedNameIDSelector `json:"scopes,omitempty"`
147145
}
148146

149-
// GlobalObject checks if the object selector semantically maps to a global object
150-
// and returns the corresponding global object if applicable.
151-
// For example:
152-
//
153-
// configs:
154-
// - name: '*'
155-
//
156-
// is interpreted as the object: catalog.
147+
// GlobalObject is deprecated and always returns false.
157148
func (t *PermissionObject) GlobalObject() (string, bool) {
158-
switch {
159-
case t.isWildcardOnly(t.Playbooks, t.Configs, t.Components, t.Connections) && len(t.Views) == 0:
160-
return policy.ObjectPlaybooks, true
161-
case t.isWildcardOnly(t.Configs, t.Playbooks, t.Components, t.Connections) && len(t.Views) == 0:
162-
return policy.ObjectCatalog, true
163-
case t.isWildcardOnly(t.Components, t.Playbooks, t.Configs, t.Connections) && len(t.Views) == 0:
164-
return policy.ObjectTopology, true
165-
case t.isWildcardOnly(t.Connections, t.Playbooks, t.Configs, t.Components) && len(t.Views) == 0:
166-
return policy.ObjectConnection, true
167-
case t.isViewWildcardOnly():
168-
return policy.ObjectViews, true
169-
default:
170-
return "", false
171-
}
172-
}
173-
174-
func (t *PermissionObject) isWildcardOnly(primary []types.ResourceSelector, others ...[]types.ResourceSelector) bool {
175-
for _, other := range others {
176-
if len(other) != 0 {
177-
return false
178-
}
179-
}
180-
181-
return len(primary) == 1 && primary[0].Wildcard()
182-
}
183-
184-
// isViewWildcardOnly checks if the permission object has only a wildcard view selector
185-
// and no other resource selectors
186-
func (t *PermissionObject) isViewWildcardOnly() bool {
187-
// Check that all other selectors are empty
188-
if len(t.Configs) != 0 || len(t.Components) != 0 ||
189-
len(t.Playbooks) != 0 || len(t.Connections) != 0 {
190-
return false
191-
}
192-
193-
// Check that we have exactly one view with wildcard name
194-
return len(t.Views) == 1 && t.Views[0].Name == "*"
149+
return "", false
195150
}
196151

197152
// +kubebuilder:object:generate=true

auth/rls.go

Lines changed: 27 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import (
99

1010
"github.com/flanksource/commons/collections"
1111
"github.com/flanksource/commons/logger"
12+
dutyAPI "github.com/flanksource/duty/api"
1213
"github.com/flanksource/duty/context"
1314
"github.com/flanksource/duty/models"
1415
dutyRBAC "github.com/flanksource/duty/rbac"
1516
"github.com/flanksource/duty/rbac/policy"
1617
"github.com/flanksource/duty/rls"
17-
"github.com/flanksource/duty/types"
1818
"github.com/google/uuid"
19+
"github.com/lib/pq"
1920
"github.com/samber/lo"
2021
"go.opentelemetry.io/otel/trace"
2122
"gorm.io/gorm"
@@ -75,11 +76,26 @@ func WithRLS(ctx context.Context, fn func(context.Context) error) error {
7576
}
7677

7778
if rlsPayload.Disable {
78-
return fn(ctx)
79+
return ctx.Transaction(func(txCtx context.Context, _ trace.Span) error {
80+
role := dutyAPI.DefaultConfig.Postgrest.DBRoleBypass
81+
if role == "" {
82+
role = dutyAPI.DefaultConfig.Postgrest.DBRole
83+
if role != "" {
84+
txCtx.Logger.Warnf("RLS bypass role not configured, using role=%s", role)
85+
}
86+
}
87+
if role == "" {
88+
return fmt.Errorf("role is required")
89+
}
90+
if err := txCtx.DB().Exec(fmt.Sprintf("SET LOCAL ROLE %s", pq.QuoteIdentifier(role))).Error; err != nil {
91+
return err
92+
}
93+
return fn(txCtx)
94+
})
7995
}
8096

8197
return ctx.Transaction(func(txCtx context.Context, _ trace.Span) error {
82-
if err := rlsPayload.SetPostgresSessionRLS(txCtx.DB()); err != nil {
98+
if err := rlsPayload.SetPostgresSessionRLSWithRole(txCtx.DB(), dutyAPI.DefaultConfig.Postgrest.DBRole); err != nil {
8399
return err
84100
}
85101

@@ -110,7 +126,6 @@ func buildRLSPayloadFromScopes(ctx context.Context) (*rls.Payload, error) {
110126
}
111127

112128
scopeIDs := map[uuid.UUID]struct{}{}
113-
wildcards := map[rls.WildcardResourceScope]struct{}{}
114129

115130
for _, perm := range permissions {
116131
if !collections.MatchItems(policy.ActionRead, strings.Split(perm.Action, ",")...) {
@@ -123,19 +138,6 @@ func buildRLSPayloadFromScopes(ctx context.Context) (*rls.Payload, error) {
123138
scopeIDs[permScopeID] = struct{}{}
124139
}
125140

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{}{}
137-
}
138-
139141
if len(perm.ObjectSelector) == 0 {
140142
continue
141143
}
@@ -148,42 +150,26 @@ func buildRLSPayloadFromScopes(ctx context.Context) (*rls.Payload, error) {
148150

149151
// Process scope references (indirect permissions)
150152
if len(selectors.Scopes) > 0 {
151-
if err := processScopeRefs(ctx, selectors.Scopes, scopeIDs, wildcards); err != nil {
153+
if err := processScopeRefs(ctx, selectors.Scopes, scopeIDs); err != nil {
152154
return nil, err
153155
}
154156
}
155157

156158
// Process direct resource selectors (configs, components, playbooks, etc.)
157159
if len(selectors.Configs) > 0 {
158-
if hasWildcardSelector(selectors.Configs) {
159-
wildcards[rls.WildcardResourceScopeConfig] = struct{}{}
160-
} else {
161-
scopeIDs[permScopeID] = struct{}{}
162-
}
160+
scopeIDs[permScopeID] = struct{}{}
163161
}
164162

165163
if len(selectors.Components) > 0 {
166-
if hasWildcardSelector(selectors.Components) {
167-
wildcards[rls.WildcardResourceScopeComponent] = struct{}{}
168-
} else {
169-
scopeIDs[permScopeID] = struct{}{}
170-
}
164+
scopeIDs[permScopeID] = struct{}{}
171165
}
172166

173167
if len(selectors.Playbooks) > 0 {
174-
if hasWildcardSelector(selectors.Playbooks) {
175-
wildcards[rls.WildcardResourceScopePlaybook] = struct{}{}
176-
} else {
177-
scopeIDs[permScopeID] = struct{}{}
178-
}
168+
scopeIDs[permScopeID] = struct{}{}
179169
}
180170

181171
if len(selectors.Views) > 0 {
182-
if hasWildcardViewRef(selectors.Views) {
183-
wildcards[rls.WildcardResourceScopeView] = struct{}{}
184-
} else {
185-
scopeIDs[permScopeID] = struct{}{}
186-
}
172+
scopeIDs[permScopeID] = struct{}{}
187173
}
188174

189175
// TODO: No RLS support for connections yet!
@@ -195,15 +181,14 @@ func buildRLSPayloadFromScopes(ctx context.Context) (*rls.Payload, error) {
195181
}
196182

197183
payload := &rls.Payload{
198-
Scopes: setToSortedUUIDSlice(scopeIDs),
199-
WildcardScopes: setToSortedWildcardSlice(wildcards),
184+
Scopes: setToSortedUUIDSlice(scopeIDs),
200185
}
201186

202187
return payload, nil
203188
}
204189

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 {
190+
// processScopeRefs fetches scopes from database and adds their IDs
191+
func processScopeRefs(ctx context.Context, scopeRefs []dutyRBAC.NamespacedNameIDSelector, scopeIDs map[uuid.UUID]struct{}) error {
207192
for _, ref := range scopeRefs {
208193
var scope models.Scope
209194
err := ctx.DB().
@@ -220,78 +205,11 @@ func processScopeRefs(ctx context.Context, scopeRefs []dutyRBAC.NamespacedNameID
220205
// Always include scope UUID for view row-level grants
221206
scopeIDs[scope.ID] = struct{}{}
222207

223-
var targets []v1.ScopeTarget
224-
if err := json.Unmarshal([]byte(scope.Targets), &targets); err != nil {
225-
ctx.Warnf("failed to unmarshal targets for scope %s: %v", scope.ID, err)
226-
continue
227-
}
228-
229-
for _, target := range targets {
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-
}
259-
}
260-
}
261208
}
262209

263210
return nil
264211
}
265212

266-
func isWildcardScopeSelector(selector *v1.ScopeResourceSelector) bool {
267-
if selector == nil {
268-
return false
269-
}
270-
271-
return selector.Name == "*" &&
272-
selector.Namespace == "" &&
273-
selector.Agent == "" &&
274-
selector.TagSelector == ""
275-
}
276-
277-
func hasWildcardSelector(selectors []types.ResourceSelector) bool {
278-
for _, selector := range selectors {
279-
if selector.Wildcard() {
280-
return true
281-
}
282-
}
283-
return false
284-
}
285-
286-
func hasWildcardViewRef(selectors []dutyRBAC.ViewRef) bool {
287-
for _, selector := range selectors {
288-
if selector.Name == "*" && selector.Namespace == "" && selector.ID == "" {
289-
return true
290-
}
291-
}
292-
return false
293-
}
294-
295213
func setToSortedUUIDSlice(set map[uuid.UUID]struct{}) []uuid.UUID {
296214
if len(set) == 0 {
297215
return nil
@@ -308,20 +226,3 @@ func setToSortedUUIDSlice(set map[uuid.UUID]struct{}) []uuid.UUID {
308226

309227
return out
310228
}
311-
312-
func setToSortedWildcardSlice(set map[rls.WildcardResourceScope]struct{}) []rls.WildcardResourceScope {
313-
if len(set) == 0 {
314-
return nil
315-
}
316-
317-
out := make([]rls.WildcardResourceScope, 0, len(set))
318-
for val := range set {
319-
out = append(out, val)
320-
}
321-
322-
sort.Slice(out, func(i, j int) bool {
323-
return string(out[i]) < string(out[j])
324-
})
325-
326-
return out
327-
}

auth/tokens.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,24 @@ func GetOrCreateJWTToken(ctx context.Context, user *models.Person, sessionId str
7171
return token.(string), nil
7272
}
7373

74+
rlsPayload, err := GetRLSPayload(ctx.WithUser(user))
75+
if err != nil {
76+
return "", ctx.Oops().Wrap(err)
77+
}
78+
79+
role := config.Postgrest.DBRole
80+
if rlsPayload.Disable && config.Postgrest.DBRoleBypass != "" {
81+
role = config.Postgrest.DBRoleBypass
82+
}
83+
7484
// Postgrest makes this jwt available as a session parameter inside postgres.
7585
// We inject the rls payload here and then access it inside postgres using request.jwt.claims parameter.
7686
claims := jwt.MapClaims{
77-
"role": config.Postgrest.DBRole,
87+
"role": role,
7888
"id": user.ID.String(),
7989
}
8090

81-
if rlsPayload, err := GetRLSPayload(ctx.WithUser(user)); err != nil {
82-
return "", ctx.Oops().Wrap(err)
83-
} else if jwtClaim := rlsPayload.JWTClaims(); jwtClaim != nil {
91+
if jwtClaim := rlsPayload.JWTClaims(); jwtClaim != nil {
8492
claims = collections.MergeMap(claims, jwtClaim)
8593
}
8694

db/permissions.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,11 @@ func PersistPermissionFromCRD(ctx context.Context, obj *v1.Permission) error {
4444
Deny: obj.Spec.Deny,
4545
}
4646

47-
// Check if the object selectors semantically match a global object.
48-
if globalObject, ok := obj.Spec.Object.GlobalObject(); ok {
49-
p.Object = globalObject
50-
} else {
51-
selectors, err := json.Marshal(obj.Spec.Object)
52-
if err != nil {
53-
return fmt.Errorf("failed to marshal object: %w", err)
54-
}
55-
p.ObjectSelector = selectors
47+
selectors, err := json.Marshal(obj.Spec.Object)
48+
if err != nil {
49+
return fmt.Errorf("failed to marshal object: %w", err)
5650
}
51+
p.ObjectSelector = selectors
5752

5853
return ctx.DB().Save(&p).Error
5954
}

0 commit comments

Comments
 (0)