cmd.validate   F
last analyzed

Complexity

Conditions 48

Size

Total Lines 425
Code Lines 233

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 48
eloc 233
nop 0
dl 0
loc 425
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 cmd.validate 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 cmd
2
3
import (
4
	"context"
5
	"errors"
6
	"fmt"
7
	"net/url"
8
	"os"
9
	"sort"
10
	"strings"
11
12
	"google.golang.org/protobuf/types/known/structpb"
13
14
	"github.com/gookit/color"
15
	"github.com/rs/xid"
16
	"github.com/spf13/cobra"
17
18
	"github.com/Permify/permify/internal/storage"
19
	serverValidation "github.com/Permify/permify/internal/validation"
20
	"github.com/Permify/permify/pkg/attribute"
21
	"github.com/Permify/permify/pkg/database"
22
	"github.com/Permify/permify/pkg/development"
23
	"github.com/Permify/permify/pkg/development/file"
24
	"github.com/Permify/permify/pkg/dsl/compiler"
25
	"github.com/Permify/permify/pkg/dsl/parser"
26
	base "github.com/Permify/permify/pkg/pb/base/v1"
27
	"github.com/Permify/permify/pkg/schema" // Schema loading package
28
	"github.com/Permify/permify/pkg/token"
29
	"github.com/Permify/permify/pkg/tuple"
30
)
31
32
// NewValidateCommand - creates a new validate command
33
func NewValidateCommand() *cobra.Command {
34
	command := &cobra.Command{
35
		Use:   "validate <file>",
36
		Short: "validate authorization model with assertions",
37
		RunE:  validate(),
38
		Args:  cobra.ExactArgs(1),
39
	}
40
41
	return command
42
}
43
44
// ErrList - error list
45
type ErrList struct {
46
	Errors []string
47
}
48
49
// Add - add error to error list
50
func (l *ErrList) Add(message string) {
51
	l.Errors = append(l.Errors, message)
52
}
53
54
// Print - print error list
55
func (l *ErrList) Print() {
56
	color.Danger.Println("fails:")
57
	for _, m := range l.Errors {
58
		// print error message with color danger
59
		color.Danger.Println(strings.ToLower("fail: " + validationError(strings.ReplaceAll(strings.ReplaceAll(m, "ERROR_CODE_", ""), "_", " "))))
60
	}
61
	// print FAILED with color danger
62
	color.Danger.Println("FAILED")
63
}
64
65
// validate returns a function that validates authorization model with assertions
66
func validate() func(cmd *cobra.Command, args []string) error {
67
	return func(cmd *cobra.Command, args []string) error {
68
		// create an empty error list
69
		list := &ErrList{
70
			Errors: []string{},
71
		}
72
73
		// create a new context
74
		ctx := context.Background()
75
76
		// create a new development container
77
		dev := development.NewContainer()
78
79
		// parse the url from the first argument
80
		u, err := url.Parse(args[0])
81
		if err != nil {
82
			return err
83
		}
84
85
		// create a new decoder from the url
86
		decoder, err := file.NewDecoderFromURL(u)
87
		if err != nil {
88
			return err
89
		}
90
91
		// create a new shape
92
		s := &file.Shape{}
93
94
		// decode the schema from the decoder
95
		err = decoder.Decode(s)
96
		if err != nil {
97
			return err
98
		}
99
100
		// if debug is true, print schema is creating with color blue
101
		color.Notice.Println("schema is creating... 🚀")
102
		// Load and parse schema
103
		loader := schema.NewSchemaLoader()         // Create schema loader
104
		loaded, err := loader.LoadSchema(s.Schema) // Load schema content
105
		if err != nil {                            // Check for loading errors
106
			return err // Return error if loading fails
107
		} // Schema loaded successfully
108
		// Parse loaded schema
109
		sch, err := parser.NewParser(loaded).Parse()
110
		if err != nil {
111
			return err
112
		}
113
114
		_, _, err = compiler.NewCompiler(true, sch).Compile()
115
		if err != nil {
116
			return err
117
		}
118
119
		version := xid.New().String()
120
121
		cnf := make([]storage.SchemaDefinition, 0, len(sch.Statements))
122
		for _, st := range sch.Statements {
123
			cnf = append(cnf, storage.SchemaDefinition{
124
				TenantID:             "t1",
125
				Version:              version,
126
				Name:                 st.GetName(),
127
				SerializedDefinition: []byte(st.String()),
128
			})
129
		}
130
131
		// write the schema
132
		err = dev.Container.SW.WriteSchema(ctx, cnf)
133
		if err != nil {
134
			list.Add(err.Error())
135
			color.Danger.Printf("fail: %s\n", validationError(err.Error()))
136
			if len(list.Errors) != 0 {
137
				list.Print()
138
				os.Exit(1)
139
			}
140
		}
141
142
		// if there are no errors and debug is true, print success with color success
143
		if len(list.Errors) == 0 {
144
			color.Success.Println("  success")
145
		}
146
147
		// if debug is true, print relationships are creating with color blue
148
		color.Notice.Println("relationships are creating... 🚀")
149
150
		// Iterate over all relationships in the subject
151
		for _, t := range s.Relationships {
152
			// Convert each relationship to a Tuple
153
			var tup *base.Tuple
154
			tup, err = tuple.Tuple(t)
155
			// If an error occurs during the conversion, add the error message to the list and continue to the next iteration
156
			if err != nil {
157
				list.Add(err.Error())
158
				continue
159
			}
160
161
			// Retrieve the entity definition associated with the tuple's entity type
162
			definition, _, err := dev.Container.SR.ReadEntityDefinition(ctx, "t1", tup.GetEntity().GetType(), version)
163
			// If an error occurs while reading the entity definition, return the error
164
			if err != nil {
165
				return err
166
			}
167
168
			// Validate the tuple using the entity definition
169
			err = serverValidation.ValidateTuple(definition, tup)
170
			// If an error occurs during validation, return the error
171
			if err != nil {
172
				return err
173
			}
174
175
			// Write the validated tuple to the database
176
			_, err = dev.Container.DW.Write(ctx, "t1", database.NewTupleCollection(tup), database.NewAttributeCollection())
177
			// If an error occurs while writing to the database, add an error message to the list, log the error and continue to the next iteration
178
			if err != nil {
179
				list.Add(fmt.Sprintf("%s failed %s", t, err.Error()))
180
				color.Danger.Println(fmt.Sprintf("fail: %s failed %s", t, validationError(err.Error())))
181
				continue
182
			}
183
184
			// If the tuple was successfully written to the database, log a success message
185
			color.Success.Println(fmt.Sprintf("  success: %s ", t))
186
		}
187
188
		// if debug is true, print attributes are creating with color blue
189
		color.Notice.Println("attributes are creating... 🚀")
190
191
		// Iterate over all attributes in the subject
192
		for _, a := range s.Attributes {
193
			// Convert each attribute to an Attribute
194
			var attr *base.Attribute
195
			attr, err = attribute.Attribute(a)
196
			// If an error occurs during the conversion, add the error message to the list and continue to the next iteration
197
			if err != nil {
198
				list.Add(err.Error())
199
				continue
200
			}
201
202
			// Retrieve the entity definition associated with the attribute's entity type
203
			definition, _, err := dev.Container.SR.ReadEntityDefinition(ctx, "t1", attr.GetEntity().GetType(), version)
204
			// If an error occurs while reading the entity definition, return the error
205
			if err != nil {
206
				return err
207
			}
208
209
			// Validate the attribute using the entity definition
210
			err = serverValidation.ValidateAttribute(definition, attr)
211
			// If an error occurs during validation, return the error
212
			if err != nil {
213
				return err
214
			}
215
216
			// Write the validated attribute to the database
217
			_, err = dev.Container.DW.Write(ctx, "t1", database.NewTupleCollection(), database.NewAttributeCollection(attr))
218
			// If an error occurs while writing to the database, add an error message to the list, log the error and continue to the next iteration
219
			if err != nil {
220
				list.Add(fmt.Sprintf("%s failed %s", a, err.Error()))
221
				color.Danger.Println(fmt.Sprintf("fail: %s failed %s", a, validationError(err.Error())))
222
				continue
223
			}
224
225
			// If the attribute was successfully written to the database, log a success message
226
			color.Success.Println(fmt.Sprintf("  success: %s ", a))
227
		}
228
229
		// if debug is true, print checking assertions with color blue
230
		color.Notice.Println("checking scenarios... 🚀")
231
232
		// Check Assertions
233
		for sn, scenario := range s.Scenarios {
234
			color.Notice.Printf("%v.scenario: %s - %s\n", sn+1, scenario.Name, scenario.Description)
235
236
			// Start log output for checks
237
			color.Notice.Println("  checks:")
238
239
			// Iterate over all checks in the scenario
240
			for _, check := range scenario.Checks {
241
				// Extract entity from the check
242
				entity, err := tuple.E(check.Entity)
243
				if err != nil {
244
					list.Add(err.Error())
245
					color.Danger.Printf("    fail: %s\n", validationError(err.Error()))
246
					continue
247
				}
248
249
				// Extract entity-attribute-relation from the check's subject
250
				ear, err := tuple.EAR(check.Subject)
251
				if err != nil {
252
					list.Add(err.Error())
253
					color.Danger.Printf("    fail: %s\n", validationError(err.Error()))
254
					continue
255
				}
256
257
				// Define the subject based on the extracted entity-attribute-relation
258
				subject := &base.Subject{
259
					Type:     ear.GetEntity().GetType(),
260
					Id:       ear.GetEntity().GetId(),
261
					Relation: ear.GetRelation(),
262
				}
263
264
				cont, err := Context(check.Context)
265
				if err != nil {
266
					list.Add(err.Error())
267
					color.Danger.Printf("    fail: %s\n", validationError(err.Error()))
268
					continue
269
				}
270
271
				// Iterate over all assertions in the check
272
				for permission, expected := range check.Assertions {
273
					// Set expected result based on the assertion
274
					exp := base.CheckResult_CHECK_RESULT_ALLOWED
275
					if !expected {
276
						exp = base.CheckResult_CHECK_RESULT_DENIED
277
					}
278
279
					// Formulate the query string for log output (build early so we can use it in error messages)
280
					query := tuple.SubjectToString(subject) + " " + permission + " " + tuple.EntityToString(entity)
281
282
					depth, err := Depth(check.Depth)
283
					if err != nil {
284
						list.Add(fmt.Sprintf("%s -> %s", query, err.Error()))
285
						color.Danger.Printf("    fail: %s -> %s\n", query, validationError(err.Error()))
286
						continue
287
					}
288
289
					// Perform a permission check based on the context, entity, permission, and subject
290
					res, err := dev.Container.Invoker.Check(ctx, &base.PermissionCheckRequest{
291
						TenantId: "t1",
292
						Context:  cont,
293
						Metadata: &base.PermissionCheckRequestMetadata{
294
							SchemaVersion: version,
295
							SnapToken:     token.NewNoopToken().Encode().String(),
296
							Depth:         depth,
297
						},
298
						Entity:     entity,
299
						Permission: permission,
300
						Subject:    subject,
301
					})
302
					if err != nil {
303
						list.Add(fmt.Sprintf("%s -> %s", query, err.Error()))
304
						color.Danger.Printf("    fail: %s -> %s\n", query, validationError(err.Error()))
305
						continue
306
					}
307
308
					// If the check result matches the expected result, log a success message
309
					if res.Can == exp {
310
						color.Success.Print("    success:")
311
						fmt.Printf(" %s \n", query)
312
					} else {
313
						// If the check result does not match the expected result, log a failure message
314
						color.Danger.Printf("    fail: %s ->", query)
315
						if res.Can == base.CheckResult_CHECK_RESULT_ALLOWED {
316
							color.Danger.Println("  expected: DENIED actual: ALLOWED ")
317
							list.Add(fmt.Sprintf("%s -> expected: DENIED actual: ALLOWED ", query))
318
						} else {
319
							color.Danger.Println("  expected: ALLOWED actual: DENIED ")
320
							list.Add(fmt.Sprintf("%s -> expected: ALLOWED actual: DENIED ", query))
321
						}
322
					}
323
				}
324
			}
325
326
			// Start of the entity filter processing.
327
			color.Notice.Println("  entity_filters:")
328
329
			// Iterate over each entity filter in the scenario.
330
			for _, filter := range scenario.EntityFilters {
331
				// Convert the subject from the filter into a base.Subject.
332
				ear, err := tuple.EAR(filter.Subject)
333
				if err != nil {
334
					// If an error occurs, add it to the list and continue to the next filter.
335
					list.Add(err.Error())
336
					color.Danger.Printf("    fail: %s\n", validationError(err.Error()))
337
					continue
338
				}
339
340
				// Create a new base.Subject from the Entity-Attribute-Relation (EAR).
341
				subject := &base.Subject{
342
					Type:     ear.GetEntity().GetType(),
343
					Id:       ear.GetEntity().GetId(),
344
					Relation: ear.GetRelation(),
345
				}
346
347
				// Convert the filter context into a base.Context.
348
				cont, err := Context(filter.Context)
349
				if err != nil {
350
					// If an error occurs, add it to the list and continue to the next filter.
351
					list.Add(err.Error())
352
					color.Danger.Printf("    fail: %s\n", validationError(err.Error()))
353
					continue
354
				}
355
356
				// Iterate over each assertion in the filter.
357
				for permission, expected := range filter.Assertions {
358
					// Format the subject, permission, and entity type as a string for logging (build early for error messages)
359
					query := tuple.SubjectToString(subject) + " " + permission + " " + filter.EntityType
360
361
					depth, err := Depth(filter.Depth)
362
					if err != nil {
363
						list.Add(fmt.Sprintf("%s -> %s", query, err.Error()))
364
						color.Danger.Printf("    fail: %s -> %s\n", query, validationError(err.Error()))
365
						continue
366
					}
367
368
					// Perform a permission lookup for the entity.
369
					res, err := dev.Container.Invoker.LookupEntity(ctx, &base.PermissionLookupEntityRequest{
370
						TenantId: "t1",
371
						Context:  cont,
372
						Metadata: &base.PermissionLookupEntityRequestMetadata{
373
							SchemaVersion: version,
374
							SnapToken:     token.NewNoopToken().Encode().String(),
375
							Depth:         depth,
376
						},
377
						EntityType: filter.EntityType,
378
						Permission: permission,
379
						Subject:    subject,
380
					})
381
					if err != nil {
382
						// If an error occurs, add it to the list and continue to the next assertion.
383
						list.Add(fmt.Sprintf("%s -> %s", query, err.Error()))
384
						color.Danger.Printf("    fail: %s -> %s\n", query, validationError(err.Error()))
385
						continue
386
					}
387
388
					// Check if the actual result matches the expected result.
389
					if isSameArray(res.GetEntityIds(), expected) {
390
						// If the results match, log a success message.
391
						color.Success.Print("    success:")
392
						fmt.Printf(" %s\n", query)
393
					} else {
394
						// If the results don't match, log a failure message with the expected and actual results.
395
						color.Danger.Printf("    fail: %s -> expected: %+v actual: %+v\n", query, expected, res.GetEntityIds())
396
						list.Add(fmt.Sprintf("%s -> expected: %+v actual: %+v", query, expected, res.GetEntityIds()))
397
					}
398
				}
399
			}
400
401
			// Print a message indicating the start of the subject filter processing.
402
			color.Notice.Println("  subject_filters:")
403
404
			// Iterate over each subject filter in the scenario.
405
			for _, filter := range scenario.SubjectFilters {
406
				// Convert the subject reference from the filter into a relation reference.
407
				subjectReference := tuple.RelationReference(filter.SubjectReference)
408
409
				// Convert the entity from the filter into a base.Entity.
410
				var entity *base.Entity
411
				entity, err = tuple.E(filter.Entity)
412
				if err != nil {
413
					// If an error occurs, add it to the list and continue to the next filter.
414
					list.Add(err.Error())
415
					color.Danger.Printf("    fail: %s\n", validationError(err.Error()))
416
					continue
417
				}
418
419
				// Convert the filter context into a base.Context.
420
				cont, err := Context(filter.Context)
421
				if err != nil {
422
					// If an error occurs, add it to the list and continue to the next filter.
423
					list.Add(err.Error())
424
					color.Danger.Printf("    fail: %s\n", validationError(err.Error()))
425
					continue
426
				}
427
428
				// Iterate over each assertion in the filter.
429
				for permission, expected := range filter.Assertions {
430
					// Format the entity, permission, and subject reference as a string for logging (build early for error messages)
431
					query := tuple.EntityToString(entity) + " " + permission + " " + filter.SubjectReference
432
433
					depth, err := Depth(filter.Depth)
434
					if err != nil {
435
						list.Add(fmt.Sprintf("%s -> %s", query, err.Error()))
436
						color.Danger.Printf("    fail: %s -> %s\n", query, validationError(err.Error()))
437
						continue
438
					}
439
440
					// Perform a permission lookup for the subject.
441
					res, err := dev.Container.Invoker.LookupSubject(ctx, &base.PermissionLookupSubjectRequest{
442
						TenantId: "t1",
443
						Context:  cont,
444
						Metadata: &base.PermissionLookupSubjectRequestMetadata{
445
							SchemaVersion: version,
446
							SnapToken:     token.NewNoopToken().Encode().String(),
447
							Depth:         depth,
448
						},
449
						SubjectReference: subjectReference,
450
						Permission:       permission,
451
						Entity:           entity,
452
					})
453
					if err != nil {
454
						// If an error occurs, add it to the list and continue to the next assertion.
455
						list.Add(fmt.Sprintf("%s -> %s", query, err.Error()))
456
						color.Danger.Printf("    fail: %s -> %s\n", query, validationError(err.Error()))
457
						continue
458
					}
459
460
					// Check if the actual result matches the expected result.
461
					if isSameArray(res.GetSubjectIds(), expected) {
462
						// If the results match, log a success message.
463
						color.Success.Print("    success:")
464
						fmt.Printf(" %s\n", query)
465
					} else {
466
						// If the results don't match, log a failure message with the expected and actual results.
467
						color.Danger.Printf("    fail: %s -> expected: %+v actual: %+v\n", query, expected, res.GetSubjectIds())
468
						list.Add(fmt.Sprintf("%s -> expected: %+v actual: %+v", query, expected, res.GetSubjectIds()))
469
					}
470
				}
471
			}
472
		}
473
474
		// If the error list is not empty, there were some errors during processing.
475
		if len(list.Errors) != 0 {
476
			// Print the errors collected during processing.
477
			list.Print()
478
			// Exit the program with a status of 1 to indicate an error.
479
			os.Exit(1)
480
		}
481
482
		// If there are no errors, print the success messages.
483
		color.Notice.Println("schema successfully created")
484
		color.Notice.Println("relationships successfully created")
485
		color.Notice.Println("assertions successfully passed")
486
487
		// Final success message to indicate everything completed successfully.
488
		color.Success.Println("SUCCESS")
489
490
		return nil
491
	}
492
}
493
494
// validationError - validation error
495
func validationError(message string) string {
496
	return strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(message, "ERROR_CODE_", ""), "_", " "))
497
}
498
499
// isSameArray - check if two arrays are the same
500
func isSameArray(a, b []string) bool {
501
	if len(a) != len(b) {
502
		return false
503
	}
504
505
	sortedA := make([]string, len(a))
506
	copy(sortedA, a)
507
	sort.Strings(sortedA)
508
509
	sortedB := make([]string, len(b))
510
	copy(sortedB, b)
511
	sort.Strings(sortedB)
512
513
	for i := range sortedA {
514
		if sortedA[i] != sortedB[i] {
515
			return false
516
		}
517
	}
518
519
	return true
520
}
521
522
// Depth - get depth
523
func Depth(depth int32) (int32, error) {
524
	if depth == 0 {
525
		return 100, nil
526
	}
527
	if depth < 3 {
528
		return 0, errors.New("depth must be greater than or equal to 3")
529
	}
530
	return depth, nil
531
}
532
533
// Context is a function that takes a file context and returns a base context and an error.
534
func Context(fileContext file.Context) (cont *base.Context, err error) {
535
	// Initialize an empty base context to be populated from the file context.
536
	cont = &base.Context{
537
		Tuples:     []*base.Tuple{},
538
		Attributes: []*base.Attribute{},
539
		Data:       nil,
540
	}
541
542
	// Convert the file context's data to a Struct object.
543
	st, err := structpb.NewStruct(fileContext.Data)
544
	if err != nil {
545
		// If an error occurs, return it.
546
		return nil, err
547
	}
548
549
	// Assign the Struct object to the context's data field.
550
	cont.Data = st
551
552
	// Iterate over the file context's tuples.
553
	for _, t := range fileContext.Tuples {
554
		// Convert each tuple to a base tuple.
555
		tup, err := tuple.Tuple(t)
556
		if err != nil {
557
			// If an error occurs, return it.
558
			return nil, err
559
		}
560
561
		// Add the converted tuple to the context's tuples slice.
562
		cont.Tuples = append(cont.Tuples, tup)
563
	}
564
565
	// Iterate over the file context's attributes.
566
	for _, t := range fileContext.Attributes {
567
		// Convert each attribute to a base attribute.
568
		attr, err := attribute.Attribute(t)
569
		if err != nil {
570
			// If an error occurs, return it.
571
			return nil, err
572
		}
573
574
		// Add the converted attribute to the context's attributes slice.
575
		cont.Attributes = append(cont.Attributes, attr)
576
	}
577
578
	// If everything goes well, return the context and a nil error.
579
	return cont, nil
580
}
581