cmd.validate   F
last analyzed

Complexity

Conditions 45

Size

Total Lines 396
Code Lines 208

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 45
eloc 208
nop 0
dl 0
loc 396
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"
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.Replace(strings.Replace(m, "ERROR_CODE_", "", -1), "_", " ", -1))))
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
102
		loader := schema.NewSchemaLoader()
103
		loaded, err := loader.LoadSchema(s.Schema)
104
		if err != nil {
105
			return err
106
		}
107
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
320
				// Convert the subject from the filter into a base.Subject.
321
				ear, err := tuple.EAR(filter.Subject)
322
				if err != nil {
323
					// If an error occurs, add it to the list and continue to the next filter.
324
					list.Add(err.Error())
325
					continue
326
				}
327
328
				// Create a new base.Subject from the Entity-Attribute-Relation (EAR).
329
				subject := &base.Subject{
330
					Type:     ear.GetEntity().GetType(),
331
					Id:       ear.GetEntity().GetId(),
332
					Relation: ear.GetRelation(),
333
				}
334
335
				// Convert the filter context into a base.Context.
336
				cont, err := Context(filter.Context)
337
				if err != nil {
338
					// If an error occurs, add it to the list and continue to the next filter.
339
					list.Add(err.Error())
340
					continue
341
				}
342
343
				// Iterate over each assertion in the filter.
344
				for permission, expected := range filter.Assertions {
345
					// Perform a permission lookup for the entity.
346
					res, err := dev.Container.Invoker.LookupEntity(ctx, &base.PermissionLookupEntityRequest{
347
						TenantId: "t1",
348
						Context:  cont,
349
						Metadata: &base.PermissionLookupEntityRequestMetadata{
350
							SchemaVersion: version,
351
							SnapToken:     token.NewNoopToken().Encode().String(),
352
							Depth:         100,
353
						},
354
						EntityType: filter.EntityType,
355
						Permission: permission,
356
						Subject:    subject,
357
					})
358
					if err != nil {
359
						// If an error occurs, add it to the list and continue to the next assertion.
360
						list.Add(err.Error())
361
						continue
362
					}
363
364
					// Format the subject, permission, and entity type as a string for logging.
365
					query := tuple.SubjectToString(subject) + " " + permission + " " + filter.EntityType
366
367
					// Check if the actual result matches the expected result.
368
					if isSameArray(res.GetEntityIds(), expected) {
369
						// If the results match, log a success message.
370
						color.Success.Print("    success:")
371
						fmt.Printf(" %v\n", query)
372
					} else {
373
						// If the results don't match, log a failure message with the expected and actual results.
374
						color.Danger.Printf("    fail: %s -> expected: %+v actual: %+v\n", query, expected, res.GetEntityIds())
375
						list.Add(fmt.Sprintf("%s -> expected: %+v actual: %+v", query, expected, res.GetEntityIds()))
376
					}
377
				}
378
			}
379
380
			// Print a message indicating the start of the subject filter processing.
381
			color.Notice.Println("  subject_filters:")
382
383
			// Iterate over each subject filter in the scenario.
384
			for _, filter := range scenario.SubjectFilters {
385
386
				// Convert the subject reference from the filter into a relation reference.
387
				subjectReference := tuple.RelationReference(filter.SubjectReference)
388
389
				// Convert the entity from the filter into a base.Entity.
390
				var entity *base.Entity
391
				entity, err = tuple.E(filter.Entity)
392
				if err != nil {
393
					// If an error occurs, add it to the list and continue to the next filter.
394
					list.Add(err.Error())
395
					continue
396
				}
397
398
				// Convert the filter context into a base.Context.
399
				cont, err := Context(filter.Context)
400
				if err != nil {
401
					// If an error occurs, add it to the list and continue to the next filter.
402
					list.Add(err.Error())
403
					continue
404
				}
405
406
				// Iterate over each assertion in the filter.
407
				for permission, expected := range filter.Assertions {
408
					// Perform a permission lookup for the subject.
409
					res, err := dev.Container.Invoker.LookupSubject(ctx, &base.PermissionLookupSubjectRequest{
410
						TenantId: "t1",
411
						Context:  cont,
412
						Metadata: &base.PermissionLookupSubjectRequestMetadata{
413
							SchemaVersion: version,
414
							SnapToken:     token.NewNoopToken().Encode().String(),
415
							Depth:         100,
416
						},
417
						SubjectReference: subjectReference,
418
						Permission:       permission,
419
						Entity:           entity,
420
					})
421
					if err != nil {
422
						// If an error occurs, add it to the list and continue to the next assertion.
423
						list.Add(err.Error())
424
						continue
425
					}
426
427
					// Format the entity, permission, and subject reference as a string for logging.
428
					query := tuple.EntityToString(entity) + " " + permission + " " + filter.SubjectReference
429
430
					// Check if the actual result matches the expected result.
431
					if isSameArray(res.GetSubjectIds(), expected) {
432
						// If the results match, log a success message.
433
						color.Success.Print("    success:")
434
						fmt.Printf(" %v\n", query)
435
					} else {
436
						// If the results don't match, log a failure message with the expected and actual results.
437
						color.Danger.Printf("    fail: %s -> expected: %+v actual: %+v\n", query, expected, res.GetSubjectIds())
438
						list.Add(fmt.Sprintf("%s -> expected: %+v actual: %+v", query, expected, res.GetSubjectIds()))
439
					}
440
				}
441
			}
442
		}
443
444
		// If the error list is not empty, there were some errors during processing.
445
		if len(list.Errors) != 0 {
446
			// Print the errors collected during processing.
447
			list.Print()
448
			// Exit the program with a status of 1 to indicate an error.
449
			os.Exit(1)
450
		}
451
452
		// If there are no errors, print the success messages.
453
		color.Notice.Println("schema successfully created")
454
		color.Notice.Println("relationships successfully created")
455
		color.Notice.Println("assertions successfully passed")
456
457
		// Final success message to indicate everything completed successfully.
458
		color.Success.Println("SUCCESS")
459
460
		return nil
461
	}
462
}
463
464
// validationError - validation error
465
func validationError(message string) string {
466
	return strings.ToLower(strings.Replace(strings.Replace(message, "ERROR_CODE_", "", -1), "_", " ", -1))
467
}
468
469
// isSameArray - check if two arrays are the same
470
func isSameArray(a, b []string) bool {
471
	if len(a) != len(b) {
472
		return false
473
	}
474
475
	sortedA := make([]string, len(a))
476
	copy(sortedA, a)
477
	sort.Strings(sortedA)
478
479
	sortedB := make([]string, len(b))
480
	copy(sortedB, b)
481
	sort.Strings(sortedB)
482
483
	for i := range sortedA {
484
		if sortedA[i] != sortedB[i] {
485
			return false
486
		}
487
	}
488
489
	return true
490
}
491
492
// Context is a function that takes a file context and returns a base context and an error.
493
func Context(fileContext file.Context) (cont *base.Context, err error) {
494
	// Initialize an empty base context to be populated from the file context.
495
	cont = &base.Context{
496
		Tuples:     []*base.Tuple{},
497
		Attributes: []*base.Attribute{},
498
		Data:       nil,
499
	}
500
501
	// Convert the file context's data to a Struct object.
502
	st, err := structpb.NewStruct(fileContext.Data)
503
	if err != nil {
504
		// If an error occurs, return it.
505
		return nil, err
506
	}
507
508
	// Assign the Struct object to the context's data field.
509
	cont.Data = st
510
511
	// Iterate over the file context's tuples.
512
	for _, t := range fileContext.Tuples {
513
		// Convert each tuple to a base tuple.
514
		tup, err := tuple.Tuple(t)
515
		if err != nil {
516
			// If an error occurs, return it.
517
			return nil, err
518
		}
519
520
		// Add the converted tuple to the context's tuples slice.
521
		cont.Tuples = append(cont.Tuples, tup)
522
	}
523
524
	// Iterate over the file context's attributes.
525
	for _, t := range fileContext.Attributes {
526
		// Convert each attribute to a base attribute.
527
		attr, err := attribute.Attribute(t)
528
		if err != nil {
529
			// If an error occurs, return it.
530
			return nil, err
531
		}
532
533
		// Add the converted attribute to the context's attributes slice.
534
		cont.Attributes = append(cont.Attributes, attr)
535
	}
536
537
	// If everything goes well, return the context and a nil error.
538
	return cont, nil
539
}
540