|
1
|
|
|
package engines |
|
2
|
|
|
|
|
3
|
|
|
import ( |
|
4
|
|
|
"context" |
|
5
|
|
|
"errors" |
|
6
|
|
|
"fmt" |
|
7
|
|
|
"sync" |
|
8
|
|
|
|
|
9
|
|
|
"github.com/google/cel-go/cel" |
|
10
|
|
|
|
|
11
|
|
|
"github.com/Permify/permify/internal/invoke" |
|
12
|
|
|
"github.com/Permify/permify/internal/schema" |
|
13
|
|
|
"github.com/Permify/permify/internal/storage" |
|
14
|
|
|
storageContext "github.com/Permify/permify/internal/storage/context" |
|
15
|
|
|
"github.com/Permify/permify/pkg/database" |
|
16
|
|
|
"github.com/Permify/permify/pkg/dsl/utils" |
|
17
|
|
|
base "github.com/Permify/permify/pkg/pb/base/v1" |
|
18
|
|
|
"github.com/Permify/permify/pkg/tuple" |
|
19
|
|
|
) |
|
20
|
|
|
|
|
21
|
|
|
// CheckEngine is a core component responsible for performing permission checks. |
|
22
|
|
|
// It reads schema and relationship information, and uses the engine key manager |
|
23
|
|
|
// to validate permission requests. |
|
24
|
|
|
type CheckEngine struct { |
|
25
|
|
|
// delegate is responsible for performing permission checks |
|
26
|
|
|
invoker invoke.Check |
|
27
|
|
|
// schemaReader is responsible for reading schema information |
|
28
|
|
|
schemaReader storage.SchemaReader |
|
29
|
|
|
// relationshipReader is responsible for reading relationship information |
|
30
|
|
|
dataReader storage.DataReader |
|
31
|
|
|
// concurrencyLimit is the maximum number of concurrent permission checks allowed |
|
32
|
|
|
concurrencyLimit int |
|
33
|
|
|
} |
|
34
|
|
|
|
|
35
|
|
|
// NewCheckEngine creates a new CheckEngine instance for performing permission checks. |
|
36
|
|
|
// It takes a key manager, schema reader, and relationship reader as parameters. |
|
37
|
|
|
// Additionally, it allows for optional configuration through CheckOption function arguments. |
|
38
|
|
|
func NewCheckEngine(sr storage.SchemaReader, rr storage.DataReader, opts ...CheckOption) *CheckEngine { |
|
39
|
|
|
// Initialize a CheckEngine with default concurrency limit and provided parameters |
|
40
|
|
|
engine := &CheckEngine{ |
|
41
|
|
|
schemaReader: sr, |
|
42
|
|
|
dataReader: rr, |
|
43
|
|
|
concurrencyLimit: _defaultConcurrencyLimit, |
|
44
|
|
|
} |
|
45
|
|
|
|
|
46
|
|
|
// Apply provided options to configure the CheckEngine |
|
47
|
|
|
for _, opt := range opts { |
|
48
|
|
|
opt(engine) |
|
49
|
|
|
} |
|
50
|
|
|
|
|
51
|
|
|
return engine |
|
52
|
|
|
} |
|
53
|
|
|
|
|
54
|
|
|
// SetInvoker sets the delegate for the CheckEngine. |
|
55
|
|
|
func (engine *CheckEngine) SetInvoker(invoker invoke.Check) { |
|
56
|
|
|
engine.invoker = invoker |
|
57
|
|
|
} |
|
58
|
|
|
|
|
59
|
|
|
// Check executes a permission check based on the provided request. |
|
60
|
|
|
// The permission field in the request can either be a relation or an permission. |
|
61
|
|
|
// This function performs various checks and returns the permission check response |
|
62
|
|
|
// along with any errors that may have occurred. |
|
63
|
|
|
func (engine *CheckEngine) Check(ctx context.Context, request *base.PermissionCheckRequest) (response *base.PermissionCheckResponse, err error) { |
|
64
|
|
|
emptyResp := denied(emptyResponseMetadata()) |
|
65
|
|
|
|
|
66
|
|
|
// Retrieve entity definition |
|
67
|
|
|
var en *base.EntityDefinition |
|
68
|
|
|
en, _, err = engine.schemaReader.ReadEntityDefinition(ctx, request.GetTenantId(), request.GetEntity().GetType(), request.GetMetadata().GetSchemaVersion()) |
|
69
|
|
|
if err != nil { |
|
70
|
|
|
return emptyResp, err |
|
71
|
|
|
} |
|
72
|
|
|
|
|
73
|
|
|
// Perform permission check |
|
74
|
|
|
var res *base.PermissionCheckResponse |
|
75
|
|
|
res, err = engine.check(ctx, request, en)(ctx) |
|
76
|
|
|
if err != nil { |
|
77
|
|
|
return emptyResp, err |
|
78
|
|
|
} |
|
79
|
|
|
|
|
80
|
|
|
return &base.PermissionCheckResponse{ |
|
81
|
|
|
Can: res.Can, |
|
82
|
|
|
Metadata: res.Metadata, |
|
83
|
|
|
}, nil |
|
84
|
|
|
} |
|
85
|
|
|
|
|
86
|
|
|
// CheckFunction is a type that represents a function that takes a context |
|
87
|
|
|
// and returns a PermissionCheckResponse along with an error. It is used |
|
88
|
|
|
// to perform individual permission checks within the CheckEngine. |
|
89
|
|
|
type CheckFunction func(ctx context.Context) (*base.PermissionCheckResponse, error) |
|
90
|
|
|
|
|
91
|
|
|
// CheckCombiner is a type that represents a function which takes a context, |
|
92
|
|
|
// a slice of CheckFunctions, and a limit. It combines the results of |
|
93
|
|
|
// multiple CheckFunctions according to a specific strategy and returns |
|
94
|
|
|
// a PermissionCheckResponse along with an error. |
|
95
|
|
|
type CheckCombiner func(ctx context.Context, functions []CheckFunction, limit int) (*base.PermissionCheckResponse, error) |
|
96
|
|
|
|
|
97
|
|
|
// run is a helper function that takes a context and a PermissionCheckRequest, |
|
98
|
|
|
// and returns a CheckFunction. The returned CheckFunction, when called with |
|
99
|
|
|
// a context, executes the Run method of the CheckEngine with the given |
|
100
|
|
|
// request, and returns the resulting PermissionCheckResponse and error. |
|
101
|
|
|
func (engine *CheckEngine) invoke(request *base.PermissionCheckRequest) CheckFunction { |
|
102
|
|
|
return func(ctx context.Context) (*base.PermissionCheckResponse, error) { |
|
103
|
|
|
return engine.invoker.Check(ctx, request) |
|
104
|
|
|
} |
|
105
|
|
|
} |
|
106
|
|
|
|
|
107
|
|
|
// check constructs a CheckFunction that performs permission checks based on the type of reference in the entity definition. |
|
108
|
|
|
func (engine *CheckEngine) check( |
|
109
|
|
|
ctx context.Context, |
|
110
|
|
|
request *base.PermissionCheckRequest, |
|
111
|
|
|
en *base.EntityDefinition, |
|
112
|
|
|
) CheckFunction { |
|
113
|
|
|
// If the request's entity and permission are the same as the subject, return a CheckFunction that always allows the permission. |
|
114
|
|
|
if tuple.AreQueryAndSubjectEqual(request.GetEntity(), request.GetPermission(), request.GetSubject()) { |
|
115
|
|
|
return func(ctx context.Context) (*base.PermissionCheckResponse, error) { |
|
116
|
|
|
return allowed(emptyResponseMetadata()), nil |
|
117
|
|
|
} |
|
118
|
|
|
} |
|
119
|
|
|
|
|
120
|
|
|
// Declare a CheckFunction variable that will later be defined based on the type of reference. |
|
121
|
|
|
var fn CheckFunction |
|
122
|
|
|
|
|
123
|
|
|
// Determine the type of the reference by name in the given entity definition. |
|
124
|
|
|
tor, _ := schema.GetTypeOfReferenceByNameInEntityDefinition(en, request.GetPermission()) |
|
125
|
|
|
|
|
126
|
|
|
// Based on the type of the reference, define the CheckFunction in different ways. |
|
127
|
|
|
switch tor { |
|
128
|
|
|
case base.EntityDefinition_REFERENCE_PERMISSION: |
|
129
|
|
|
// Get the permission from the entity definition. |
|
130
|
|
|
permission, err := schema.GetPermissionByNameInEntityDefinition(en, request.GetPermission()) |
|
131
|
|
|
if err != nil { |
|
132
|
|
|
// If an error is encountered while getting the permission, a CheckFunction is returned that always fails with this error. |
|
133
|
|
|
return checkFail(err) |
|
134
|
|
|
} |
|
135
|
|
|
// Get the child of the permission. |
|
136
|
|
|
child := permission.GetChild() |
|
137
|
|
|
|
|
138
|
|
|
// If the child has a rewrite, check the rewrite. |
|
139
|
|
|
// If not, check the leaf. |
|
140
|
|
|
if child.GetRewrite() != nil { |
|
141
|
|
|
fn = engine.checkRewrite(ctx, request, child.GetRewrite()) |
|
142
|
|
|
} else { |
|
143
|
|
|
fn = engine.checkLeaf(request, child.GetLeaf()) |
|
144
|
|
|
} |
|
145
|
|
|
case base.EntityDefinition_REFERENCE_ATTRIBUTE: |
|
146
|
|
|
// If the reference is an attribute, check the direct attribute. |
|
147
|
|
|
fn = engine.checkDirectAttribute(request) |
|
148
|
|
|
case base.EntityDefinition_REFERENCE_RELATION: |
|
149
|
|
|
// If the reference is a relation, check the direct relation. |
|
150
|
|
|
fn = engine.checkDirectRelation(request) |
|
151
|
|
|
default: |
|
152
|
|
|
fn = engine.checkDirectCall(request) |
|
153
|
|
|
} |
|
154
|
|
|
|
|
155
|
|
|
// If the CheckFunction is still undefined after the switch, return a CheckFunction that always fails with an error indicating an undefined child kind. |
|
156
|
|
|
if fn == nil { |
|
157
|
|
|
return checkFail(errors.New(base.ErrorCode_ERROR_CODE_UNDEFINED_CHILD_KIND.String())) |
|
158
|
|
|
} |
|
159
|
|
|
|
|
160
|
|
|
// Otherwise, return a CheckFunction that checks a union of CheckFunctions with a concurrency limit. |
|
161
|
|
|
return func(ctx context.Context) (*base.PermissionCheckResponse, error) { |
|
162
|
|
|
return checkUnion(ctx, []CheckFunction{fn}, engine.concurrencyLimit) |
|
163
|
|
|
} |
|
164
|
|
|
} |
|
165
|
|
|
|
|
166
|
|
|
// checkRewrite prepares a CheckFunction according to the provided Rewrite operation. |
|
167
|
|
|
// It uses a Rewrite object that describes how to combine the results of multiple CheckFunctions. |
|
168
|
|
|
func (engine *CheckEngine) checkRewrite(ctx context.Context, request *base.PermissionCheckRequest, rewrite *base.Rewrite) CheckFunction { |
|
169
|
|
|
// Switch statement depending on the Rewrite operation |
|
170
|
|
|
switch rewrite.GetRewriteOperation() { |
|
171
|
|
|
// In case of UNION operation, set the children CheckFunctions to be run concurrently |
|
172
|
|
|
// and return the permission if any of the CheckFunctions succeeds (union). |
|
173
|
|
|
case *base.Rewrite_OPERATION_UNION.Enum(): |
|
174
|
|
|
return engine.setChild(ctx, request, rewrite.GetChildren(), checkUnion) |
|
175
|
|
|
// In case of INTERSECTION operation, set the children CheckFunctions to be run concurrently |
|
176
|
|
|
// and return the permission if all the CheckFunctions succeed (intersection). |
|
177
|
|
|
case *base.Rewrite_OPERATION_INTERSECTION.Enum(): |
|
178
|
|
|
return engine.setChild(ctx, request, rewrite.GetChildren(), checkIntersection) |
|
179
|
|
|
// In case of EXCLUSION operation, set the children CheckFunctions to be run concurrently |
|
180
|
|
|
// and return the permission if the first CheckFunction succeeds and all others fail (exclusion). |
|
181
|
|
|
case *base.Rewrite_OPERATION_EXCLUSION.Enum(): |
|
182
|
|
|
return engine.setChild(ctx, request, rewrite.GetChildren(), checkExclusion) |
|
183
|
|
|
// In case of an undefined child type, return a CheckFunction that always fails. |
|
184
|
|
|
default: |
|
185
|
|
|
return checkFail(errors.New(base.ErrorCode_ERROR_CODE_UNDEFINED_CHILD_TYPE.String())) |
|
186
|
|
|
} |
|
187
|
|
|
} |
|
188
|
|
|
|
|
189
|
|
|
// checkLeaf prepares a CheckFunction according to the provided Leaf operation. |
|
190
|
|
|
// It uses a Leaf object that describes how to check a permission request. |
|
191
|
|
|
func (engine *CheckEngine) checkLeaf(request *base.PermissionCheckRequest, leaf *base.Leaf) CheckFunction { |
|
192
|
|
|
// Switch statement depending on the Leaf type |
|
193
|
|
|
switch op := leaf.GetType().(type) { |
|
194
|
|
|
// In case of TupleToUserSet operation, prepare a CheckFunction that checks |
|
195
|
|
|
// if the request's user is in the UserSet referenced by the tuple. |
|
196
|
|
|
case *base.Leaf_TupleToUserSet: |
|
197
|
|
|
return engine.checkTupleToUserSet(request, op.TupleToUserSet) |
|
198
|
|
|
// In case of ComputedUserSet operation, prepare a CheckFunction that checks |
|
199
|
|
|
// if the request's user is in the computed UserSet. |
|
200
|
|
|
case *base.Leaf_ComputedUserSet: |
|
201
|
|
|
return engine.checkComputedUserSet(request, op.ComputedUserSet) |
|
202
|
|
|
// In case of ComputedAttribute operation, prepare a CheckFunction that checks |
|
203
|
|
|
// the computed attribute's permission. |
|
204
|
|
|
case *base.Leaf_ComputedAttribute: |
|
205
|
|
|
return engine.checkComputedAttribute(request, op.ComputedAttribute) |
|
206
|
|
|
// In case of Call operation, prepare a CheckFunction that checks |
|
207
|
|
|
// the Call's permission. |
|
208
|
|
|
case *base.Leaf_Call: |
|
209
|
|
|
return engine.checkCall(request, op.Call) |
|
210
|
|
|
// In case of an undefined type, return a CheckFunction that always fails. |
|
211
|
|
|
default: |
|
212
|
|
|
return checkFail(errors.New(base.ErrorCode_ERROR_CODE_UNDEFINED_CHILD_TYPE.String())) |
|
213
|
|
|
} |
|
214
|
|
|
} |
|
215
|
|
|
|
|
216
|
|
|
// setChild prepares a CheckFunction according to the provided combiner function |
|
217
|
|
|
// and children. It uses the Child object which contains the information about the child |
|
218
|
|
|
// nodes and can be either a Rewrite or a Leaf. |
|
219
|
|
|
func (engine *CheckEngine) setChild( |
|
220
|
|
|
ctx context.Context, |
|
221
|
|
|
request *base.PermissionCheckRequest, |
|
222
|
|
|
children []*base.Child, |
|
223
|
|
|
combiner CheckCombiner, |
|
224
|
|
|
) CheckFunction { |
|
225
|
|
|
// Create a slice to store the CheckFunctions |
|
226
|
|
|
functions := make([]CheckFunction, 0, len(children)) |
|
227
|
|
|
// Loop over each child node |
|
228
|
|
|
for _, child := range children { |
|
229
|
|
|
// Switch on the type of the child node |
|
230
|
|
|
switch child.GetType().(type) { |
|
231
|
|
|
// In case of a Rewrite node, create a CheckFunction for the Rewrite and append it |
|
232
|
|
|
case *base.Child_Rewrite: |
|
233
|
|
|
functions = append(functions, engine.checkRewrite(ctx, request, child.GetRewrite())) |
|
234
|
|
|
// In case of a Leaf node, create a CheckFunction for the Leaf and append it |
|
235
|
|
|
case *base.Child_Leaf: |
|
236
|
|
|
functions = append(functions, engine.checkLeaf(request, child.GetLeaf())) |
|
237
|
|
|
// In case of an undefined type, return a CheckFunction that always fails |
|
238
|
|
|
default: |
|
239
|
|
|
return checkFail(errors.New(base.ErrorCode_ERROR_CODE_UNDEFINED_CHILD_TYPE.String())) |
|
240
|
|
|
} |
|
241
|
|
|
} |
|
242
|
|
|
|
|
243
|
|
|
// Return a function that when called, runs the appropriate combiner function |
|
244
|
|
|
// (union, intersection, exclusion) on the prepared CheckFunctions with the provided concurrency limit |
|
245
|
|
|
return func(ctx context.Context) (*base.PermissionCheckResponse, error) { |
|
246
|
|
|
return combiner(ctx, functions, engine.concurrencyLimit) |
|
247
|
|
|
} |
|
248
|
|
|
} |
|
249
|
|
|
|
|
250
|
|
|
// checkDirectRelation is a method of CheckEngine struct that returns a CheckFunction. |
|
251
|
|
|
// It's responsible for directly checking the permissions on an entity |
|
252
|
|
|
func (engine *CheckEngine) checkDirectRelation(request *base.PermissionCheckRequest) CheckFunction { |
|
253
|
|
|
// The returned CheckFunction is a closure over the provided context and request |
|
254
|
|
|
return func(ctx context.Context) (result *base.PermissionCheckResponse, err error) { |
|
255
|
|
|
// Define a TupleFilter. This specifies which tuples we're interested in. |
|
256
|
|
|
// We want tuples that match the entity type and ID from the request, and have a specific relation. |
|
257
|
|
|
filter := &base.TupleFilter{ |
|
258
|
|
|
Entity: &base.EntityFilter{ |
|
259
|
|
|
Type: request.GetEntity().GetType(), |
|
260
|
|
|
Ids: []string{request.GetEntity().GetId()}, |
|
261
|
|
|
}, |
|
262
|
|
|
Relation: request.GetPermission(), |
|
263
|
|
|
} |
|
264
|
|
|
|
|
265
|
|
|
// Use the filter to query for relationships in the given context. |
|
266
|
|
|
// NewContextualRelationships() creates a ContextualRelationships instance from tuples in the request. |
|
267
|
|
|
// QueryRelationships() then uses the filter to find and return matching relationships. |
|
268
|
|
|
var cti *database.TupleIterator |
|
269
|
|
|
cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, database.NewCursorPagination()) |
|
270
|
|
|
if err != nil { |
|
271
|
|
|
// If an error occurred while querying, return a "denied" response and the error. |
|
272
|
|
|
return denied(emptyResponseMetadata()), err |
|
273
|
|
|
} |
|
274
|
|
|
|
|
275
|
|
|
// Query the relationships for the entity in the request. |
|
276
|
|
|
// TupleFilter helps in filtering out the relationships for a specific entity and a permission. |
|
277
|
|
|
var rit *database.TupleIterator |
|
278
|
|
|
rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), database.NewCursorPagination()) |
|
279
|
|
|
// If there's an error in querying, return a denied permission response along with the error. |
|
280
|
|
|
if err != nil { |
|
281
|
|
|
return denied(emptyResponseMetadata()), err |
|
282
|
|
|
} |
|
283
|
|
|
|
|
284
|
|
|
// Create a new UniqueTupleIterator from the two TupleIterators. |
|
285
|
|
|
// NewUniqueTupleIterator() ensures that the iterator only returns unique tuples. |
|
286
|
|
|
it := database.NewUniqueTupleIterator(rit, cti) |
|
287
|
|
|
|
|
288
|
|
|
// Define a slice of CheckFunctions to hold the check functions for each subject. |
|
289
|
|
|
checkFunctions := make([]CheckFunction, 0, 4) |
|
290
|
|
|
// Iterate over all tuples returned by the iterator. |
|
291
|
|
|
for it.HasNext() { |
|
292
|
|
|
// Get the next tuple's subject. |
|
293
|
|
|
next, ok := it.GetNext() |
|
294
|
|
|
if !ok { |
|
295
|
|
|
break |
|
296
|
|
|
} |
|
297
|
|
|
subject := next.GetSubject() |
|
298
|
|
|
|
|
299
|
|
|
// If the subject of the tuple is the same as the subject in the request, permission is allowed. |
|
300
|
|
|
if tuple.AreSubjectsEqual(subject, request.GetSubject()) { |
|
301
|
|
|
return allowed(emptyResponseMetadata()), nil |
|
302
|
|
|
} |
|
303
|
|
|
// If the subject is not a user and the relation is not ELLIPSIS, append a check function to the list. |
|
304
|
|
|
if !tuple.IsDirectSubject(subject) && subject.GetRelation() != tuple.ELLIPSIS { |
|
305
|
|
|
checkFunctions = append(checkFunctions, engine.invoke(&base.PermissionCheckRequest{ |
|
306
|
|
|
TenantId: request.GetTenantId(), |
|
307
|
|
|
Entity: &base.Entity{ |
|
308
|
|
|
Type: subject.GetType(), |
|
309
|
|
|
Id: subject.GetId(), |
|
310
|
|
|
}, |
|
311
|
|
|
Permission: subject.GetRelation(), |
|
312
|
|
|
Subject: request.GetSubject(), |
|
313
|
|
|
Metadata: request.GetMetadata(), |
|
314
|
|
|
Context: request.GetContext(), |
|
315
|
|
|
})) |
|
316
|
|
|
} |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
// If there's any CheckFunction in the list, return the union of all CheckFunctions |
|
320
|
|
|
if len(checkFunctions) > 0 { |
|
321
|
|
|
return checkUnion(ctx, checkFunctions, engine.concurrencyLimit) |
|
322
|
|
|
} |
|
323
|
|
|
|
|
324
|
|
|
// If there's no CheckFunction, return a denied permission response. |
|
325
|
|
|
return denied(emptyResponseMetadata()), nil |
|
326
|
|
|
} |
|
327
|
|
|
} |
|
328
|
|
|
|
|
329
|
|
|
// checkTupleToUserSet is a method of CheckEngine that checks permissions using the |
|
330
|
|
|
// TupleToUserSet data structure. It returns a CheckFunction closure that does the check. |
|
331
|
|
|
func (engine *CheckEngine) checkTupleToUserSet( |
|
332
|
|
|
request *base.PermissionCheckRequest, |
|
333
|
|
|
ttu *base.TupleToUserSet, |
|
334
|
|
|
) CheckFunction { |
|
335
|
|
|
// The returned CheckFunction is a closure over the provided context, request, and ttu. |
|
336
|
|
|
return func(ctx context.Context) (*base.PermissionCheckResponse, error) { |
|
337
|
|
|
// Define a TupleFilter. This specifies which tuples we're interested in. |
|
338
|
|
|
// We want tuples that match the entity type and ID from the request, and have a specific relation. |
|
339
|
|
|
filter := &base.TupleFilter{ |
|
340
|
|
|
Entity: &base.EntityFilter{ |
|
341
|
|
|
Type: request.GetEntity().GetType(), // Filter by entity type from request |
|
342
|
|
|
Ids: []string{request.GetEntity().GetId()}, // Filter by entity ID from request |
|
343
|
|
|
}, |
|
344
|
|
|
Relation: ttu.GetTupleSet().GetRelation(), // Filter by relation from tuple set |
|
345
|
|
|
} |
|
346
|
|
|
|
|
347
|
|
|
// Use the filter to query for relationships in the given context. |
|
348
|
|
|
// NewContextualRelationships() creates a ContextualRelationships instance from tuples in the request. |
|
349
|
|
|
// QueryRelationships() then uses the filter to find and return matching relationships. |
|
350
|
|
|
cti, err := storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, database.NewCursorPagination()) |
|
351
|
|
|
if err != nil { |
|
352
|
|
|
// If an error occurred while querying, return a "denied" response and the error. |
|
353
|
|
|
return denied(emptyResponseMetadata()), err |
|
354
|
|
|
} |
|
355
|
|
|
|
|
356
|
|
|
// Use the filter to query for relationships in the database. |
|
357
|
|
|
// relationshipReader.QueryRelationships() uses the filter to find and return matching relationships. |
|
358
|
|
|
rit, err := engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), database.NewCursorPagination()) |
|
359
|
|
|
if err != nil { |
|
360
|
|
|
// If an error occurred while querying, return a "denied" response and the error. |
|
361
|
|
|
return denied(emptyResponseMetadata()), err |
|
362
|
|
|
} |
|
363
|
|
|
|
|
364
|
|
|
// Create a new UniqueTupleIterator from the two TupleIterators. |
|
365
|
|
|
// NewUniqueTupleIterator() ensures that the iterator only returns unique tuples. |
|
366
|
|
|
it := database.NewUniqueTupleIterator(rit, cti) |
|
367
|
|
|
|
|
368
|
|
|
// Define a slice of CheckFunctions to hold the check functions for each subject. |
|
369
|
|
|
checkFunctions := make([]CheckFunction, 0, 4) |
|
370
|
|
|
// Iterate over all tuples returned by the iterator. |
|
371
|
|
|
for it.HasNext() { |
|
372
|
|
|
// Get the next tuple's subject. |
|
373
|
|
|
next, ok := it.GetNext() |
|
374
|
|
|
if !ok { |
|
375
|
|
|
break |
|
376
|
|
|
} |
|
377
|
|
|
subject := next.GetSubject() |
|
378
|
|
|
|
|
379
|
|
|
// For each subject, generate a check function for its computed user set and append it to the list. |
|
380
|
|
|
checkFunctions = append(checkFunctions, engine.checkComputedUserSet(&base.PermissionCheckRequest{ |
|
381
|
|
|
TenantId: request.GetTenantId(), |
|
382
|
|
|
Entity: &base.Entity{ |
|
383
|
|
|
Type: subject.GetType(), |
|
384
|
|
|
Id: subject.GetId(), |
|
385
|
|
|
}, |
|
386
|
|
|
Permission: subject.GetRelation(), |
|
387
|
|
|
Subject: request.GetSubject(), |
|
388
|
|
|
Metadata: request.GetMetadata(), |
|
389
|
|
|
Context: request.GetContext(), |
|
390
|
|
|
Arguments: request.GetArguments(), |
|
391
|
|
|
}, ttu.GetComputed())) |
|
392
|
|
|
} |
|
393
|
|
|
|
|
394
|
|
|
// Return the union of all CheckFunctions |
|
395
|
|
|
// If any one of the check functions allows the action, the permission is granted. |
|
396
|
|
|
return checkUnion(ctx, checkFunctions, engine.concurrencyLimit) |
|
397
|
|
|
} |
|
398
|
|
|
} |
|
399
|
|
|
|
|
400
|
|
|
// metadata to determine if the computed user set should be excluded from the result. |
|
401
|
|
|
// checkComputedUserSet is a method of CheckEngine that checks permissions using the |
|
402
|
|
|
// ComputedUserSet data structure. It returns a CheckFunction closure that performs the check. |
|
403
|
|
|
func (engine *CheckEngine) checkComputedUserSet( |
|
404
|
|
|
request *base.PermissionCheckRequest, // The request containing details about the permission to be checked |
|
405
|
|
|
cu *base.ComputedUserSet, // The computed user set containing user set information |
|
406
|
|
|
) CheckFunction { |
|
407
|
|
|
// The returned CheckFunction invokes a permission check with a new request that is almost the same |
|
408
|
|
|
// as the incoming request, but changes the Permission to be the relation defined in the computed user set. |
|
409
|
|
|
// This is how the check "descends" into the computed user set to check permissions there. |
|
410
|
|
|
return engine.invoke(&base.PermissionCheckRequest{ |
|
411
|
|
|
TenantId: request.GetTenantId(), // Tenant ID from the incoming request |
|
412
|
|
|
Entity: request.GetEntity(), // Entity from the incoming request |
|
413
|
|
|
Permission: cu.GetRelation(), // Permission is set to the relation defined in the computed user set |
|
414
|
|
|
Subject: request.GetSubject(), // The subject from the incoming request |
|
415
|
|
|
Metadata: request.GetMetadata(), // Metadata from the incoming request |
|
416
|
|
|
Context: request.GetContext(), |
|
417
|
|
|
Arguments: request.GetArguments(), |
|
418
|
|
|
}) |
|
419
|
|
|
} |
|
420
|
|
|
|
|
421
|
|
|
// checkComputedAttribute constructs a CheckFunction that checks if a computed attribute |
|
422
|
|
|
// permission check request is allowed or denied. |
|
423
|
|
|
func (engine *CheckEngine) checkComputedAttribute( |
|
424
|
|
|
request *base.PermissionCheckRequest, |
|
425
|
|
|
ca *base.ComputedAttribute, |
|
426
|
|
|
) CheckFunction { |
|
427
|
|
|
// We're returning a function here - this is the CheckFunction. |
|
428
|
|
|
// Instead of performing the check directly here, we're using the 'invoke' method. |
|
429
|
|
|
// We pass a new PermissionCheckRequest to 'invoke', copying most of the fields |
|
430
|
|
|
// from the original request, but replacing the 'Permission' with the computed |
|
431
|
|
|
// attribute's name. |
|
432
|
|
|
return engine.invoke(&base.PermissionCheckRequest{ |
|
433
|
|
|
TenantId: request.GetTenantId(), |
|
434
|
|
|
Entity: request.GetEntity(), |
|
435
|
|
|
Permission: ca.GetName(), |
|
436
|
|
|
Subject: request.GetSubject(), |
|
437
|
|
|
Metadata: request.GetMetadata(), |
|
438
|
|
|
Context: request.GetContext(), |
|
439
|
|
|
Arguments: request.GetArguments(), |
|
440
|
|
|
}) |
|
441
|
|
|
} |
|
442
|
|
|
|
|
443
|
|
|
// checkDirectAttribute constructs a CheckFunction that checks if a direct attribute |
|
444
|
|
|
// permission check request is allowed or denied. |
|
445
|
|
|
func (engine *CheckEngine) checkDirectAttribute( |
|
446
|
|
|
request *base.PermissionCheckRequest, |
|
447
|
|
|
) CheckFunction { |
|
448
|
|
|
// We're returning a function here - this is the actual CheckFunction. |
|
449
|
|
|
return func(ctx context.Context) (*base.PermissionCheckResponse, error) { |
|
450
|
|
|
// Initial error declaration |
|
451
|
|
|
var err error |
|
452
|
|
|
|
|
453
|
|
|
// Create a new AttributeFilter with the entity type and ID from the request |
|
454
|
|
|
// and the requested permission. |
|
455
|
|
|
filter := &base.AttributeFilter{ |
|
456
|
|
|
Entity: &base.EntityFilter{ |
|
457
|
|
|
Type: request.GetEntity().GetType(), |
|
458
|
|
|
Ids: []string{request.GetEntity().GetId()}, |
|
459
|
|
|
}, |
|
460
|
|
|
Attributes: []string{request.GetPermission()}, |
|
461
|
|
|
} |
|
462
|
|
|
|
|
463
|
|
|
var val *base.Attribute |
|
464
|
|
|
|
|
465
|
|
|
// storageContext.NewContextualAttributes creates a new instance of ContextualAttributes based on the attributes |
|
466
|
|
|
// retrieved from the request context. |
|
467
|
|
|
val, err = storageContext.NewContextualAttributes(request.GetContext().GetAttributes()...).QuerySingleAttribute(filter) |
|
468
|
|
|
// An error occurred while querying the single attribute, so we return a denied response with empty metadata |
|
469
|
|
|
// and the error. |
|
470
|
|
|
if err != nil { |
|
471
|
|
|
return denied(emptyResponseMetadata()), err |
|
472
|
|
|
} |
|
473
|
|
|
|
|
474
|
|
|
if val == nil { |
|
475
|
|
|
// Use the data reader's QuerySingleAttribute method to find the relevant attribute |
|
476
|
|
|
val, err = engine.dataReader.QuerySingleAttribute(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken()) |
|
477
|
|
|
// If there was an error, return a denied response and the error. |
|
478
|
|
|
if err != nil { |
|
479
|
|
|
return denied(emptyResponseMetadata()), err |
|
480
|
|
|
} |
|
481
|
|
|
} |
|
482
|
|
|
|
|
483
|
|
|
// No attribute was found matching the provided filter. In this case, we return a denied response with empty metadata |
|
484
|
|
|
// and no error. |
|
485
|
|
|
if val == nil { |
|
486
|
|
|
return denied(emptyResponseMetadata()), nil |
|
487
|
|
|
} |
|
488
|
|
|
|
|
489
|
|
|
// Unmarshal the attribute value into a BoolValue message. |
|
490
|
|
|
var msg base.BooleanValue |
|
491
|
|
|
if err := val.GetValue().UnmarshalTo(&msg); err != nil { |
|
492
|
|
|
// If there was an error unmarshaling, return a denied response and the error. |
|
493
|
|
|
return denied(emptyResponseMetadata()), err |
|
494
|
|
|
} |
|
495
|
|
|
|
|
496
|
|
|
// If the attribute's value is true, return an allowed response. |
|
497
|
|
|
if msg.Data { |
|
498
|
|
|
return allowed(emptyResponseMetadata()), nil |
|
499
|
|
|
} |
|
500
|
|
|
|
|
501
|
|
|
// If the attribute's value is not true, return a denied response. |
|
502
|
|
|
return denied(emptyResponseMetadata()), nil |
|
503
|
|
|
} |
|
504
|
|
|
} |
|
505
|
|
|
|
|
506
|
|
|
// checkCall creates and returns a CheckFunction based on the provided request and call details. |
|
507
|
|
|
// It essentially constructs a new PermissionCheckRequest based on the call details and then invokes |
|
508
|
|
|
// the permission check using the engine's invoke method. |
|
509
|
|
|
func (engine *CheckEngine) checkCall( |
|
510
|
|
|
request *base.PermissionCheckRequest, |
|
511
|
|
|
call *base.Call, |
|
512
|
|
|
) CheckFunction { |
|
513
|
|
|
// Construct a new permission check request based on the input request and call details. |
|
514
|
|
|
return engine.invoke(&base.PermissionCheckRequest{ |
|
515
|
|
|
TenantId: request.GetTenantId(), |
|
516
|
|
|
Entity: request.GetEntity(), |
|
517
|
|
|
Permission: call.GetRuleName(), |
|
518
|
|
|
Subject: request.GetSubject(), |
|
519
|
|
|
Metadata: request.GetMetadata(), |
|
520
|
|
|
Context: request.GetContext(), |
|
521
|
|
|
Arguments: call.GetArguments(), |
|
522
|
|
|
}) |
|
523
|
|
|
} |
|
524
|
|
|
|
|
525
|
|
|
// checkDirectCall creates and returns a CheckFunction that performs direct permission checking. |
|
526
|
|
|
// The function evaluates permissions based on rule definitions, arguments, and attributes. |
|
527
|
|
|
func (engine *CheckEngine) checkDirectCall( |
|
528
|
|
|
request *base.PermissionCheckRequest, |
|
529
|
|
|
) CheckFunction { |
|
530
|
|
|
return func(ctx context.Context) (*base.PermissionCheckResponse, error) { |
|
531
|
|
|
var err error |
|
532
|
|
|
|
|
533
|
|
|
// If an error occurs during the check, this default "denied" response will be returned. |
|
534
|
|
|
emptyResp := denied(emptyResponseMetadata()) |
|
535
|
|
|
|
|
536
|
|
|
// Read the rule definition from the schema. If an error occurs, return the default denied response. |
|
537
|
|
|
var ru *base.RuleDefinition |
|
538
|
|
|
ru, _, err = engine.schemaReader.ReadRuleDefinition(ctx, request.GetTenantId(), request.GetPermission(), request.GetMetadata().GetSchemaVersion()) |
|
539
|
|
|
if err != nil { |
|
540
|
|
|
return emptyResp, err |
|
541
|
|
|
} |
|
542
|
|
|
|
|
543
|
|
|
// Initialize an arguments map to hold argument values. |
|
544
|
|
|
arguments := map[string]any{ |
|
545
|
|
|
"context": map[string]any{ |
|
546
|
|
|
"data": request.GetContext().GetData().AsMap(), |
|
547
|
|
|
}, |
|
548
|
|
|
} |
|
549
|
|
|
|
|
550
|
|
|
// List to store computed attributes. |
|
551
|
|
|
attributes := make([]string, 0) |
|
552
|
|
|
|
|
553
|
|
|
// Iterate over request arguments to classify and process them. |
|
554
|
|
|
for _, arg := range request.GetArguments() { |
|
555
|
|
|
switch actualArg := arg.Type.(type) { |
|
556
|
|
|
case *base.Argument_ComputedAttribute: |
|
557
|
|
|
// Handle computed attributes: Set them to a default empty value. |
|
558
|
|
|
attrName := actualArg.ComputedAttribute.GetName() |
|
559
|
|
|
emptyValue := getEmptyValueForType(ru.GetArguments()[attrName]) |
|
560
|
|
|
arguments[attrName] = emptyValue |
|
561
|
|
|
attributes = append(attributes, attrName) |
|
562
|
|
|
default: |
|
563
|
|
|
// Return an error for any unsupported argument types. |
|
564
|
|
|
return denied(emptyResponseMetadata()), errors.New(base.ErrorCode_ERROR_CODE_INTERNAL.String()) |
|
565
|
|
|
} |
|
566
|
|
|
} |
|
567
|
|
|
|
|
568
|
|
|
// If there are computed attributes, fetch them from the data source. |
|
569
|
|
|
if len(attributes) > 0 { |
|
570
|
|
|
filter := &base.AttributeFilter{ |
|
571
|
|
|
Entity: &base.EntityFilter{ |
|
572
|
|
|
Type: request.GetEntity().GetType(), |
|
573
|
|
|
Ids: []string{request.GetEntity().GetId()}, |
|
574
|
|
|
}, |
|
575
|
|
|
Attributes: attributes, |
|
576
|
|
|
} |
|
577
|
|
|
|
|
578
|
|
|
ait, err := engine.dataReader.QueryAttributes(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), database.NewCursorPagination()) |
|
579
|
|
|
if err != nil { |
|
580
|
|
|
return denied(emptyResponseMetadata()), err |
|
581
|
|
|
} |
|
582
|
|
|
|
|
583
|
|
|
cta, err := storageContext.NewContextualAttributes(request.GetContext().GetAttributes()...).QueryAttributes(filter, database.NewCursorPagination()) |
|
584
|
|
|
if err != nil { |
|
585
|
|
|
return denied(emptyResponseMetadata()), err |
|
586
|
|
|
} |
|
587
|
|
|
|
|
588
|
|
|
// Combine attributes from different sources ensuring uniqueness. |
|
589
|
|
|
it := database.NewUniqueAttributeIterator(ait, cta) |
|
590
|
|
|
for it.HasNext() { |
|
591
|
|
|
next, ok := it.GetNext() |
|
592
|
|
|
if !ok { |
|
593
|
|
|
break |
|
594
|
|
|
} |
|
595
|
|
|
arguments[next.GetAttribute()] = utils.ConvertProtoAnyToInterface(next.GetValue()) |
|
596
|
|
|
} |
|
597
|
|
|
} |
|
598
|
|
|
|
|
599
|
|
|
// Prepare the CEL environment with the argument values. |
|
600
|
|
|
env, err := utils.ArgumentsAsCelEnv(ru.Arguments) |
|
601
|
|
|
if err != nil { |
|
602
|
|
|
return nil, err |
|
603
|
|
|
} |
|
604
|
|
|
|
|
605
|
|
|
// Compile the rule expression into an executable form. |
|
606
|
|
|
exp := cel.CheckedExprToAst(ru.Expression) |
|
607
|
|
|
prg, err := env.Program(exp) |
|
608
|
|
|
if err != nil { |
|
609
|
|
|
return nil, err |
|
610
|
|
|
} |
|
611
|
|
|
|
|
612
|
|
|
// Evaluate the rule expression with the provided arguments. |
|
613
|
|
|
out, _, err := prg.Eval(arguments) |
|
614
|
|
|
if err != nil { |
|
615
|
|
|
return denied(emptyResponseMetadata()), fmt.Errorf("failed to evaluate expression: %w", err) |
|
|
|
|
|
|
616
|
|
|
} |
|
617
|
|
|
|
|
618
|
|
|
// Ensure the result of evaluation is boolean and decide on permission. |
|
619
|
|
|
result, ok := out.Value().(bool) |
|
620
|
|
|
if !ok { |
|
621
|
|
|
return denied(emptyResponseMetadata()), fmt.Errorf("expected boolean result, but got %T", out.Value()) |
|
622
|
|
|
} |
|
623
|
|
|
|
|
624
|
|
|
// If the result of the CEL evaluation is true, return an "allowed" response, otherwise return a "denied" response |
|
625
|
|
|
if result { |
|
626
|
|
|
return allowed(emptyResponseMetadata()), nil |
|
627
|
|
|
} |
|
628
|
|
|
|
|
629
|
|
|
return denied(emptyResponseMetadata()), nil |
|
630
|
|
|
} |
|
631
|
|
|
} |
|
632
|
|
|
|
|
633
|
|
|
// checkUnion checks if the subject has permission by running multiple CheckFunctions concurrently, |
|
634
|
|
|
// the permission check is successful if any one of the CheckFunctions succeeds (union). |
|
635
|
|
|
func checkUnion(ctx context.Context, functions []CheckFunction, limit int) (*base.PermissionCheckResponse, error) { |
|
636
|
|
|
// Initialize the response metadata |
|
637
|
|
|
responseMetadata := emptyResponseMetadata() |
|
638
|
|
|
|
|
639
|
|
|
// If there are no functions, deny the permission and return |
|
640
|
|
|
if len(functions) == 0 { |
|
641
|
|
|
return &base.PermissionCheckResponse{ |
|
642
|
|
|
Can: base.CheckResult_CHECK_RESULT_DENIED, |
|
643
|
|
|
Metadata: responseMetadata, |
|
644
|
|
|
}, nil |
|
645
|
|
|
} |
|
646
|
|
|
|
|
647
|
|
|
// Create a channel to receive the results of the CheckFunctions |
|
648
|
|
|
decisionChan := make(chan CheckResponse, len(functions)) |
|
649
|
|
|
// Create a context that can be cancelled |
|
650
|
|
|
cancelCtx, cancel := context.WithCancel(ctx) |
|
651
|
|
|
|
|
652
|
|
|
// Run the CheckFunctions concurrently |
|
653
|
|
|
clean := checkRun(cancelCtx, functions, decisionChan, limit) |
|
654
|
|
|
|
|
655
|
|
|
// When the function returns, ensure to cancel the context and clean up the resources |
|
656
|
|
|
defer func() { |
|
657
|
|
|
cancel() |
|
658
|
|
|
clean() |
|
659
|
|
|
close(decisionChan) |
|
660
|
|
|
}() |
|
661
|
|
|
|
|
662
|
|
|
// Iterate over the results of the CheckFunctions |
|
663
|
|
|
for range len(functions) { |
|
664
|
|
|
select { |
|
665
|
|
|
// If a result is received |
|
666
|
|
|
case d := <-decisionChan: |
|
667
|
|
|
// Merge the response metadata with the received metadata |
|
668
|
|
|
responseMetadata = joinResponseMetas(responseMetadata, d.resp.Metadata) |
|
669
|
|
|
// If there was an error, deny the permission and return the error |
|
670
|
|
|
if d.err != nil { |
|
671
|
|
|
return denied(responseMetadata), d.err |
|
672
|
|
|
} |
|
673
|
|
|
// If the CheckFunction allowed the permission, allow the permission and return |
|
674
|
|
|
if d.resp.GetCan() == base.CheckResult_CHECK_RESULT_ALLOWED { |
|
675
|
|
|
return allowed(responseMetadata), nil |
|
676
|
|
|
} |
|
677
|
|
|
// If the context is done, deny the permission and return a cancellation error |
|
678
|
|
|
case <-ctx.Done(): |
|
679
|
|
|
return denied(responseMetadata), errors.New(base.ErrorCode_ERROR_CODE_CANCELLED.String()) |
|
680
|
|
|
} |
|
681
|
|
|
} |
|
682
|
|
|
|
|
683
|
|
|
// If all CheckFunctions are done and none have allowed the permission, deny the permission and return |
|
684
|
|
|
return denied(responseMetadata), nil |
|
685
|
|
|
} |
|
686
|
|
|
|
|
687
|
|
|
// checkIntersection checks if the subject has permission by running multiple CheckFunctions concurrently, |
|
688
|
|
|
// the permission check is successful only when all CheckFunctions succeed (intersection). |
|
689
|
|
|
func checkIntersection(ctx context.Context, functions []CheckFunction, limit int) (*base.PermissionCheckResponse, error) { |
|
690
|
|
|
// Initialize the response metadata |
|
691
|
|
|
responseMetadata := emptyResponseMetadata() |
|
692
|
|
|
|
|
693
|
|
|
// If there are no functions, deny the permission and return |
|
694
|
|
|
if len(functions) == 0 { |
|
695
|
|
|
return denied(responseMetadata), nil |
|
696
|
|
|
} |
|
697
|
|
|
|
|
698
|
|
|
// Create a channel to receive the results of the CheckFunctions |
|
699
|
|
|
decisionChan := make(chan CheckResponse, len(functions)) |
|
700
|
|
|
// Create a context that can be cancelled |
|
701
|
|
|
cancelCtx, cancel := context.WithCancel(ctx) |
|
702
|
|
|
|
|
703
|
|
|
// Run the CheckFunctions concurrently |
|
704
|
|
|
clean := checkRun(cancelCtx, functions, decisionChan, limit) |
|
705
|
|
|
|
|
706
|
|
|
// When the function returns, ensure to cancel the context and clean up the resources |
|
707
|
|
|
defer func() { |
|
708
|
|
|
cancel() |
|
709
|
|
|
clean() |
|
710
|
|
|
close(decisionChan) |
|
711
|
|
|
}() |
|
712
|
|
|
|
|
713
|
|
|
// Iterate over the results of the CheckFunctions |
|
714
|
|
|
for range len(functions) { |
|
715
|
|
|
select { |
|
716
|
|
|
// If a result is received |
|
717
|
|
|
case d := <-decisionChan: |
|
718
|
|
|
// Merge the response metadata with the received metadata |
|
719
|
|
|
responseMetadata = joinResponseMetas(responseMetadata, d.resp.Metadata) |
|
720
|
|
|
// If there was an error, deny the permission and return the error |
|
721
|
|
|
if d.err != nil { |
|
722
|
|
|
return denied(responseMetadata), d.err |
|
723
|
|
|
} |
|
724
|
|
|
// If the CheckFunction denied the permission, deny the permission and return |
|
725
|
|
|
if d.resp.GetCan() == base.CheckResult_CHECK_RESULT_DENIED { |
|
726
|
|
|
return denied(responseMetadata), nil |
|
727
|
|
|
} |
|
728
|
|
|
// If the context is done, deny the permission and return a cancellation error |
|
729
|
|
|
case <-ctx.Done(): |
|
730
|
|
|
return denied(responseMetadata), errors.New(base.ErrorCode_ERROR_CODE_CANCELLED.String()) |
|
731
|
|
|
} |
|
732
|
|
|
} |
|
733
|
|
|
|
|
734
|
|
|
// If all CheckFunctions allowed the permission, allow the permission and return |
|
735
|
|
|
return allowed(responseMetadata), nil |
|
736
|
|
|
} |
|
737
|
|
|
|
|
738
|
|
|
// checkExclusion is a function that checks if there are any exclusions for given CheckFunctions |
|
739
|
|
|
func checkExclusion(ctx context.Context, functions []CheckFunction, limit int) (*base.PermissionCheckResponse, error) { |
|
740
|
|
|
// Initialize the response metadata |
|
741
|
|
|
responseMetadata := emptyResponseMetadata() |
|
742
|
|
|
|
|
743
|
|
|
// Check if there are at least 2 functions, otherwise return an error indicating that exclusion requires more than one function |
|
744
|
|
|
if len(functions) <= 1 { |
|
745
|
|
|
return denied(responseMetadata), errors.New(base.ErrorCode_ERROR_CODE_EXCLUSION_REQUIRES_MORE_THAN_ONE_FUNCTION.String()) |
|
746
|
|
|
} |
|
747
|
|
|
|
|
748
|
|
|
// Initialize channels to handle the result of the first function and the remaining functions separately |
|
749
|
|
|
leftDecisionChan := make(chan CheckResponse, 1) |
|
750
|
|
|
decisionChan := make(chan CheckResponse, len(functions)-1) |
|
751
|
|
|
|
|
752
|
|
|
// Create a new context that can be cancelled |
|
753
|
|
|
cancelCtx, cancel := context.WithCancel(ctx) |
|
754
|
|
|
|
|
755
|
|
|
// Start the first function in a separate goroutine |
|
756
|
|
|
var wg sync.WaitGroup |
|
757
|
|
|
wg.Add(1) |
|
758
|
|
|
go func() { |
|
759
|
|
|
result, err := functions[0](cancelCtx) |
|
760
|
|
|
leftDecisionChan <- CheckResponse{ |
|
761
|
|
|
resp: result, |
|
762
|
|
|
err: err, |
|
763
|
|
|
} |
|
764
|
|
|
wg.Done() |
|
765
|
|
|
}() |
|
766
|
|
|
|
|
767
|
|
|
// Run the remaining functions concurrently with a limit |
|
768
|
|
|
clean := checkRun(cancelCtx, functions[1:], decisionChan, limit-1) |
|
769
|
|
|
|
|
770
|
|
|
// Ensure that all resources are properly cleaned up when the function exits |
|
771
|
|
|
defer func() { |
|
772
|
|
|
cancel() |
|
773
|
|
|
clean() |
|
774
|
|
|
close(decisionChan) |
|
775
|
|
|
wg.Wait() |
|
776
|
|
|
close(leftDecisionChan) |
|
777
|
|
|
}() |
|
778
|
|
|
|
|
779
|
|
|
// Process the result from the first function |
|
780
|
|
|
select { |
|
781
|
|
|
case left := <-leftDecisionChan: |
|
782
|
|
|
responseMetadata = joinResponseMetas(responseMetadata, left.resp.Metadata) |
|
783
|
|
|
|
|
784
|
|
|
if left.err != nil { |
|
785
|
|
|
return denied(responseMetadata), left.err |
|
786
|
|
|
} |
|
787
|
|
|
|
|
788
|
|
|
if left.resp.GetCan() == base.CheckResult_CHECK_RESULT_DENIED { |
|
789
|
|
|
return denied(responseMetadata), nil |
|
790
|
|
|
} |
|
791
|
|
|
|
|
792
|
|
|
case <-ctx.Done(): |
|
793
|
|
|
return denied(responseMetadata), errors.New(base.ErrorCode_ERROR_CODE_CANCELLED.String()) |
|
794
|
|
|
} |
|
795
|
|
|
|
|
796
|
|
|
// Process the results from the remaining functions |
|
797
|
|
|
for range len(functions) - 1 { |
|
798
|
|
|
select { |
|
799
|
|
|
case d := <-decisionChan: |
|
800
|
|
|
responseMetadata = joinResponseMetas(responseMetadata, d.resp.Metadata) |
|
801
|
|
|
|
|
802
|
|
|
if d.err != nil { |
|
803
|
|
|
return denied(responseMetadata), d.err |
|
804
|
|
|
} |
|
805
|
|
|
|
|
806
|
|
|
if d.resp.GetCan() == base.CheckResult_CHECK_RESULT_ALLOWED { |
|
807
|
|
|
return denied(responseMetadata), nil |
|
808
|
|
|
} |
|
809
|
|
|
|
|
810
|
|
|
case <-ctx.Done(): |
|
811
|
|
|
return denied(responseMetadata), errors.New(base.ErrorCode_ERROR_CODE_CANCELLED.String()) |
|
812
|
|
|
} |
|
813
|
|
|
} |
|
814
|
|
|
|
|
815
|
|
|
// If none of the functions allowed the action, then it's allowed by exclusion |
|
816
|
|
|
return allowed(responseMetadata), nil |
|
817
|
|
|
} |
|
818
|
|
|
|
|
819
|
|
|
// checkRun is a function that executes a list of CheckFunctions concurrently with a specified limit. |
|
820
|
|
|
func checkRun(ctx context.Context, functions []CheckFunction, decisionChan chan<- CheckResponse, limit int) func() { |
|
821
|
|
|
// Create a channel that enforces the concurrency limit |
|
822
|
|
|
cl := make(chan struct{}, limit) |
|
823
|
|
|
var wg sync.WaitGroup |
|
824
|
|
|
|
|
825
|
|
|
// Define a helper function that calls a CheckFunction and sends the result to the decisionChan |
|
826
|
|
|
check := func(child CheckFunction) { |
|
827
|
|
|
result, err := child(ctx) |
|
828
|
|
|
decisionChan <- CheckResponse{ |
|
829
|
|
|
resp: result, |
|
830
|
|
|
err: err, |
|
831
|
|
|
} |
|
832
|
|
|
// Once the CheckFunction is done, release the concurrency limit |
|
833
|
|
|
<-cl |
|
834
|
|
|
wg.Done() |
|
835
|
|
|
} |
|
836
|
|
|
|
|
837
|
|
|
// Start a goroutine that iterates over the functions |
|
838
|
|
|
wg.Add(1) |
|
839
|
|
|
go func() { |
|
840
|
|
|
run: |
|
841
|
|
|
// Iterate over the functions |
|
842
|
|
|
for _, fun := range functions { |
|
843
|
|
|
child := fun |
|
844
|
|
|
select { |
|
845
|
|
|
// If the concurrency limit allows it, start the function in a new goroutine |
|
846
|
|
|
case cl <- struct{}{}: |
|
847
|
|
|
wg.Add(1) |
|
848
|
|
|
go check(child) |
|
849
|
|
|
// If the context is done, break the loop |
|
850
|
|
|
case <-ctx.Done(): |
|
851
|
|
|
break run |
|
852
|
|
|
} |
|
853
|
|
|
} |
|
854
|
|
|
wg.Done() |
|
855
|
|
|
}() |
|
856
|
|
|
|
|
857
|
|
|
// Return a cleanup function that waits for all goroutines to finish and then closes the concurrency limit channel |
|
858
|
|
|
return func() { |
|
859
|
|
|
wg.Wait() |
|
860
|
|
|
close(cl) |
|
861
|
|
|
} |
|
862
|
|
|
} |
|
863
|
|
|
|
|
864
|
|
|
// checkFail is a helper function that returns a CheckFunction that always returns a denied PermissionCheckResponse |
|
865
|
|
|
// with the provided error and an empty PermissionCheckResponseMetadata. |
|
866
|
|
|
// |
|
867
|
|
|
// The function works as follows: |
|
868
|
|
|
// 1. The function takes an error as input parameter. |
|
869
|
|
|
// 2. The function returns a CheckFunction that takes a context as input parameter and always returns a denied |
|
870
|
|
|
// PermissionCheckResponse with the provided error and an empty PermissionCheckResponseMetadata. |
|
871
|
|
|
func checkFail(err error) CheckFunction { |
|
872
|
|
|
return func(ctx context.Context) (*base.PermissionCheckResponse, error) { |
|
873
|
|
|
return denied(&base.PermissionCheckResponseMetadata{}), err |
|
874
|
|
|
} |
|
875
|
|
|
} |
|
876
|
|
|
|
|
877
|
|
|
// denied is a helper function that returns a denied PermissionCheckResponse with the provided PermissionCheckResponseMetadata. |
|
878
|
|
|
// |
|
879
|
|
|
// The function works as follows: |
|
880
|
|
|
// 1. The function takes a PermissionCheckResponseMetadata as input parameter. |
|
881
|
|
|
// 2. The function returns a denied PermissionCheckResponse with a RESULT_DENIED Can value and the provided metadata. |
|
882
|
|
|
func denied(meta *base.PermissionCheckResponseMetadata) *base.PermissionCheckResponse { |
|
883
|
|
|
return &base.PermissionCheckResponse{ |
|
884
|
|
|
Can: base.CheckResult_CHECK_RESULT_DENIED, |
|
885
|
|
|
Metadata: meta, |
|
886
|
|
|
} |
|
887
|
|
|
} |
|
888
|
|
|
|
|
889
|
|
|
// allowed is a helper function that returns an allowed PermissionCheckResponse with the provided PermissionCheckResponseMetadata. |
|
890
|
|
|
// |
|
891
|
|
|
// The function works as follows: |
|
892
|
|
|
// 1. The function takes a PermissionCheckResponseMetadata as input parameter. |
|
893
|
|
|
// 2. The function returns an allowed PermissionCheckResponse with a RESULT_ALLOWED Can value and the provided metadata. |
|
894
|
|
|
func allowed(meta *base.PermissionCheckResponseMetadata) *base.PermissionCheckResponse { |
|
895
|
|
|
return &base.PermissionCheckResponse{ |
|
896
|
|
|
Can: base.CheckResult_CHECK_RESULT_ALLOWED, |
|
897
|
|
|
Metadata: meta, |
|
898
|
|
|
} |
|
899
|
|
|
} |
|
900
|
|
|
|
|
901
|
|
|
// emptyResponseMetadata creates and returns an empty PermissionCheckResponseMetadata. |
|
902
|
|
|
// |
|
903
|
|
|
// Returns: |
|
904
|
|
|
// - A pointer to PermissionCheckResponseMetadata with the CheckCount initialized to 0. |
|
905
|
|
|
func emptyResponseMetadata() *base.PermissionCheckResponseMetadata { |
|
906
|
|
|
return &base.PermissionCheckResponseMetadata{ |
|
907
|
|
|
CheckCount: 0, |
|
908
|
|
|
} |
|
909
|
|
|
} |
|
910
|
|
|
|