diff --git a/docs/api-reference/apidocs.swagger.json b/docs/api-reference/apidocs.swagger.json index 1b0ad04a4..6b108651e 100644 --- a/docs/api-reference/apidocs.swagger.json +++ b/docs/api-reference/apidocs.swagger.json @@ -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.5", + "version": "v1.1.6", "contact": { "name": "API Support", "url": "https://github.com/Permify/permify/issues", diff --git a/docs/api-reference/openapiv2/apidocs.swagger.json b/docs/api-reference/openapiv2/apidocs.swagger.json index 8083ec9ce..e493d33cb 100644 --- a/docs/api-reference/openapiv2/apidocs.swagger.json +++ b/docs/api-reference/openapiv2/apidocs.swagger.json @@ -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.5", + "version": "v1.1.6", "contact": { "name": "API Support", "url": "https://github.com/Permify/permify/issues", diff --git a/internal/engines/lookup.go b/internal/engines/lookup.go index 28b4f6ef9..f2aac0555 100644 --- a/internal/engines/lookup.go +++ b/internal/engines/lookup.go @@ -240,14 +240,36 @@ func (engine *LookupEngine) LookupSubject(ctx context.Context, request *base.Per }, nil } + start := "" + // Sort the IDs sort.Strings(ids) + // Handle continuous token if present + if request.GetContinuousToken() != "" { + var t database.ContinuousToken + t, err := utils.EncodedContinuousToken{Value: request.GetContinuousToken()}.Decode() + if err != nil { + return nil, err + } + start = t.(utils.ContinuousToken).Value + } + // Since the incoming 'ids' are already filtered based on the continuous token, // there is no need to decode or handle the continuous token manually. // The startIndex is initialized to 0. 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 { + if id >= start { + startIndex = i + break + } + } + } + // Convert size to int for compatibility with startIndex pageSize := int(size) diff --git a/internal/engines/lookup_test.go b/internal/engines/lookup_test.go index 09973098a..5ab8bd3f5 100644 --- a/internal/engines/lookup_test.go +++ b/internal/engines/lookup_test.go @@ -4570,4 +4570,187 @@ var _ = Describe("lookup-entity-engine", func() { } }) }) + + exampleSchemaSubjectFilter := ` +entity user { + attribute first_name string +} + +entity org { + relation admin @user + relation perms @group_perms +} + +entity project { + relation parent @org + relation admin @user + relation perms @group_perms + + permission project_edit = admin or parent.admin or perms.project_edit + permission project_view = project_edit or perms.project_view + permission edit = perms.project_edit +} + +entity group { + relation member @user +} + +entity group_perms { + relation members @group + + attribute can_project_edit boolean + attribute can_project_view boolean + + permission project_edit = can_project_edit and members.member + permission project_view = (can_project_view and members.member) or project_edit + permission edit = can_project_edit +} + ` + + Context("Sample: Subject Filter", func() { + It("Sample: Case 1", func() { + db, err := factories.DatabaseFactory( + config.Database{ + Engine: "memory", + }, + ) + + Expect(err).ShouldNot(HaveOccurred()) + + conf, err := newSchema(exampleSchemaSubjectFilter) + 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{ + "project:project_1#perms@group_perms:group_perms_1", + "project:project_2#perms@group_perms:group_perms_2", + + "group_perms:group_perms_1#members@group:group_1", + "group:group_1#member@user:user_1", + "group:group_1#member@user:user_3", + "group:group_1#member@user:user_4", + + "group:group_2#member@user:user_2", + }, + attributes: []string{ + "group_perms:group_perms_1$can_project_view|boolean:true", + "group_perms:group_perms_1$can_project_edit|boolean:true", + }, + filters: []filter{ + { + subjectReference: "user", + entity: "project:project_1", + assertions: map[string][]string{ + "project_view": {"user_1", "user_3", "user_4"}, + "edit": {"user_1", "user_3", "user_4"}, + }, + }, + { + subjectReference: "user", + entity: "group_perms:group_perms_1", + assertions: map[string][]string{ + "edit": {"user_1", "user_2", "user_3", "user_4"}, + }, + }, + }, + } + + // 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 { + + ct := "" + + var ids []string + + for { + 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: "", + }, + ContinuousToken: ct, + PageSize: 2, + }) + Expect(err).ShouldNot(HaveOccurred()) + + ids = append(ids, response.GetSubjectIds()...) + + ct = response.GetContinuousToken() + + if ct == "" { + break + } + } + + Expect(ids).Should(Equal(res)) + } + } + }) + }) }) diff --git a/internal/engines/subjectFilter.go b/internal/engines/subjectFilter.go index 7a3cb6987..e8aeaf4cb 100644 --- a/internal/engines/subjectFilter.go +++ b/internal/engines/subjectFilter.go @@ -429,7 +429,7 @@ func (engine *SubjectFilter) subjectFilterDirectRelation( // NewContextualRelationships() creates a ContextualRelationships instance from tuples in the request. // QueryRelationships() then uses the filter to find and return matching relationships. var cti *database.TupleIterator - cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, database.NewCursorPagination(database.Cursor(request.GetContinuousToken()), database.Sort("subject_id"))) + cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, database.NewCursorPagination()) if err != nil { return subjectFilterEmpty(), err } @@ -437,7 +437,7 @@ func (engine *SubjectFilter) subjectFilterDirectRelation( // Query the relationships for the entity in the request. // TupleFilter helps in filtering out the relationships for a specific entity and a permission. var rit *database.TupleIterator - rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), database.NewCursorPagination(database.Cursor(request.GetContinuousToken()), database.Sort("subject_id"))) + rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), database.NewCursorPagination()) if err != nil { return subjectFilterEmpty(), err } @@ -695,7 +695,7 @@ func subjectFilterUnion(ctx context.Context, functions []SubjectFilterFunction, encounteredWildcard = true // Collect any additional IDs alongside "<>", treat them as exclusions for _, id := range d.resp { - if id != "<>" && !slices.Contains(excludedIds, id) { + if id != ALL && !slices.Contains(excludedIds, id) { excludedIds = append(excludedIds, id) } } diff --git a/internal/info.go b/internal/info.go index 6a7c367fd..7cffe1829 100644 --- a/internal/info.go +++ b/internal/info.go @@ -23,7 +23,7 @@ var Identifier = "" */ const ( // Version is the last release of the Permify (e.g. v0.1.0) - Version = "v1.1.5" + Version = "v1.1.6" ) // Function to create a single line of the ASCII art with centered content and color diff --git a/internal/storage/postgres/dataReader.go b/internal/storage/postgres/dataReader.go index ded5b663c..0425d3dad 100644 --- a/internal/storage/postgres/dataReader.go +++ b/internal/storage/postgres/dataReader.go @@ -561,6 +561,9 @@ func (r *DataReader) HeadSnapshot(ctx context.Context, tenantID string) (token.S return nil, utils.HandleError(ctx, span, err, base.ErrorCode_ERROR_CODE_SQL_BUILDER) } + // TODO: To optimize this query, create the following index concurrently to avoid table locks: + // CREATE INDEX CONCURRENTLY idx_transactions_tenant_id_id ON transactions(tenant_id, id DESC); + // Execute the query and retrieve the highest transaction ID. err = r.database.ReadPool.QueryRow(ctx, query, args...).Scan(&xid) if err != nil { diff --git a/pkg/pb/base/v1/openapi.pb.go b/pkg/pb/base/v1/openapi.pb.go index 579394b41..7bb261029 100644 --- a/pkg/pb/base/v1/openapi.pb.go +++ b/pkg/pb/base/v1/openapi.pb.go @@ -46,7 +46,7 @@ var file_base_v1_openapi_proto_rawDesc = []byte{ 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x50, 0x65, 0x72, 0x6d, 0x69, 0x66, 0x79, 0x2f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x66, 0x79, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, - 0x32, 0x06, 0x76, 0x31, 0x2e, 0x31, 0x2e, 0x35, 0x2a, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, + 0x32, 0x06, 0x76, 0x31, 0x2e, 0x31, 0x2e, 0x36, 0x2a, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x5a, 0x23, 0x0a, 0x21, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, diff --git a/proto/base/v1/openapi.proto b/proto/base/v1/openapi.proto index 27eb1fd88..f9da55442 100644 --- a/proto/base/v1/openapi.proto +++ b/proto/base/v1/openapi.proto @@ -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.5"; + version: "v1.1.6"; contact: { name: "API Support"; url: "https://github.com/Permify/permify/issues";