Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(depth-check): correctly handle 'depth not enough' error condition #1707

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/api-reference/apidocs.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "Permify API",
"description": "Permify is an open source authorization service for creating fine-grained and scalable authorization systems.",
"version": "v1.1.4",
"version": "v1.1.5",
"contact": {
"name": "API Support",
"url": "https://github.com/Permify/permify/issues",
Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference/openapiv2/apidocs.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"info": {
"title": "Permify API",
"description": "Permify is an open source authorization service for creating fine-grained and scalable authorization systems.",
"version": "v1.1.4",
"version": "v1.1.5",
"contact": {
"name": "API Support",
"url": "https://github.com/Permify/permify/issues",
Expand Down
File renamed without changes.
67 changes: 38 additions & 29 deletions internal/engines/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ func (engine *CheckEngine) SetInvoker(invoker invoke.Check) {
// This function performs various checks and returns the permission check response
// along with any errors that may have occurred.
func (engine *CheckEngine) Check(ctx context.Context, request *base.PermissionCheckRequest) (response *base.PermissionCheckResponse, err error) {
emptyResp := denied(&base.PermissionCheckResponseMetadata{
CheckCount: 0,
})
emptyResp := denied(emptyResponseMetadata())

// Retrieve entity definition
var en *base.EntityDefinition
Expand Down Expand Up @@ -115,7 +113,7 @@ func (engine *CheckEngine) check(
// If the request's entity and permission are the same as the subject, return a CheckFunction that always allows the permission.
if tuple.AreQueryAndSubjectEqual(request.GetEntity(), request.GetPermission(), request.GetSubject()) {
return func(ctx context.Context) (*base.PermissionCheckResponse, error) {
return allowed(&base.PermissionCheckResponseMetadata{}), nil
return allowed(emptyResponseMetadata()), nil
}
}

Expand Down Expand Up @@ -271,7 +269,7 @@ func (engine *CheckEngine) checkDirectRelation(request *base.PermissionCheckRequ
cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, database.NewCursorPagination())
if err != nil {
// If an error occurred while querying, return a "denied" response and the error.
return denied(&base.PermissionCheckResponseMetadata{}), err
return denied(emptyResponseMetadata()), err
}

// Query the relationships for the entity in the request.
Expand All @@ -281,7 +279,7 @@ func (engine *CheckEngine) checkDirectRelation(request *base.PermissionCheckRequ

// If there's an error in querying, return a denied permission response along with the error.
if err != nil {
return denied(&base.PermissionCheckResponseMetadata{}), err
return denied(emptyResponseMetadata()), err
}

// Create a new UniqueTupleIterator from the two TupleIterators.
Expand All @@ -301,7 +299,7 @@ func (engine *CheckEngine) checkDirectRelation(request *base.PermissionCheckRequ

// If the subject of the tuple is the same as the subject in the request, permission is allowed.
if tuple.AreSubjectsEqual(subject, request.GetSubject()) {
return allowed(&base.PermissionCheckResponseMetadata{}), nil
return allowed(emptyResponseMetadata()), nil
}
// If the subject is not a user and the relation is not ELLIPSIS, append a check function to the list.
if !tuple.IsDirectSubject(subject) && subject.GetRelation() != tuple.ELLIPSIS {
Expand All @@ -325,7 +323,7 @@ func (engine *CheckEngine) checkDirectRelation(request *base.PermissionCheckRequ
}

// If there's no CheckFunction, return a denied permission response.
return denied(&base.PermissionCheckResponseMetadata{}), nil
return denied(emptyResponseMetadata()), nil
}
}

Expand Down Expand Up @@ -353,15 +351,15 @@ func (engine *CheckEngine) checkTupleToUserSet(
cti, err := storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, database.NewCursorPagination())
if err != nil {
// If an error occurred while querying, return a "denied" response and the error.
return denied(&base.PermissionCheckResponseMetadata{}), err
return denied(emptyResponseMetadata()), err
}

// Use the filter to query for relationships in the database.
// relationshipReader.QueryRelationships() uses the filter to find and return matching relationships.
rit, err := engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), database.NewCursorPagination())
if err != nil {
// If an error occurred while querying, return a "denied" response and the error.
return denied(&base.PermissionCheckResponseMetadata{}), err
return denied(emptyResponseMetadata()), err
}

// Create a new UniqueTupleIterator from the two TupleIterators.
Expand Down Expand Up @@ -390,6 +388,7 @@ func (engine *CheckEngine) checkTupleToUserSet(
Subject: request.GetSubject(),
Metadata: request.GetMetadata(),
Context: request.GetContext(),
Arguments: request.GetArguments(),
}, ttu.GetComputed()))
}

Expand All @@ -416,6 +415,7 @@ func (engine *CheckEngine) checkComputedUserSet(
Subject: request.GetSubject(), // The subject from the incoming request
Metadata: request.GetMetadata(), // Metadata from the incoming request
Context: request.GetContext(),
Arguments: request.GetArguments(),
})
}

Expand All @@ -437,6 +437,7 @@ func (engine *CheckEngine) checkComputedAttribute(
Subject: request.GetSubject(),
Metadata: request.GetMetadata(),
Context: request.GetContext(),
Arguments: request.GetArguments(),
})
}

Expand Down Expand Up @@ -469,38 +470,38 @@ func (engine *CheckEngine) checkDirectAttribute(
// An error occurred while querying the single attribute, so we return a denied response with empty metadata
// and the error.
if err != nil {
return denied(&base.PermissionCheckResponseMetadata{}), err
return denied(emptyResponseMetadata()), err
}

if val == nil {
// Use the data reader's QuerySingleAttribute method to find the relevant attribute
val, err = engine.dataReader.QuerySingleAttribute(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken())
// If there was an error, return a denied response and the error.
if err != nil {
return denied(&base.PermissionCheckResponseMetadata{}), err
return denied(emptyResponseMetadata()), err
}
}

// No attribute was found matching the provided filter. In this case, we return a denied response with empty metadata
// and no error.
if val == nil {
return denied(&base.PermissionCheckResponseMetadata{}), nil
return denied(emptyResponseMetadata()), nil
}

// Unmarshal the attribute value into a BoolValue message.
var msg base.BooleanValue
if err := val.GetValue().UnmarshalTo(&msg); err != nil {
// If there was an error unmarshaling, return a denied response and the error.
return denied(&base.PermissionCheckResponseMetadata{}), err
return denied(emptyResponseMetadata()), err
}

// If the attribute's value is true, return an allowed response.
if msg.Data {
return allowed(&base.PermissionCheckResponseMetadata{}), nil
return allowed(emptyResponseMetadata()), nil
}

// If the attribute's value is not true, return a denied response.
return denied(&base.PermissionCheckResponseMetadata{}), nil
return denied(emptyResponseMetadata()), nil
}
}

Expand Down Expand Up @@ -532,9 +533,7 @@ func (engine *CheckEngine) checkDirectCall(
var err error

// If an error occurs during the check, this default "denied" response will be returned.
emptyResp := denied(&base.PermissionCheckResponseMetadata{
CheckCount: 0,
})
emptyResp := denied(emptyResponseMetadata())

// Read the rule definition from the schema. If an error occurs, return the default denied response.
var ru *base.RuleDefinition
Expand Down Expand Up @@ -564,7 +563,7 @@ func (engine *CheckEngine) checkDirectCall(
attributes = append(attributes, attrName)
default:
// Return an error for any unsupported argument types.
return denied(&base.PermissionCheckResponseMetadata{}), fmt.Errorf(base.ErrorCode_ERROR_CODE_INTERNAL.String())
return denied(emptyResponseMetadata()), fmt.Errorf(base.ErrorCode_ERROR_CODE_INTERNAL.String())
}
}

Expand All @@ -580,12 +579,12 @@ func (engine *CheckEngine) checkDirectCall(

ait, err := engine.dataReader.QueryAttributes(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), database.NewCursorPagination())
if err != nil {
return denied(&base.PermissionCheckResponseMetadata{}), err
return denied(emptyResponseMetadata()), err
}

cta, err := storageContext.NewContextualAttributes(request.GetContext().GetAttributes()...).QueryAttributes(filter, database.NewCursorPagination())
if err != nil {
return denied(&base.PermissionCheckResponseMetadata{}), err
return denied(emptyResponseMetadata()), err
}

// Combine attributes from different sources ensuring uniqueness.
Expand Down Expand Up @@ -615,29 +614,29 @@ func (engine *CheckEngine) checkDirectCall(
// Evaluate the rule expression with the provided arguments.
out, _, err := prg.Eval(arguments)
if err != nil {
return denied(&base.PermissionCheckResponseMetadata{}), fmt.Errorf("failed to evaluate expression: %w", err)
return denied(emptyResponseMetadata()), fmt.Errorf("failed to evaluate expression: %w", err)
}

// Ensure the result of evaluation is boolean and decide on permission.
result, ok := out.Value().(bool)
if !ok {
return denied(&base.PermissionCheckResponseMetadata{}), fmt.Errorf("expected boolean result, but got %T", out.Value())
return denied(emptyResponseMetadata()), fmt.Errorf("expected boolean result, but got %T", out.Value())
}

// If the result of the CEL evaluation is true, return an "allowed" response, otherwise return a "denied" response
if result {
return allowed(&base.PermissionCheckResponseMetadata{}), nil
return allowed(emptyResponseMetadata()), nil
}

return denied(&base.PermissionCheckResponseMetadata{}), err
return denied(emptyResponseMetadata()), err
}
}

// checkUnion checks if the subject has permission by running multiple CheckFunctions concurrently,
// the permission check is successful if any one of the CheckFunctions succeeds (union).
func checkUnion(ctx context.Context, functions []CheckFunction, limit int) (*base.PermissionCheckResponse, error) {
// Initialize the response metadata
responseMetadata := &base.PermissionCheckResponseMetadata{}
responseMetadata := emptyResponseMetadata()

// If there are no functions, deny the permission and return
if len(functions) == 0 {
Expand Down Expand Up @@ -691,7 +690,7 @@ func checkUnion(ctx context.Context, functions []CheckFunction, limit int) (*bas
// the permission check is successful only when all CheckFunctions succeed (intersection).
func checkIntersection(ctx context.Context, functions []CheckFunction, limit int) (*base.PermissionCheckResponse, error) {
// Initialize the response metadata
responseMetadata := &base.PermissionCheckResponseMetadata{}
responseMetadata := emptyResponseMetadata()

// If there are no functions, deny the permission and return
if len(functions) == 0 {
Expand Down Expand Up @@ -741,7 +740,7 @@ func checkIntersection(ctx context.Context, functions []CheckFunction, limit int
// checkExclusion is a function that checks if there are any exclusions for given CheckFunctions
func checkExclusion(ctx context.Context, functions []CheckFunction, limit int) (*base.PermissionCheckResponse, error) {
// Initialize the response metadata
responseMetadata := &base.PermissionCheckResponseMetadata{}
responseMetadata := emptyResponseMetadata()

// Check if there are at least 2 functions, otherwise return an error indicating that exclusion requires more than one function
if len(functions) <= 1 {
Expand Down Expand Up @@ -900,3 +899,13 @@ func allowed(meta *base.PermissionCheckResponseMetadata) *base.PermissionCheckRe
Metadata: meta,
}
}

// emptyResponseMetadata creates and returns an empty PermissionCheckResponseMetadata.
//
// Returns:
// - A pointer to PermissionCheckResponseMetadata with the CheckCount initialized to 0.
func emptyResponseMetadata() *base.PermissionCheckResponseMetadata {
return &base.PermissionCheckResponseMetadata{
CheckCount: 0,
}
}
2 changes: 1 addition & 1 deletion internal/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ var Identifier = ""
*/
const (
// Version is the last release of the Permify (e.g. v0.1.0)
Version = "v1.1.4"
Version = "v1.1.5"
)

// Function to create a single line of the ASCII art with centered content and color
Expand Down
8 changes: 4 additions & 4 deletions internal/invoke/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package invoke

import (
"context"
"sync/atomic"
"time"

"go.opentelemetry.io/otel"
Expand Down Expand Up @@ -175,8 +176,7 @@ func (invoker *DirectInvoker) Check(ctx context.Context, request *base.Permissio
}
}

// Decrease the depth of the request metadata.
request.Metadata = decreaseDepth(request.GetMetadata())
atomic.AddInt32(&request.GetMetadata().Depth, -1)

// Perform the actual permission check using the provided request.
response, err = invoker.cc.Check(ctx, request)
Expand All @@ -194,8 +194,8 @@ func (invoker *DirectInvoker) Check(ctx context.Context, request *base.Permissio
duration := time.Since(start)
invoker.checkDurationHistogram.Record(ctx, duration.Microseconds())

// Increase the check count in the response metadata.
response.Metadata = increaseCheckCount(response.Metadata)
// increaseCheckCount increments the CheckCount value in the response metadata by 1.
atomic.AddInt32(&response.GetMetadata().CheckCount, +1)

// Increase the check count in the metrics.
invoker.checkCounter.Add(ctx, 1)
Expand Down
19 changes: 2 additions & 17 deletions internal/invoke/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,15 @@ package invoke

import (
"errors"
"sync/atomic"

base "github.com/Permify/permify/pkg/pb/base/v1"
)

// checkDepth - a helper function that returns an error if the depth in a PermissionCheckRequest is zero.
func checkDepth(request *base.PermissionCheckRequest) error {
if request.GetMetadata().Depth == 0 {
if atomic.LoadInt32(&request.GetMetadata().Depth) == 0 {
return errors.New(base.ErrorCode_ERROR_CODE_DEPTH_NOT_ENOUGH.String())
}
return nil
}

// increaseCheckCount - a helper function that increments the check count in a PermissionCheckResponseMetadata struct.
func increaseCheckCount(metadata *base.PermissionCheckResponseMetadata) *base.PermissionCheckResponseMetadata {
return &base.PermissionCheckResponseMetadata{
CheckCount: metadata.CheckCount + 1,
}
}

// increaseCheckCount - a helper function that increments the check count in a PermissionCheckResponseMetadata struct.
func decreaseDepth(metadata *base.PermissionCheckRequestMetadata) *base.PermissionCheckRequestMetadata {
return &base.PermissionCheckRequestMetadata{
SchemaVersion: metadata.GetSchemaVersion(),
SnapToken: metadata.GetSnapToken(),
Depth: metadata.Depth - 1,
}
}
2 changes: 1 addition & 1 deletion pkg/pb/base/v1/openapi.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion proto/base/v1/openapi.proto
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {
title: "Permify API";
description: "Permify is an open source authorization service for creating fine-grained and scalable authorization systems.";
version: "v1.1.4";
version: "v1.1.5";
contact: {
name: "API Support";
url: "https://github.com/Permify/permify/issues";
Expand Down