Passed
Pull Request — master (#1465)
by
unknown
02:36
created

engines.*SchemaBasedEntityFilter.l   C

Complexity

Conditions 8

Size

Total Lines 58
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 44
nop 7
dl 0
loc 58
rs 6.9573
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
package engines
2
3
import (
4
	"context"
5
	"errors"
6
7
	"golang.org/x/sync/errgroup"
8
9
	"github.com/Permify/permify/internal/schema"
10
	"github.com/Permify/permify/internal/storage"
11
	storageContext "github.com/Permify/permify/internal/storage/context"
12
	"github.com/Permify/permify/pkg/database"
13
	base "github.com/Permify/permify/pkg/pb/base/v1"
14
	"github.com/Permify/permify/pkg/tuple"
15
)
16
17
// SchemaBasedEntityFilter is a struct that performs permission checks on a set of entities
18
type SchemaBasedEntityFilter struct {
19
	// dataReader is responsible for reading relationship information
20
	dataReader storage.DataReader
21
22
	schema *base.SchemaDefinition
23
}
24
25
// NewSchemaBasedEntityFilter creates a new EntityFilter engine
26
func NewSchemaBasedEntityFilter(dataReader storage.DataReader, sch *base.SchemaDefinition) *SchemaBasedEntityFilter {
27
	return &SchemaBasedEntityFilter{
28
		dataReader: dataReader,
29
		schema:     sch,
30
	}
31
}
32
33
// EntityFilter is a method of the EntityFilterEngine struct. It executes a permission request for linked entities.
34
func (engine *SchemaBasedEntityFilter) EntityFilter(
35
	ctx context.Context, // A context used for tracing and cancellation.
36
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
37
	visits *ERMap, // A map that keeps track of visited entities to avoid infinite loops.
38
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
39
	permissionChecks *ERMap, // A thread safe map to check if for a entity same permission already checked or not
40
) (err error) { // Returns an error if one occurs during execution.
41
	// Check if direct result
42
	for _, entityReference := range request.GetEntityReferences() {
43
		if entityReference.GetType() == request.GetSubject().GetType() && entityReference.GetRelation() == request.GetSubject().GetRelation() {
44
			found := &base.Entity{
45
				Type: request.GetSubject().GetType(),
46
				Id:   request.GetSubject().GetId(),
47
			}
48
			// If the entity reference is the same as the subject, publish the result directly and return.
49
			publisher.Publish(found, &base.PermissionCheckRequestMetadata{
50
				SnapToken:     request.GetMetadata().GetSnapToken(),
51
				SchemaVersion: request.GetMetadata().GetSchemaVersion(),
52
				Depth:         request.GetMetadata().GetDepth(),
53
			}, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED, permissionChecks)
54
			// Breaking loop here as publisher would make sure now to check for all permissions
55
			break
56
		}
57
	}
58
59
	// Retrieve linked entrances
60
	cn := schema.NewLinkedGraph(engine.schema) // Create a new linked graph from the schema definition.
61
	var entrances []*schema.LinkedEntrance
62
	entrances, err = cn.RelationshipLinkedEntrances(
63
		request.GetEntityReferences(),
64
		&base.RelationReference{
65
			Type:     request.GetSubject().GetType(),
66
			Relation: request.GetSubject().GetRelation(),
67
		},
68
	) // Retrieve the linked entrances between the entity reference and subject.
69
70
	// Create a new context for executing goroutines and a cancel function.
71
	cctx, cancel := context.WithCancel(ctx)
72
	defer cancel()
73
74
	// Create a new errgroup and a new context that inherits the original context.
75
	g, cont := errgroup.WithContext(cctx)
76
77
	// Loop over each linked entrance.
78
	for _, entrance := range entrances {
79
		// Switch on the kind of linked entrance.
80
		switch entrance.LinkedEntranceKind() {
81
		case schema.RelationLinkedEntrance: // If the linked entrance is a relation entrance.
82
			err = engine.relationEntrance(cont, request, entrance, visits, g, publisher, permissionChecks) // Call the relation entrance method.
83
			if err != nil {
84
				return err
85
			}
86
		case schema.ComputedUserSetLinkedEntrance: // If the linked entrance is a computed user set entrance.
87
			err = engine.l(ctx, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
88
				Entity: &base.Entity{
89
					Type: entrance.TargetEntrance.GetType(),
90
					Id:   request.GetSubject().GetId(),
91
				},
92
				Relation: entrance.TargetEntrance.GetRelation(),
93
			}, visits, g, publisher, permissionChecks)
94
			if err != nil {
95
				return err
96
			}
97
		case schema.TupleToUserSetLinkedEntrance: // If the linked entrance is a tuple to user set entrance.
98
			err = engine.tupleToUserSetEntrance(cont, request, entrance, visits, g, publisher, permissionChecks) // Call the tuple to user set entrance method.
99
			if err != nil {
100
				return err
101
			}
102
		default:
103
			return errors.New("unknown linked entrance type") // Return an error if the linked entrance is of an unknown type.
104
		}
105
	}
106
107
	return g.Wait() // Wait for all goroutines in the errgroup to complete and return any errors that occur.
108
}
109
110
// relationEntrance is a method of the EntityFilterEngine struct. It handles relation entrances.
111
func (engine *SchemaBasedEntityFilter) relationEntrance(
112
	ctx context.Context, // A context used for tracing and cancellation.
113
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
114
	entrance *schema.LinkedEntrance, // A linked entrance.
115
	visits *ERMap, // A map that keeps track of visited entities to avoid infinite loops.
116
	g *errgroup.Group, // An errgroup used for executing goroutines.
117
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
118
	permissionChecks *ERMap, // A thread safe map to check if for a entity same permission already checked or not
119
) error { // Returns an error if one occurs during execution.
120
	// Define a TupleFilter. This specifies which tuples we're interested in.
121
	// We want tuples that match the entity type and ID from the request, and have a specific relation.
122
	filter := &base.TupleFilter{
123
		Entity: &base.EntityFilter{
124
			Type: entrance.TargetEntrance.GetType(),
125
			Ids:  []string{},
126
		},
127
		Relation: entrance.TargetEntrance.GetRelation(),
128
		Subject: &base.SubjectFilter{
129
			Type:     request.GetSubject().GetType(),
130
			Ids:      []string{request.GetSubject().GetId()},
131
			Relation: request.GetSubject().GetRelation(),
132
		},
133
	}
134
135
	var (
136
		cti, rit   *database.TupleIterator
137
		err        error
138
		pagination database.CursorPagination
139
	)
140
141
	// Determine the pagination settings based on the entity type in the request.
142
	// If the entity type matches the target entrance, use cursor pagination with sorting by "entity_id".
143
	// Otherwise, use the default pagination settings.
144
	pagination = database.NewCursorPagination()
145
	for _, entityReference := range request.GetEntityReferences() {
146
		if entityReference.GetType() == entrance.TargetEntrance.GetType() {
147
			pagination = database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
148
			break
149
		}
150
	}
151
152
	// Query the relationships using the specified pagination settings.
153
	// The context tuples are filtered based on the provided filter.
154
	cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, pagination)
155
	if err != nil {
156
		return err
157
	}
158
159
	// Query the relationships for the entity in the request.
160
	// The results are filtered based on the provided filter and pagination settings.
161
	rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
162
	if err != nil {
163
		return err
164
	}
165
166
	// Create a new UniqueTupleIterator from the two TupleIterators.
167
	// NewUniqueTupleIterator() ensures that the iterator only returns unique tuples.
168
	it := database.NewUniqueTupleIterator(rit, cti)
169
170
	for it.HasNext() { // Loop over each relationship.
171
		// Get the next tuple's subject.
172
		current, ok := it.GetNext()
173
		if !ok {
174
			break
175
		}
176
		g.Go(func() error {
177
			return engine.l(ctx, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
178
				Entity: &base.Entity{
179
					Type: current.GetEntity().GetType(),
180
					Id:   current.GetEntity().GetId(),
181
				},
182
				Relation: current.GetRelation(),
183
			}, visits, g, publisher, permissionChecks)
184
		})
185
	}
186
	return nil
187
}
188
189
// tupleToUserSetEntrance is a method of the EntityFilterEngine struct. It handles tuple to user set entrances.
190
func (engine *SchemaBasedEntityFilter) tupleToUserSetEntrance(
191
	// A context used for tracing and cancellation.
192
	ctx context.Context,
193
	// A permission request for linked entities.
194
	request *base.PermissionEntityFilterRequest,
195
	// A linked entrance.
196
	entrance *schema.LinkedEntrance,
197
	// A map that keeps track of visited entities to avoid infinite loops.
198
	visits *ERMap,
199
	// An errgroup used for executing goroutines.
200
	g *errgroup.Group,
201
	// A custom publisher that publishes results in bulk.
202
	publisher *BulkEntityPublisher,
203
	// A thread safe map to check if for a entity same permission already checked or not
204
	permissionChecks *ERMap,
205
) error { // Returns an error if one occurs during execution.
206
	for _, relation := range []string{"", tuple.ELLIPSIS} {
207
		// Define a TupleFilter. This specifies which tuples we're interested in.
208
		// We want tuples that match the entity type and ID from the request, and have a specific relation.
209
		filter := &base.TupleFilter{
210
			Entity: &base.EntityFilter{
211
				Type: entrance.TargetEntrance.GetType(),
212
				Ids:  []string{},
213
			},
214
			Relation: entrance.TupleSetRelation, // Query for relationships that match the tuple set relation.
215
			Subject: &base.SubjectFilter{
216
				Type:     request.GetSubject().GetType(),
217
				Ids:      []string{request.GetSubject().GetId()},
218
				Relation: relation,
219
			},
220
		}
221
222
		var (
223
			cti, rit   *database.TupleIterator
224
			err        error
225
			pagination database.CursorPagination
226
		)
227
228
		// Determine the pagination settings based on the entity type in the request.
229
		// If the entity type matches the target entrance, use cursor pagination with sorting by "entity_id".
230
		// Otherwise, use the default pagination settings.
231
		pagination = database.NewCursorPagination()
232
		for _, entityReference := range request.GetEntityReferences() {
233
			if entityReference.GetType() == entrance.TargetEntrance.GetType() {
234
				pagination = database.NewCursorPagination(database.Cursor(request.GetCursor()), database.Sort("entity_id"))
235
				break
236
			}
237
		}
238
239
		// Query the relationships using the specified pagination settings.
240
		// The context tuples are filtered based on the provided filter.
241
		cti, err = storageContext.NewContextualTuples(request.GetContext().GetTuples()...).QueryRelationships(filter, pagination)
242
		if err != nil {
243
			return err
244
		}
245
246
		// Query the relationships for the entity in the request.
247
		// The results are filtered based on the provided filter and pagination settings.
248
		rit, err = engine.dataReader.QueryRelationships(ctx, request.GetTenantId(), filter, request.GetMetadata().GetSnapToken(), pagination)
249
		if err != nil {
250
			return err
251
		}
252
253
		// Create a new UniqueTupleIterator from the two TupleIterators.
254
		// NewUniqueTupleIterator() ensures that the iterator only returns unique tuples.
255
		it := database.NewUniqueTupleIterator(rit, cti)
256
257
		for it.HasNext() { // Loop over each relationship.
258
			// Get the next tuple's subject.
259
			current, ok := it.GetNext()
260
			if !ok {
261
				break
262
			}
263
			g.Go(func() error {
264
				return engine.l(ctx, request, &base.EntityAndRelation{ // Call the run method with a new entity and relation.
265
					Entity: &base.Entity{
266
						Type: entrance.TargetEntrance.GetType(),
267
						Id:   current.GetEntity().GetId(),
268
					},
269
					Relation: entrance.TargetEntrance.GetRelation(),
270
				}, visits, g, publisher, permissionChecks)
271
			})
272
		}
273
	}
274
	return nil
275
}
276
277
// run is a method of the EntityFilterEngine struct. It executes the linked entity engine for a given request.
278
func (engine *SchemaBasedEntityFilter) l(
279
	ctx context.Context, // A context used for tracing and cancellation.
280
	request *base.PermissionEntityFilterRequest, // A permission request for linked entities.
281
	found *base.EntityAndRelation, // An entity and relation that was previously found.
282
	visits *ERMap, // A map that keeps track of visited entities to avoid infinite loops.
283
	g *errgroup.Group, // An errgroup used for executing goroutines.
284
	publisher *BulkEntityPublisher, // A custom publisher that publishes results in bulk.
285
	permissionChecks *ERMap, // A thread safe map to check if for a entity same permission already checked or not
286
) error { // Returns an error if one occurs during execution.
287
	if !visits.Add(found.GetEntity(), found.GetRelation()) { // If the entity and relation has already been visited.
288
		return nil
289
	}
290
291
	var err error
292
293
	// Retrieve linked entrances
294
	cn := schema.NewLinkedGraph(engine.schema)
295
	var entrances []*schema.LinkedEntrance
296
	entrances, err = cn.RelationshipLinkedEntrances(
297
		request.GetEntityReferences(),
298
		&base.RelationReference{
299
			Type:     request.GetSubject().GetType(),
300
			Relation: request.GetSubject().GetRelation(),
301
		},
302
	) // Retrieve the linked entrances for the request.
303
	if err != nil {
304
		return err
305
	}
306
307
	if entrances == nil { // If there are no linked entrances for the request.
308
		for _, reference := range request.GetEntityReferences() {
309
			if found.GetEntity().GetType() == reference.GetType() && found.GetRelation() == reference.GetRelation() { // Check if the found entity matches the requested entity reference.
310
				publisher.Publish(found.GetEntity(), &base.PermissionCheckRequestMetadata{ // Publish the found entity with the permission check metadata.
311
					SnapToken:     request.GetMetadata().GetSnapToken(),
312
					SchemaVersion: request.GetMetadata().GetSchemaVersion(),
313
					Depth:         request.GetMetadata().GetDepth(),
314
				}, request.GetContext(), base.CheckResult_CHECK_RESULT_UNSPECIFIED, permissionChecks)
315
				return nil
316
			}
317
		}
318
		return nil // Otherwise, return without publishing any results.
319
	}
320
321
	g.Go(func() error {
322
		return engine.EntityFilter(ctx, &base.PermissionEntityFilterRequest{ // Call the Run method recursively with a new permission request.
323
			TenantId:         request.GetTenantId(),
324
			EntityReferences: request.GetEntityReferences(),
325
			Subject: &base.Subject{
326
				Type:     found.GetEntity().GetType(),
327
				Id:       found.GetEntity().GetId(),
328
				Relation: found.GetRelation(),
329
			},
330
			Metadata: request.GetMetadata(),
331
			Context:  request.GetContext(),
332
			Cursor:   request.GetCursor(),
333
		}, visits, publisher, permissionChecks)
334
	})
335
	return nil
336
}
337