Passed
Pull Request — master (#1470)
by Tolga
02:39
created

rSetEntrance   C

Complexity

Conditions 8

Size

Total Lines 81
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

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