Issues (35)

internal/engines/check.go (1 issue)

Severity
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
	var functions []CheckFunction
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
		var checkFunctions []CheckFunction
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
		var checkFunctions []CheckFunction
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]interface{}{
545
			"context": map[string]interface{}{
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)
0 ignored issues
show
unrecognized printf verb 'w'
Loading history...
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()), err
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 i := 0; i < len(functions); i++ {
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 i := 0; i < len(functions); i++ {
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 i := 0; i < len(functions)-1; i++ {
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