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
introduced
by
![]() |
|||
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 |