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
|
|
|
|