development.*Development.RunWithShape   F
last analyzed

Complexity

Conditions 38

Size

Total Lines 380
Code Lines 240

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 38
eloc 240
nop 2
dl 0
loc 380
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

Complexity

Complex classes like development.*Development.RunWithShape often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
package development
2
3
import (
4
	"context"
5
	"fmt"
6
	"log/slog"
7
	"os"
8
	"sort"
9
	"strings"
10
11
	"google.golang.org/protobuf/types/known/structpb"
12
13
	"gopkg.in/yaml.v3"
14
15
	"github.com/rs/xid"
16
17
	"github.com/Permify/permify/internal/config"
18
	"github.com/Permify/permify/internal/engines"
19
	"github.com/Permify/permify/internal/factories"
20
	"github.com/Permify/permify/internal/invoke"
21
	"github.com/Permify/permify/internal/servers"
22
	"github.com/Permify/permify/internal/storage"
23
	"github.com/Permify/permify/internal/validation"
24
	"github.com/Permify/permify/pkg/attribute"
25
	"github.com/Permify/permify/pkg/database"
26
	"github.com/Permify/permify/pkg/development/file"
27
	"github.com/Permify/permify/pkg/dsl/compiler"
28
	"github.com/Permify/permify/pkg/dsl/parser"
29
	v1 "github.com/Permify/permify/pkg/pb/base/v1"
30
	"github.com/Permify/permify/pkg/token"
31
	"github.com/Permify/permify/pkg/tuple"
32
)
33
34
type Development struct {
35
	Container *servers.Container
36
}
37
38
func NewContainer() *Development {
39
	var err error
40
41
	// Create a new in-memory database using the factories package
42
	var db database.Database
43
	db, err = factories.DatabaseFactory(config.Database{Engine: database.MEMORY.String()})
44
	if err != nil {
45
		fmt.Println(err)
46
	}
47
48
	// Create a new logger instance
49
	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
50
	slog.SetDefault(logger)
51
52
	// Create instances of storage using the factories package
53
	dataReader := factories.DataReaderFactory(db)
54
	dataWriter := factories.DataWriterFactory(db)
55
	bundleReader := factories.BundleReaderFactory(db)
56
	bundleWriter := factories.BundleWriterFactory(db)
57
	schemaReader := factories.SchemaReaderFactory(db)
58
	schemaWriter := factories.SchemaWriterFactory(db)
59
	tenantReader := factories.TenantReaderFactory(db)
60
	tenantWriter := factories.TenantWriterFactory(db)
61
62
	// Create instances of engines
63
	checkEngine := engines.NewCheckEngine(schemaReader, dataReader)
64
	expandEngine := engines.NewExpandEngine(schemaReader, dataReader)
65
	lookupEngine := engines.NewLookupEngine(checkEngine, schemaReader, dataReader)
66
	subjectPermissionEngine := engines.NewSubjectPermission(checkEngine, schemaReader)
67
68
	invoker := invoke.NewDirectInvoker(
69
		schemaReader,
70
		dataReader,
71
		checkEngine,
72
		expandEngine,
73
		lookupEngine,
74
		subjectPermissionEngine,
75
	)
76
77
	checkEngine.SetInvoker(invoker)
78
79
	// Create a new container instance with engines, storage, and other dependencies
80
	return &Development{
81
		Container: servers.NewContainer(
82
			invoker,
83
			dataReader,
84
			dataWriter,
85
			bundleReader,
86
			bundleWriter,
87
			schemaReader,
88
			schemaWriter,
89
			tenantReader,
90
			tenantWriter,
91
			storage.NewNoopWatcher(),
92
		),
93
	}
94
}
95
96
// ReadSchema - Creates new read schema request
97
func (c *Development) ReadSchema(ctx context.Context) (sch *v1.SchemaDefinition, err error) {
98
	// Get the head version of the "t1" schema from the schema repository
99
	version, err := c.Container.SR.HeadVersion(ctx, "t1")
100
	if err != nil {
101
		return nil, err
102
	}
103
104
	// Read the schema definition for the given schema and version from the schema repository
105
	return c.Container.SR.ReadSchema(ctx, "t1", version)
106
}
107
108
type Error struct {
109
	Type    string `json:"type"`
110
	Key     any    `json:"key"`
111
	Message string `json:"message"`
112
}
113
114
func (c *Development) Run(ctx context.Context, shape map[string]interface{}) (errors []Error) {
115
	// Marshal the shape map into YAML format
116
	out, err := yaml.Marshal(shape)
117
	if err != nil {
118
		errors = append(errors, Error{
119
			Type:    "file_validation",
120
			Key:     "",
121
			Message: err.Error(),
122
		})
123
		return errors
124
	}
125
126
	// Unmarshal the YAML data into a file.Shape object
127
	s := &file.Shape{}
128
	err = yaml.Unmarshal(out, &s)
129
	if err != nil {
130
		errors = append(errors, Error{
131
			Type:    "file_validation",
132
			Key:     "",
133
			Message: err.Error(),
134
		})
135
		return errors
136
	}
137
138
	return c.RunWithShape(ctx, s)
139
}
140
141
func (c *Development) RunWithShape(ctx context.Context, shape *file.Shape) (errors []Error) {
142
	// Parse the schema using the parser library
143
	sch, err := parser.NewParser(shape.Schema).Parse()
144
	if err != nil {
145
		errors = append(errors, Error{
146
			Type:    "schema",
147
			Key:     "",
148
			Message: err.Error(),
149
		})
150
		return errors
151
	}
152
153
	// Compile the parsed schema
154
	_, _, err = compiler.NewCompiler(true, sch).Compile()
155
	if err != nil {
156
		errors = append(errors, Error{
157
			Type:    "schema",
158
			Key:     "",
159
			Message: err.Error(),
160
		})
161
		return errors
162
	}
163
164
	// Generate a new unique ID for this version of the schema
165
	version := xid.New().String()
166
167
	// Create a slice of SchemaDefinitions, one for each statement in the schema
168
	cnf := make([]storage.SchemaDefinition, 0, len(sch.Statements))
169
	for _, st := range sch.Statements {
170
		cnf = append(cnf, storage.SchemaDefinition{
171
			TenantID:             "t1",
172
			Version:              version,
173
			Name:                 st.GetName(),
174
			SerializedDefinition: []byte(st.String()),
175
		})
176
	}
177
178
	// Write the schema definitions into the storage
179
	err = c.Container.SW.WriteSchema(ctx, cnf)
180
	if err != nil {
181
		errors = append(errors, Error{
182
			Type:    "schema",
183
			Key:     "",
184
			Message: err.Error(),
185
		})
186
		return errors
187
	}
188
189
	// Each item in the Relationships slice is processed individually
190
	for _, t := range shape.Relationships {
191
		tup, err := tuple.Tuple(t)
192
		if err != nil {
193
			errors = append(errors, Error{
194
				Type:    "relationships",
195
				Key:     t,
196
				Message: err.Error(),
197
			})
198
			continue
199
		}
200
201
		// Read the schema definition for this relationship
202
		definition, _, err := c.Container.SR.ReadEntityDefinition(ctx, "t1", tup.GetEntity().GetType(), version)
203
		if err != nil {
204
			errors = append(errors, Error{
205
				Type:    "relationships",
206
				Key:     t,
207
				Message: err.Error(),
208
			})
209
			continue
210
		}
211
212
		// Validate the relationship tuple against the schema definition
213
		err = validation.ValidateTuple(definition, tup)
214
		if err != nil {
215
			errors = append(errors, Error{
216
				Type:    "relationships",
217
				Key:     t,
218
				Message: err.Error(),
219
			})
220
			continue
221
		}
222
223
		// Write the relationship to the database
224
		_, err = c.Container.DW.Write(ctx, "t1", database.NewTupleCollection(tup), database.NewAttributeCollection())
225
		// Continue to the next relationship if an error occurred
226
		if err != nil {
227
			errors = append(errors, Error{
228
				Type:    "relationships",
229
				Key:     t,
230
				Message: err.Error(),
231
			})
232
			continue
233
		}
234
	}
235
236
	// Each item in the Attributes slice is processed individually
237
	for _, a := range shape.Attributes {
238
		attr, err := attribute.Attribute(a)
239
		if err != nil {
240
			errors = append(errors, Error{
241
				Type:    "attributes",
242
				Key:     a,
243
				Message: err.Error(),
244
			})
245
			continue
246
		}
247
248
		// Read the schema definition for this attribute
249
		definition, _, err := c.Container.SR.ReadEntityDefinition(ctx, "t1", attr.GetEntity().GetType(), version)
250
		if err != nil {
251
			errors = append(errors, Error{
252
				Type:    "attributes",
253
				Key:     a,
254
				Message: err.Error(),
255
			})
256
			continue
257
		}
258
259
		// Validate the attribute against the schema definition
260
		err = validation.ValidateAttribute(definition, attr)
261
		if err != nil {
262
			errors = append(errors, Error{
263
				Type:    "attributes",
264
				Key:     a,
265
				Message: err.Error(),
266
			})
267
			continue
268
		}
269
270
		// Write the attribute to the database
271
		_, err = c.Container.DW.Write(ctx, "t1", database.NewTupleCollection(), database.NewAttributeCollection(attr))
272
		// Continue to the next attribute if an error occurred
273
		if err != nil {
274
			errors = append(errors, Error{
275
				Type:    "attributes",
276
				Key:     a,
277
				Message: err.Error(),
278
			})
279
			continue
280
		}
281
	}
282
283
	// Each item in the Scenarios slice is processed individually
284
	for i, scenario := range shape.Scenarios {
285
		// Each Check in the current scenario is processed
286
		for _, check := range scenario.Checks {
287
			entity, err := tuple.E(check.Entity)
288
			if err != nil {
289
				errors = append(errors, Error{
290
					Type:    "scenarios",
291
					Key:     i,
292
					Message: err.Error(),
293
				})
294
				continue
295
			}
296
297
			ear, err := tuple.EAR(check.Subject)
298
			if err != nil {
299
				errors = append(errors, Error{
300
					Type:    "scenarios",
301
					Key:     i,
302
					Message: err.Error(),
303
				})
304
				continue
305
			}
306
307
			cont, err := Context(check.Context)
308
			if err != nil {
309
				errors = append(errors, Error{
310
					Type:    "scenarios",
311
					Key:     i,
312
					Message: err.Error(),
313
				})
314
				continue
315
			}
316
317
			subject := &v1.Subject{
318
				Type:     ear.GetEntity().GetType(),
319
				Id:       ear.GetEntity().GetId(),
320
				Relation: ear.GetRelation(),
321
			}
322
323
			// Each Assertion in the current check is processed
324
			for permission, expected := range check.Assertions {
325
				exp := v1.CheckResult_CHECK_RESULT_ALLOWED
326
				if !expected {
327
					exp = v1.CheckResult_CHECK_RESULT_DENIED
328
				}
329
330
				// A Permission Check is made for the current entity, permission and subject
331
				res, err := c.Container.Invoker.Check(ctx, &v1.PermissionCheckRequest{
332
					TenantId: "t1",
333
					Metadata: &v1.PermissionCheckRequestMetadata{
334
						SchemaVersion: version,
335
						SnapToken:     token.NewNoopToken().Encode().String(),
336
						Depth:         100,
337
					},
338
					Context:    cont,
339
					Entity:     entity,
340
					Permission: permission,
341
					Subject:    subject,
342
				})
343
				if err != nil {
344
					errors = append(errors, Error{
345
						Type:    "scenarios",
346
						Key:     i,
347
						Message: err.Error(),
348
					})
349
					continue
350
				}
351
352
				query := tuple.SubjectToString(subject) + " " + permission + " " + tuple.EntityToString(entity)
353
354
				// Check if the permission check result matches the expected result
355
				if res.Can != exp {
356
					var expectedStr, actualStr string
357
					if exp == v1.CheckResult_CHECK_RESULT_ALLOWED {
358
						expectedStr = "true"
359
					} else {
360
						expectedStr = "false"
361
					}
362
363
					if res.Can == v1.CheckResult_CHECK_RESULT_ALLOWED {
364
						actualStr = "true"
365
					} else {
366
						actualStr = "false"
367
					}
368
369
					// Construct a detailed error message with the expected result, actual result, and the query
370
					errorMsg := fmt.Sprintf("Query: %s, Expected: %s, Actual: %s", query, expectedStr, actualStr)
371
372
					errors = append(errors, Error{
373
						Type:    "scenarios",
374
						Key:     i,
375
						Message: errorMsg,
376
					})
377
				}
378
			}
379
		}
380
381
		// Each EntityFilter in the current scenario is processed
382
		for _, filter := range scenario.EntityFilters {
383
			ear, err := tuple.EAR(filter.Subject)
384
			if err != nil {
385
				errors = append(errors, Error{
386
					Type:    "scenarios",
387
					Key:     i,
388
					Message: err.Error(),
389
				})
390
				continue
391
			}
392
393
			cont, err := Context(filter.Context)
394
			if err != nil {
395
				errors = append(errors, Error{
396
					Type:    "scenarios",
397
					Key:     i,
398
					Message: err.Error(),
399
				})
400
				continue
401
			}
402
403
			subject := &v1.Subject{
404
				Type:     ear.GetEntity().GetType(),
405
				Id:       ear.GetEntity().GetId(),
406
				Relation: ear.GetRelation(),
407
			}
408
409
			// Each Assertion in the current filter is processed
410
411
			for permission, expected := range filter.Assertions {
412
				// Perform a lookup for the entity with the given subject and permission
413
				res, err := c.Container.Invoker.LookupEntity(ctx, &v1.PermissionLookupEntityRequest{
414
					TenantId: "t1",
415
					Metadata: &v1.PermissionLookupEntityRequestMetadata{
416
						SchemaVersion: version,
417
						SnapToken:     token.NewNoopToken().Encode().String(),
418
						Depth:         100,
419
					},
420
					Context:    cont,
421
					EntityType: filter.EntityType,
422
					Permission: permission,
423
					Subject:    subject,
424
				})
425
				if err != nil {
426
					errors = append(errors, Error{
427
						Type:    "scenarios",
428
						Key:     i,
429
						Message: err.Error(),
430
					})
431
					continue
432
				}
433
434
				query := tuple.SubjectToString(subject) + " " + permission + " " + filter.EntityType
435
436
				// Check if the actual result of the entity lookup does NOT match the expected result
437
				if !isSameArray(res.GetEntityIds(), expected) {
438
					expectedStr := strings.Join(expected, ", ")
439
					actualStr := strings.Join(res.GetEntityIds(), ", ")
440
441
					errorMsg := fmt.Sprintf("Query: %s, Expected: [%s], Actual: [%s]", query, expectedStr, actualStr)
442
443
					errors = append(errors, Error{
444
						Type:    "scenarios",
445
						Key:     i,
446
						Message: errorMsg,
447
					})
448
				}
449
			}
450
		}
451
452
		// Each SubjectFilter in the current scenario is processed
453
		for _, filter := range scenario.SubjectFilters {
454
			subjectReference := tuple.RelationReference(filter.SubjectReference)
455
456
			cont, err := Context(filter.Context)
457
			if err != nil {
458
				errors = append(errors, Error{
459
					Type:    "scenarios",
460
					Key:     i,
461
					Message: err.Error(),
462
				})
463
				continue
464
			}
465
466
			var entity *v1.Entity
467
			entity, err = tuple.E(filter.Entity)
468
			if err != nil {
469
				errors = append(errors, Error{
470
					Type:    "scenarios",
471
					Key:     i,
472
					Message: err.Error(),
473
				})
474
				continue
475
			}
476
477
			// Each Assertion in the current filter is processed
478
			for permission, expected := range filter.Assertions {
479
				// Perform a lookup for the subject with the given entity and permission
480
				res, err := c.Container.Invoker.LookupSubject(ctx, &v1.PermissionLookupSubjectRequest{
481
					TenantId: "t1",
482
					Metadata: &v1.PermissionLookupSubjectRequestMetadata{
483
						SchemaVersion: version,
484
						SnapToken:     token.NewNoopToken().Encode().String(),
485
						Depth:         100,
486
					},
487
					Context:          cont,
488
					SubjectReference: subjectReference,
489
					Permission:       permission,
490
					Entity:           entity,
491
				})
492
				if err != nil {
493
					errors = append(errors, Error{
494
						Type:    "scenarios",
495
						Key:     i,
496
						Message: err.Error(),
497
					})
498
					continue
499
				}
500
501
				query := tuple.EntityToString(entity) + " " + permission + " " + filter.SubjectReference
502
503
				// Check if the actual result of the subject lookup does NOT match the expected result
504
				if !isSameArray(res.GetSubjectIds(), expected) {
505
					expectedStr := strings.Join(expected, ", ")
506
					actualStr := strings.Join(res.GetSubjectIds(), ", ")
507
508
					errorMsg := fmt.Sprintf("Query: %s, Expected: [%s], Actual: [%s]", query, expectedStr, actualStr)
509
510
					errors = append(errors, Error{
511
						Type:    "scenarios",
512
						Key:     i,
513
						Message: errorMsg,
514
					})
515
				}
516
			}
517
		}
518
	}
519
520
	return errors
521
}
522
523
// Context is a function that takes a file context and returns a base context and an error.
524
func Context(fileContext file.Context) (cont *v1.Context, err error) {
525
	// Initialize an empty base context to be populated from the file context.
526
	cont = &v1.Context{
527
		Tuples:     []*v1.Tuple{},
528
		Attributes: []*v1.Attribute{},
529
		Data:       nil,
530
	}
531
532
	// Convert the file context's data to a Struct object.
533
	st, err := structpb.NewStruct(fileContext.Data)
534
	if err != nil {
535
		// If an error occurs, return it.
536
		return nil, err
537
	}
538
539
	// Assign the Struct object to the context's data field.
540
	cont.Data = st
541
542
	// Iterate over the file context's tuples.
543
	for _, t := range fileContext.Tuples {
544
		// Convert each tuple to a base tuple.
545
		tup, err := tuple.Tuple(t)
546
		if err != nil {
547
			// If an error occurs, return it.
548
			return nil, err
549
		}
550
551
		// Add the converted tuple to the context's tuples slice.
552
		cont.Tuples = append(cont.Tuples, tup)
553
	}
554
555
	// Iterate over the file context's attributes.
556
	for _, t := range fileContext.Attributes {
557
		// Convert each attribute to a base attribute.
558
		attr, err := attribute.Attribute(t)
559
		if err != nil {
560
			// If an error occurs, return it.
561
			return nil, err
562
		}
563
564
		// Add the converted attribute to the context's attributes slice.
565
		cont.Attributes = append(cont.Attributes, attr)
566
	}
567
568
	// If everything goes well, return the context and a nil error.
569
	return cont, nil
570
}
571
572
// isSameArray - check if two arrays are the same
573
func isSameArray(a, b []string) bool {
574
	if len(a) != len(b) {
575
		return false
576
	}
577
578
	sortedA := make([]string, len(a))
579
	copy(sortedA, a)
580
	sort.Strings(sortedA)
581
582
	sortedB := make([]string, len(b))
583
	copy(sortedB, b)
584
	sort.Strings(sortedB)
585
586
	for i := range sortedA {
587
		if sortedA[i] != sortedB[i] {
588
			return false
589
		}
590
	}
591
592
	return true
593
}
594