cmd.validate   F
last analyzed

Complexity

Conditions 45

Size

Total Lines 394
Code Lines 208

Duplication

Lines 0
Ratio 0 %

Importance

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