Skip to content

Commit

Permalink
Merge pull request #1696 from Permify/feat/handle-wildcard-exclusions…
Browse files Browse the repository at this point in the history
…-union-intersection

feat(subject-filter): handle wildcard and exclusions in union and int…
  • Loading branch information
tolgaOzen authored Oct 16, 2024
2 parents 5dc383c + 7df0189 commit 3ad0240
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 169 deletions.
41 changes: 29 additions & 12 deletions internal/engines/lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"slices"
"sort"
"strings"
"sync"

"github.com/Permify/permify/internal/invoke"
Expand Down Expand Up @@ -192,7 +193,7 @@ func (engine *LookupEngine) LookupSubject(ctx context.Context, request *base.Per
size = 1000
}

var ids IdResponse
var ids []string
var ct string

// Use the schema-based subject filter to get the list of subjects with the requested permission.
Expand All @@ -201,13 +202,29 @@ func (engine *LookupEngine) LookupSubject(ctx context.Context, request *base.Per
return nil, err
}

// Check if the wildcard '*' is present in the ids.Ids
if slices.Contains(ids.Ids, "*") {
// Initialize excludedIds to be used in the query
var excludedIds []string

// Check if the wildcard '*' is present in the ids.Ids or if it's formatted like "*-1,2,3"
for _, id := range ids {
if id == "*" {
// Handle '*' case: no exclusions, include all resources
excludedIds = nil
break
} else if strings.HasPrefix(id, "*-") {
// Handle '*-1,2,3' case: parse exclusions after '-'
excludedIds = strings.Split(strings.TrimPrefix(id, "*-"), ",")
break
}
}

// If '*' was found, query all subjects with exclusions if provided
if excludedIds != nil || slices.Contains(ids, "*") {
resp, pct, err := engine.dataReader.QueryUniqueSubjectReferences(
ctx,
request.GetTenantId(),
request.GetSubjectReference(),
ids.ExcludedIds,
excludedIds, // Pass the exclusions if any
request.GetMetadata().GetSnapToken(),
database.NewPagination(database.Size(size), database.Token(request.GetContinuousToken())),
)
Expand All @@ -224,7 +241,7 @@ func (engine *LookupEngine) LookupSubject(ctx context.Context, request *base.Per
}

// Sort the IDs
sort.Strings(ids.Ids)
sort.Strings(ids)

// Initialize the start index as a string (to match token format)
start := ""
Expand All @@ -243,7 +260,7 @@ func (engine *LookupEngine) LookupSubject(ctx context.Context, request *base.Per
startIndex := 0
if start != "" {
// Locate the position in the sorted list where the ID equals or exceeds the token value
for i, id := range ids.Ids {
for i, id := range ids {
if id >= start {
startIndex = i
break
Expand All @@ -256,21 +273,21 @@ func (engine *LookupEngine) LookupSubject(ctx context.Context, request *base.Per

// Calculate the end index based on the page size
end := startIndex + pageSize
if end > len(ids.Ids) {
end = len(ids.Ids)
if end > len(ids) {
end = len(ids)
}

// Generate the next continuous token if there are more results
if end < len(ids.Ids) {
ct = utils.NewContinuousToken(ids.Ids[end]).Encode().String()
if end < len(ids) {
ct = utils.NewContinuousToken(ids[end]).Encode().String()
} else {
ct = ""
}

// Return the paginated and sorted list of IDs
return &base.PermissionLookupSubjectResponse{
SubjectIds: ids.Ids[startIndex:end], // Slice the IDs based on pagination
ContinuousToken: ct, // Return the next continuous token
SubjectIds: ids[startIndex:end], // Slice the IDs based on pagination
ContinuousToken: ct, // Return the next continuous token
}, nil
}

Expand Down
164 changes: 164 additions & 0 deletions internal/engines/lookup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4078,17 +4078,22 @@ var _ = Describe("lookup-entity-engine", func() {
attribute balance integer
permission view = check_balance(balance) and member
permission delete = check_balance(balance) not member
}
entity repository {
relation organization @organization
relation member @user
attribute is_public boolean
permission view = is_public
permission edit = organization.view
permission delete = is_workday(is_public)
permission up = is_public not organization.member
permission deploy = is_public not member
permission check = is_public and organization.delete
}
rule check_balance(balance integer) {
Expand Down Expand Up @@ -4230,5 +4235,164 @@ var _ = Describe("lookup-entity-engine", func() {
}
}
})

It("Weekday Sample: Case 2", func() {
db, err := factories.DatabaseFactory(
config.Database{
Engine: "memory",
},
)

Expect(err).ShouldNot(HaveOccurred())

conf, err := newSchema(workdaySchemaSubjectFilter)
Expect(err).ShouldNot(HaveOccurred())

schemaWriter := factories.SchemaWriterFactory(db)
err = schemaWriter.WriteSchema(context.Background(), conf)

Expect(err).ShouldNot(HaveOccurred())

type filter struct {
subjectReference string
entity string
assertions map[string][]string
}

tests := struct {
relationships []string
attributes []string
filters []filter
}{
relationships: []string{
"organization:1#member@user:1",
"repository:4#organization@organization:1",

"repository:3#organization@organization:1",
"repository:1#organization@organization:1",

"organization:2#member@user:1",
"organization:2#member@user:3",
"organization:5#member@user:2",
"organization:5#member@user:5",

"repository:12#member@user:1",
"repository:12#member@user:2",

"repository:82#organization@organization:43",

"organization:43#member@user:90",
"organization:43#member@user:54",
},
attributes: []string{
"repository:1$is_public|boolean:true",
"repository:2$is_public|boolean:false",
"repository:3$is_public|boolean:true",
"repository:12$is_public|boolean:true",
"repository:82$is_public|boolean:true",

"organization:1$balance|integer:4000",
"organization:2$balance|integer:6000",

"organization:43$balance|integer:6000",
},
filters: []filter{
{
subjectReference: "user",
entity: "repository:1",
assertions: map[string][]string{
"up": {"2", "3", "5", "54", "90"},
},
},
{
subjectReference: "user",
entity: "repository:3",
assertions: map[string][]string{
"up": {"2", "3", "5", "54", "90"},
},
},
{
subjectReference: "user",
entity: "repository:12",
assertions: map[string][]string{
"deploy": {"3", "5", "54", "90"},
},
},
{
subjectReference: "user",
entity: "repository:82",
assertions: map[string][]string{
"check": {"1", "2", "3", "5"},
},
},
},
}

// filters

schemaReader := factories.SchemaReaderFactory(db)
dataReader := factories.DataReaderFactory(db)
dataWriter := factories.DataWriterFactory(db)

checkEngine := NewCheckEngine(schemaReader, dataReader)

lookupEngine := NewLookupEngine(
checkEngine,
schemaReader,
dataReader,
)

invoker := invoke.NewDirectInvoker(
schemaReader,
dataReader,
checkEngine,
nil,
lookupEngine,
nil,
)

checkEngine.SetInvoker(invoker)

var tuples []*base.Tuple

for _, relationship := range tests.relationships {
t, err := tuple.Tuple(relationship)
Expect(err).ShouldNot(HaveOccurred())
tuples = append(tuples, t)
}

var attributes []*base.Attribute

for _, attr := range tests.attributes {
a, err := attribute.Attribute(attr)
Expect(err).ShouldNot(HaveOccurred())
attributes = append(attributes, a)
}

_, err = dataWriter.Write(context.Background(), "t1", database.NewTupleCollection(tuples...), database.NewAttributeCollection(attributes...))
Expect(err).ShouldNot(HaveOccurred())

for _, filter := range tests.filters {
entity, err := tuple.E(filter.entity)
Expect(err).ShouldNot(HaveOccurred())

for permission, res := range filter.assertions {
response, err := invoker.LookupSubject(context.Background(), &base.PermissionLookupSubjectRequest{
TenantId: "t1",
SubjectReference: tuple.RelationReference(filter.subjectReference),
Entity: entity,
Permission: permission,
Metadata: &base.PermissionLookupSubjectRequestMetadata{
SnapToken: token.NewNoopToken().Encode().String(),
SchemaVersion: "",
Depth: 100,
},
})

Expect(err).ShouldNot(HaveOccurred())
Expect(response.GetSubjectIds()).Should(Equal(res))
}
}
})
})
})
Loading

0 comments on commit 3ad0240

Please sign in to comment.