Skip to content

Commit

Permalink
feat: add schema reader for define cache keys type
Browse files Browse the repository at this point in the history
  • Loading branch information
tolgaOzen committed Sep 16, 2023
1 parent c94646b commit 97ff507
Show file tree
Hide file tree
Showing 12 changed files with 197 additions and 132 deletions.
31 changes: 29 additions & 2 deletions internal/engines/consistent/hashring.go
Expand Up @@ -9,6 +9,8 @@ import (

"github.com/Permify/permify/internal/engines/keys"
"github.com/Permify/permify/internal/invoke"
"github.com/Permify/permify/internal/schema"
"github.com/Permify/permify/internal/storage"
hash "github.com/Permify/permify/pkg/consistent"
"github.com/Permify/permify/pkg/gossip"
"github.com/Permify/permify/pkg/logger"
Expand All @@ -17,6 +19,8 @@ import (

// Hashring is a wrapper around the consistent hash implementation that
type Hashring struct {
// schemaReader is responsible for reading schema information
schemaReader storage.SchemaReader
checker invoke.Check
gossip gossip.IGossip
consistent hash.Consistent
Expand All @@ -27,14 +31,15 @@ type Hashring struct {

// NewCheckEngineWithHashring creates a new instance of EngineKeyManager by initializing an EngineKeys
// struct with the provided cache.Cache instance.
func NewCheckEngineWithHashring(checker invoke.Check, consistent *hash.ConsistentHash, g gossip.IGossip, port string, l *logger.Logger) (invoke.Check, error) {
func NewCheckEngineWithHashring(checker invoke.Check, schemaReader storage.SchemaReader, consistent *hash.ConsistentHash, g gossip.IGossip, port string, l *logger.Logger) (invoke.Check, error) {
// Return a new instance of EngineKeys with the provided cache
ip, err := gossip.ExternalIP()
if err != nil {
return nil, err
}

return &Hashring{
schemaReader: schemaReader,
checker: checker,
localNodeAddress: ip + ":" + port,
gossip: g,
Expand All @@ -44,8 +49,30 @@ func NewCheckEngineWithHashring(checker invoke.Check, consistent *hash.Consisten
}

func (c *Hashring) Check(ctx context.Context, request *base.PermissionCheckRequest) (response *base.PermissionCheckResponse, err error) {
// Retrieve entity definition
var en *base.EntityDefinition
en, _, err = c.schemaReader.ReadEntityDefinition(ctx, request.GetTenantId(), request.GetEntity().GetType(), request.GetMetadata().GetSchemaVersion())
if err != nil {
return &base.PermissionCheckResponse{
Can: base.CheckResult_CHECK_RESULT_DENIED,
Metadata: &base.PermissionCheckResponseMetadata{
CheckCount: 0,
},
}, err
}

isRelational := false

// Determine the type of the reference by name in the given entity definition.
tor, err := schema.GetTypeOfReferenceByNameInEntityDefinition(en, request.GetPermission())
if err == nil {
if tor != base.EntityDefinition_REFERENCE_ATTRIBUTE {
isRelational = true
}
}

// Generate a unique checkKey string based on the provided PermissionCheckRequest
k := keys.GenerateKey(request)
k := keys.GenerateKey(request, isRelational)

_, _, ok := c.consistent.Get(k)
if !ok {
Expand Down
83 changes: 57 additions & 26 deletions internal/engines/keys/keys.go
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/cespare/xxhash/v2"

"github.com/Permify/permify/internal/invoke"
"github.com/Permify/permify/internal/schema"
"github.com/Permify/permify/internal/storage"
"github.com/Permify/permify/pkg/attribute"
"github.com/Permify/permify/pkg/cache"
"github.com/Permify/permify/pkg/logger"
Expand All @@ -18,25 +20,50 @@ import (

// CheckEngineWithKeys is a struct that holds an instance of a cache.Cache for managing engine keys.
type CheckEngineWithKeys struct {
checker invoke.Check
cache cache.Cache
l *logger.Logger
// schemaReader is responsible for reading schema information
schemaReader storage.SchemaReader
checker invoke.Check
cache cache.Cache
l *logger.Logger
}

// NewCheckEngineWithKeys creates a new instance of EngineKeyManager by initializing an EngineKeys
// struct with the provided cache.Cache instance.
func NewCheckEngineWithKeys(checker invoke.Check, cache cache.Cache, l *logger.Logger) invoke.Check {
func NewCheckEngineWithKeys(checker invoke.Check, schemaReader storage.SchemaReader, cache cache.Cache, l *logger.Logger) invoke.Check {
return &CheckEngineWithKeys{
checker: checker,
cache: cache,
l: l,
schemaReader: schemaReader,
checker: checker,
cache: cache,
l: l,
}
}

// Check performs a permission check for a given request, using the cached results if available.
func (c *CheckEngineWithKeys) Check(ctx context.Context, request *base.PermissionCheckRequest) (response *base.PermissionCheckResponse, err error) {
// Retrieve entity definition
var en *base.EntityDefinition
en, _, err = c.schemaReader.ReadEntityDefinition(ctx, request.GetTenantId(), request.GetEntity().GetType(), request.GetMetadata().GetSchemaVersion())
if err != nil {
return &base.PermissionCheckResponse{
Can: base.CheckResult_CHECK_RESULT_DENIED,
Metadata: &base.PermissionCheckResponseMetadata{
CheckCount: 0,
},
}, err
}

isRelational := false

// Determine the type of the reference by name in the given entity definition.
tor, err := schema.GetTypeOfReferenceByNameInEntityDefinition(en, request.GetPermission())
if err == nil {
if tor != base.EntityDefinition_REFERENCE_ATTRIBUTE {
isRelational = true
}
}

// Try to get the cached result for the given request.
res, found := c.getCheckKey(request)
res, found := c.getCheckKey(request, isRelational)

// If a cached result is found, handle exclusion and return the result.
if found {
Expand All @@ -63,7 +90,7 @@ func (c *CheckEngineWithKeys) Check(ctx context.Context, request *base.Permissio
c.setCheckKey(request, &base.PermissionCheckResponse{
Can: res.GetCan(),
Metadata: &base.PermissionCheckResponseMetadata{},
})
}, isRelational)

// Return the result of the permission check.
return res, err
Expand All @@ -72,7 +99,7 @@ func (c *CheckEngineWithKeys) Check(ctx context.Context, request *base.Permissio
// GetCheckKey retrieves the value for the given key from the EngineKeys cache.
// It returns the PermissionCheckResponse if the key is found, and a boolean value
// indicating whether the key was found or not.
func (c *CheckEngineWithKeys) getCheckKey(key *base.PermissionCheckRequest) (*base.PermissionCheckResponse, bool) {
func (c *CheckEngineWithKeys) getCheckKey(key *base.PermissionCheckRequest, isRelational bool) (*base.PermissionCheckResponse, bool) {
if key == nil {
// If either the key or value is nil, return false
return nil, false
Expand All @@ -82,7 +109,7 @@ func (c *CheckEngineWithKeys) getCheckKey(key *base.PermissionCheckRequest) (*ba
h := xxhash.New()

// Write the checkKey string to the hash object
_, err := h.Write([]byte(GenerateKey(key)))
_, err := h.Write([]byte(GenerateKey(key, isRelational)))
if err != nil {
// If there's an error, return nil and false
return nil, false
Expand Down Expand Up @@ -112,7 +139,7 @@ func (c *CheckEngineWithKeys) getCheckKey(key *base.PermissionCheckRequest) (*ba
// setCheckKey is a function to set a check key in the cache of the CheckEngineWithKeys.
// It takes a permission check request as a key, a permission check response as a value,
// and returns a boolean value indicating if the operation was successful.
func (c *CheckEngineWithKeys) setCheckKey(key *base.PermissionCheckRequest, value *base.PermissionCheckResponse) bool {
func (c *CheckEngineWithKeys) setCheckKey(key *base.PermissionCheckRequest, value *base.PermissionCheckResponse, isRelational bool) bool {
// If either the key or the value is nil, return false.
if key == nil || value == nil {
return false
Expand All @@ -123,7 +150,7 @@ func (c *CheckEngineWithKeys) setCheckKey(key *base.PermissionCheckRequest, valu

// Generate a key string from the permission check request and write it to the hash.
// If there's an error while writing to the hash, return false.
size, err := h.Write([]byte(GenerateKey(key)))
size, err := h.Write([]byte(GenerateKey(key, isRelational)))
if err != nil {
return false
}
Expand All @@ -133,18 +160,12 @@ func (c *CheckEngineWithKeys) setCheckKey(key *base.PermissionCheckRequest, valu

// Set the hashed key and the check result in the cache, using the size of the hashed key as an expiry.
// The Set method should return true if the operation was successful, so return the result.
return c.cache.Set(k, value.Can, int64(size))
return c.cache.Set(k, value.GetCan(), int64(size))
}

// GenerateKey function takes a PermissionCheckRequest and generates a unique key
// Key format: check|{tenant_id}|{schema_version}|{snap_token}|{context}|{entity:id#permission(optional_arguments)@subject:id#optional_relation}
func GenerateKey(key *base.PermissionCheckRequest) string {
// Create a new EntityAndRelation object with the entity and permission from the key
entityRelation := &base.EntityAndRelation{
Entity: key.GetEntity(),
Relation: key.GetPermission(),
}

func GenerateKey(key *base.PermissionCheckRequest, isRelational bool) string {
// Initialize the parts slice with the string "check"
parts := []string{"check"}

Expand All @@ -168,12 +189,22 @@ func GenerateKey(key *base.PermissionCheckRequest) string {
parts = append(parts, ContextToString(ctx))
}

// Convert entity and relation to string with any optional arguments and append to parts
entityRelationString := tuple.EntityAndRelationToString(entityRelation, key.GetArguments()...)
subjectString := tuple.SubjectToString(key.GetSubject())
if isRelational {
// Convert entity and relation to string with any optional arguments and append to parts
entityRelationString := tuple.EntityAndRelationToString(key.GetEntity(), key.GetPermission())

subjectString := tuple.SubjectToString(key.GetSubject())

if entityRelationString != "" {
parts = append(parts, fmt.Sprintf("%s@%s", entityRelationString, subjectString))
}

if entityRelationString != "" {
parts = append(parts, fmt.Sprintf("%s@%s", entityRelationString, subjectString))
} else {
parts = append(parts, attribute.EntityAndCallOrAttributeToString(
key.GetEntity(),
key.GetPermission(),
key.GetArguments()...,
))
}

// Join all parts with "|" delimiter to generate the final key
Expand Down
44 changes: 21 additions & 23 deletions internal/engines/keys/keys_test.go
Expand Up @@ -3,11 +3,9 @@ package keys
import (
"testing"

"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"

"github.com/stretchr/testify/assert"

"github.com/Permify/permify/pkg/cache/ristretto"
"github.com/Permify/permify/pkg/logger"
Expand All @@ -22,7 +20,7 @@ func TestEngineKeys_SetCheckKey(t *testing.T) {
l := logger.New("debug")

// Initialize a new EngineKeys struct with a new cache.Cache instance
engineKeys := CheckEngineWithKeys{nil, cache, l}
engineKeys := CheckEngineWithKeys{nil, nil, cache, l}

// Create a new PermissionCheckRequest and PermissionCheckResponse
checkReq := &base.PermissionCheckRequest{
Expand Down Expand Up @@ -51,15 +49,15 @@ func TestEngineKeys_SetCheckKey(t *testing.T) {
}

// Set the value for the given key in the cache
success := engineKeys.setCheckKey(checkReq, checkResp)
success := engineKeys.setCheckKey(checkReq, checkResp, true)

cache.Wait()

// Check that the operation was successful
assert.True(t, success)

// Retrieve the value for the given key from the cache
resp, found := engineKeys.getCheckKey(checkReq)
resp, found := engineKeys.getCheckKey(checkReq, true)

// Check that the key was found and the retrieved value is the same as the original value
assert.True(t, found)
Expand All @@ -74,7 +72,7 @@ func TestEngineKeys_SetCheckKey_WithHashError(t *testing.T) {
l := logger.New("debug")

// Initialize a new EngineKeys struct with a new cache.Cache instance
engineKeys := CheckEngineWithKeys{nil, cache, l}
engineKeys := CheckEngineWithKeys{nil, nil, cache, l}

// Create a new PermissionCheckRequest and PermissionCheckResponse
checkReq := &base.PermissionCheckRequest{
Expand Down Expand Up @@ -103,15 +101,15 @@ func TestEngineKeys_SetCheckKey_WithHashError(t *testing.T) {
}

// Force an error while writing the key to the hash object by passing a nil key
success := engineKeys.setCheckKey(nil, checkResp)
success := engineKeys.setCheckKey(nil, checkResp, true)

cache.Wait()

// Check that the operation was unsuccessful
assert.False(t, success)

// Retrieve the value for the given key from the cache
resp, found := engineKeys.getCheckKey(checkReq)
resp, found := engineKeys.getCheckKey(checkReq, true)

// Check that the key was not found
assert.False(t, found)
Expand All @@ -126,7 +124,7 @@ func TestEngineKeys_GetCheckKey_KeyNotFound(t *testing.T) {
l := logger.New("debug")

// Initialize a new EngineKeys struct with a new cache.Cache instance
engineKeys := CheckEngineWithKeys{nil, cache, l}
engineKeys := CheckEngineWithKeys{nil, nil, cache, l}

// Create a new PermissionCheckRequest
checkReq := &base.PermissionCheckRequest{
Expand All @@ -148,7 +146,7 @@ func TestEngineKeys_GetCheckKey_KeyNotFound(t *testing.T) {
}

// Retrieve the value for a non-existent key from the cache
resp, found := engineKeys.getCheckKey(checkReq)
resp, found := engineKeys.getCheckKey(checkReq, true)

// Check that the key was not found
assert.False(t, found)
Expand All @@ -163,7 +161,7 @@ func TestEngineKeys_SetAndGetMultipleKeys(t *testing.T) {
l := logger.New("debug")

// Initialize a new EngineKeys struct with a new cache.Cache instance
engineKeys := CheckEngineWithKeys{nil, cache, l}
engineKeys := CheckEngineWithKeys{nil, nil, cache, l}

// Create some new PermissionCheckRequests and PermissionCheckResponses
checkReq1 := &base.PermissionCheckRequest{
Expand Down Expand Up @@ -239,9 +237,9 @@ func TestEngineKeys_SetAndGetMultipleKeys(t *testing.T) {
}

// Set the values for the given keys in the cache
success1 := engineKeys.setCheckKey(checkReq1, checkResp1)
success2 := engineKeys.setCheckKey(checkReq2, checkResp2)
success3 := engineKeys.setCheckKey(checkReq3, checkResp3)
success1 := engineKeys.setCheckKey(checkReq1, checkResp1, true)
success2 := engineKeys.setCheckKey(checkReq2, checkResp2, true)
success3 := engineKeys.setCheckKey(checkReq3, checkResp3, true)

cache.Wait()

Expand All @@ -251,9 +249,9 @@ func TestEngineKeys_SetAndGetMultipleKeys(t *testing.T) {
assert.True(t, success3)

// Retrieve the value for the given key from the cache
resp1, found1 := engineKeys.getCheckKey(checkReq1)
resp2, found2 := engineKeys.getCheckKey(checkReq2)
resp3, found3 := engineKeys.getCheckKey(checkReq3)
resp1, found1 := engineKeys.getCheckKey(checkReq1, true)
resp2, found2 := engineKeys.getCheckKey(checkReq2, true)
resp3, found3 := engineKeys.getCheckKey(checkReq3, true)

// Check that the key was not found
assert.True(t, found1)
Expand All @@ -274,7 +272,7 @@ func TestEngineKeys_SetCheckKeyWithArguments(t *testing.T) {
l := logger.New("debug")

// Initialize a new EngineKeys struct with a new cache.Cache instance
engineKeys := CheckEngineWithKeys{nil, cache, l}
engineKeys := CheckEngineWithKeys{nil, nil, cache, l}

// Create a new PermissionCheckRequest and PermissionCheckResponse
checkReq := &base.PermissionCheckRequest{
Expand Down Expand Up @@ -319,23 +317,23 @@ func TestEngineKeys_SetCheckKeyWithArguments(t *testing.T) {
}

// Set the value for the given key in the cache
success := engineKeys.setCheckKey(checkReq, checkResp)
success := engineKeys.setCheckKey(checkReq, checkResp, true)

cache.Wait()

// Check that the operation was successful
assert.True(t, success)

// Retrieve the value for the given key from the cache
resp, found := engineKeys.getCheckKey(checkReq)
resp, found := engineKeys.getCheckKey(checkReq, true)

// Check that the key was found and the retrieved value is the same as the original value
assert.True(t, found)
assert.Equal(t, checkResp, resp)
}

func TestEngineKeys_SetCheckKeyWithContext(t *testing.T) {
value, err := anypb.New(wrapperspb.Bool(true))
value, err := anypb.New(&base.BooleanValue{Data: true})
if err != nil {
}

Expand Down Expand Up @@ -407,5 +405,5 @@ func TestEngineKeys_SetCheckKeyWithContext(t *testing.T) {
},
}

assert.Equal(t, "check|t1|test_version|test_snap_token|entity_type:entity_id#relation@subject_type:subject_id,entity_type:entity_id#is_public@boolean:true,day_of_a_week:saturday,day_of_a_year:35|test-entity:e1#test-rule(test_argument_1,test_argument_2)@user:u1", GenerateKey(checkReq))
assert.Equal(t, "check|t1|test_version|test_snap_token|entity_type:entity_id#relation@subject_type:subject_id,entity_type:entity_id$is_public|boolean:true,day_of_a_week:saturday,day_of_a_year:35|test-entity:e1$test-rule(test_argument_1,test_argument_2)", GenerateKey(checkReq, false))
}

0 comments on commit 97ff507

Please sign in to comment.