Passed
Push — master ( 71e296...bfa930 )
by Tolga
06:19 queued 03:09
created

cmd.Depth   A

Complexity

Conditions 3

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nop 1
dl 0
loc 8
rs 10
c 0
b 0
f 0
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
					continue
246
				}
247
248
				// Extract entity-attribute-relation from the check's subject
249
				ear, err := tuple.EAR(check.Subject)
250
				if err != nil {
251
					list.Add(err.Error())
252
					continue
253
				}
254
255
				// Define the subject based on the extracted entity-attribute-relation
256
				subject := &base.Subject{
257
					Type:     ear.GetEntity().GetType(),
258
					Id:       ear.GetEntity().GetId(),
259
					Relation: ear.GetRelation(),
260
				}
261
262
				cont, err := Context(check.Context)
263
				if err != nil {
264
					list.Add(err.Error())
265
					continue
266
				}
267
268
				// Iterate over all assertions in the check
269
				for permission, expected := range check.Assertions {
270
					// Set expected result based on the assertion
271
					exp := base.CheckResult_CHECK_RESULT_ALLOWED
272
					if !expected {
273
						exp = base.CheckResult_CHECK_RESULT_DENIED
274
					}
275
276
					depth, err := Depth(check.Depth)
277
					if err != nil {
278
						list.Add(err.Error())
279
						continue
280
					}
281
282
					// Perform a permission check based on the context, entity, permission, and subject
283
					res, err := dev.Container.Invoker.Check(ctx, &base.PermissionCheckRequest{
284
						TenantId: "t1",
285
						Context:  cont,
286
						Metadata: &base.PermissionCheckRequestMetadata{
287
							SchemaVersion: version,
288
							SnapToken:     token.NewNoopToken().Encode().String(),
289
							Depth:         depth,
290
						},
291
						Entity:     entity,
292
						Permission: permission,
293
						Subject:    subject,
294
					})
295
					if err != nil {
296
						list.Add(err.Error())
297
						continue
298
					}
299
300
					// Formulate the query string for log output
301
					query := tuple.SubjectToString(subject) + " " + permission + " " + tuple.EntityToString(entity)
302
303
					// If the check result matches the expected result, log a success message
304
					if res.Can == exp {
305
						color.Success.Print("    success:")
306
						fmt.Printf(" %s \n", query)
307
					} else {
308
						// If the check result does not match the expected result, log a failure message
309
						color.Danger.Printf("    fail: %s ->", query)
310
						if res.Can == base.CheckResult_CHECK_RESULT_ALLOWED {
311
							color.Danger.Println("  expected: DENIED actual: ALLOWED ")
312
							list.Add(fmt.Sprintf("%s -> expected: DENIED actual: ALLOWED ", query))
313
						} else {
314
							color.Danger.Println("  expected: ALLOWED actual: DENIED ")
315
							list.Add(fmt.Sprintf("%s -> expected: ALLOWED actual: DENIED ", query))
316
						}
317
					}
318
				}
319
			}
320
321
			// Start of the entity filter processing.
322
			color.Notice.Println("  entity_filters:")
323
324
			// Iterate over each entity filter in the scenario.
325
			for _, filter := range scenario.EntityFilters {
326
				// Convert the subject from the filter into a base.Subject.
327
				ear, err := tuple.EAR(filter.Subject)
328
				if err != nil {
329
					// If an error occurs, add it to the list and continue to the next filter.
330
					list.Add(err.Error())
331
					continue
332
				}
333
334
				// Create a new base.Subject from the Entity-Attribute-Relation (EAR).
335
				subject := &base.Subject{
336
					Type:     ear.GetEntity().GetType(),
337
					Id:       ear.GetEntity().GetId(),
338
					Relation: ear.GetRelation(),
339
				}
340
341
				// Convert the filter context into a base.Context.
342
				cont, err := Context(filter.Context)
343
				if err != nil {
344
					// If an error occurs, add it to the list and continue to the next filter.
345
					list.Add(err.Error())
346
					continue
347
				}
348
349
				depth, err := Depth(filter.Depth)
350
				if err != nil {
351
					list.Add(err.Error())
352
					continue
353
				}
354
355
				// Iterate over each assertion in the filter.
356
				for permission, expected := range filter.Assertions {
357
					// Perform a permission lookup for the entity.
358
					res, err := dev.Container.Invoker.LookupEntity(ctx, &base.PermissionLookupEntityRequest{
359
						TenantId: "t1",
360
						Context:  cont,
361
						Metadata: &base.PermissionLookupEntityRequestMetadata{
362
							SchemaVersion: version,
363
							SnapToken:     token.NewNoopToken().Encode().String(),
364
							Depth:         depth,
365
						},
366
						EntityType: filter.EntityType,
367
						Permission: permission,
368
						Subject:    subject,
369
					})
370
					if err != nil {
371
						// If an error occurs, add it to the list and continue to the next assertion.
372
						list.Add(err.Error())
373
						continue
374
					}
375
376
					// Format the subject, permission, and entity type as a string for logging.
377
					query := tuple.SubjectToString(subject) + " " + permission + " " + filter.EntityType
378
379
					// Check if the actual result matches the expected result.
380
					if isSameArray(res.GetEntityIds(), expected) {
381
						// If the results match, log a success message.
382
						color.Success.Print("    success:")
383
						fmt.Printf(" %v\n", query)
384
					} else {
385
						// If the results don't match, log a failure message with the expected and actual results.
386
						color.Danger.Printf("    fail: %s -> expected: %+v actual: %+v\n", query, expected, res.GetEntityIds())
387
						list.Add(fmt.Sprintf("%s -> expected: %+v actual: %+v", query, expected, res.GetEntityIds()))
388
					}
389
				}
390
			}
391
392
			// Print a message indicating the start of the subject filter processing.
393
			color.Notice.Println("  subject_filters:")
394
395
			// Iterate over each subject filter in the scenario.
396
			for _, filter := range scenario.SubjectFilters {
397
				// Convert the subject reference from the filter into a relation reference.
398
				subjectReference := tuple.RelationReference(filter.SubjectReference)
399
400
				// Convert the entity from the filter into a base.Entity.
401
				var entity *base.Entity
402
				entity, err = tuple.E(filter.Entity)
403
				if err != nil {
404
					// If an error occurs, add it to the list and continue to the next filter.
405
					list.Add(err.Error())
406
					continue
407
				}
408
409
				// Convert the filter context into a base.Context.
410
				cont, err := Context(filter.Context)
411
				if err != nil {
412
					// If an error occurs, add it to the list and continue to the next filter.
413
					list.Add(err.Error())
414
					continue
415
				}
416
417
				depth, err := Depth(filter.Depth)
418
				if err != nil {
419
					list.Add(err.Error())
420
					continue
421
				}
422
423
				// Iterate over each assertion in the filter.
424
				for permission, expected := range filter.Assertions {
425
					// Perform a permission lookup for the subject.
426
					res, err := dev.Container.Invoker.LookupSubject(ctx, &base.PermissionLookupSubjectRequest{
427
						TenantId: "t1",
428
						Context:  cont,
429
						Metadata: &base.PermissionLookupSubjectRequestMetadata{
430
							SchemaVersion: version,
431
							SnapToken:     token.NewNoopToken().Encode().String(),
432
							Depth:         depth,
433
						},
434
						SubjectReference: subjectReference,
435
						Permission:       permission,
436
						Entity:           entity,
437
					})
438
					if err != nil {
439
						// If an error occurs, add it to the list and continue to the next assertion.
440
						list.Add(err.Error())
441
						continue
442
					}
443
444
					// Format the entity, permission, and subject reference as a string for logging.
445
					query := tuple.EntityToString(entity) + " " + permission + " " + filter.SubjectReference
446
447
					// Check if the actual result matches the expected result.
448
					if isSameArray(res.GetSubjectIds(), expected) {
449
						// If the results match, log a success message.
450
						color.Success.Print("    success:")
451
						fmt.Printf(" %v\n", query)
452
					} else {
453
						// If the results don't match, log a failure message with the expected and actual results.
454
						color.Danger.Printf("    fail: %s -> expected: %+v actual: %+v\n", query, expected, res.GetSubjectIds())
455
						list.Add(fmt.Sprintf("%s -> expected: %+v actual: %+v", query, expected, res.GetSubjectIds()))
456
					}
457
				}
458
			}
459
		}
460
461
		// If the error list is not empty, there were some errors during processing.
462
		if len(list.Errors) != 0 {
463
			// Print the errors collected during processing.
464
			list.Print()
465
			// Exit the program with a status of 1 to indicate an error.
466
			os.Exit(1)
467
		}
468
469
		// If there are no errors, print the success messages.
470
		color.Notice.Println("schema successfully created")
471
		color.Notice.Println("relationships successfully created")
472
		color.Notice.Println("assertions successfully passed")
473
474
		// Final success message to indicate everything completed successfully.
475
		color.Success.Println("SUCCESS")
476
477
		return nil
478
	}
479
}
480
481
// validationError - validation error
482
func validationError(message string) string {
483
	return strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(message, "ERROR_CODE_", ""), "_", " "))
484
}
485
486
// isSameArray - check if two arrays are the same
487
func isSameArray(a, b []string) bool {
488
	if len(a) != len(b) {
489
		return false
490
	}
491
492
	sortedA := make([]string, len(a))
493
	copy(sortedA, a)
494
	sort.Strings(sortedA)
495
496
	sortedB := make([]string, len(b))
497
	copy(sortedB, b)
498
	sort.Strings(sortedB)
499
500
	for i := range sortedA {
501
		if sortedA[i] != sortedB[i] {
502
			return false
503
		}
504
	}
505
506
	return true
507
}
508
509
// Depth - get depth
510
func Depth(depth int32) (int32, error) {
511
	if depth == 0 {
512
		return 100, nil
513
	}
514
	if depth < 3 {
515
		return 0, errors.New("depth must be greater than or equal to 3")
516
	}
517
	return depth, nil
518
}
519
520
// Context is a function that takes a file context and returns a base context and an error.
521
func Context(fileContext file.Context) (cont *base.Context, err error) {
522
	// Initialize an empty base context to be populated from the file context.
523
	cont = &base.Context{
524
		Tuples:     []*base.Tuple{},
525
		Attributes: []*base.Attribute{},
526
		Data:       nil,
527
	}
528
529
	// Convert the file context's data to a Struct object.
530
	st, err := structpb.NewStruct(fileContext.Data)
531
	if err != nil {
532
		// If an error occurs, return it.
533
		return nil, err
534
	}
535
536
	// Assign the Struct object to the context's data field.
537
	cont.Data = st
538
539
	// Iterate over the file context's tuples.
540
	for _, t := range fileContext.Tuples {
541
		// Convert each tuple to a base tuple.
542
		tup, err := tuple.Tuple(t)
543
		if err != nil {
544
			// If an error occurs, return it.
545
			return nil, err
546
		}
547
548
		// Add the converted tuple to the context's tuples slice.
549
		cont.Tuples = append(cont.Tuples, tup)
550
	}
551
552
	// Iterate over the file context's attributes.
553
	for _, t := range fileContext.Attributes {
554
		// Convert each attribute to a base attribute.
555
		attr, err := attribute.Attribute(t)
556
		if err != nil {
557
			// If an error occurs, return it.
558
			return nil, err
559
		}
560
561
		// Add the converted attribute to the context's attributes slice.
562
		cont.Attributes = append(cont.Attributes, attr)
563
	}
564
565
	// If everything goes well, return the context and a nil error.
566
	return cont, nil
567
}
568