Passed
Pull Request — master (#1694)
by Tolga
03:52
created

engines.mapToString   A

Complexity

Conditions 3

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nop 1
dl 0
loc 13
rs 9.9
c 0
b 0
f 0
1
package engines
2
3
import (
4
	"errors"
5
	"fmt"
6
	"sort"
7
	"strings"
8
	"sync"
9
10
	"google.golang.org/protobuf/types/known/anypb"
11
12
	"github.com/Permify/permify/internal/schema"
13
	"github.com/Permify/permify/pkg/attribute"
14
	base "github.com/Permify/permify/pkg/pb/base/v1"
15
	"github.com/Permify/permify/pkg/tuple"
16
)
17
18
const (
19
	_defaultConcurrencyLimit = 100
20
)
21
22
// CheckOption - a functional option type for configuring the CheckEngine.
23
type CheckOption func(engine *CheckEngine)
24
25
// CheckConcurrencyLimit - a functional option that sets the concurrency limit for the CheckEngine.
26
func CheckConcurrencyLimit(limit int) CheckOption {
27
	return func(c *CheckEngine) {
28
		c.concurrencyLimit = limit
29
	}
30
}
31
32
type LookupOption func(engine *LookupEngine)
33
34
func LookupConcurrencyLimit(limit int) LookupOption {
35
	return func(c *LookupEngine) {
36
		c.concurrencyLimit = limit
37
	}
38
}
39
40
// SchemaBaseSubjectFilterOption - a functional option type for configuring the LookupSubjectEngine.
41
type SchemaBaseSubjectFilterOption func(engine *SchemaBasedSubjectFilter)
42
43
// SchemaBaseSubjectFilterConcurrencyLimit - a functional option that sets the concurrency limit for the LookupSubjectEngine.
44
func SchemaBaseSubjectFilterConcurrencyLimit(limit int) SchemaBaseSubjectFilterOption {
45
	return func(c *SchemaBasedSubjectFilter) {
46
		c.concurrencyLimit = limit
47
	}
48
}
49
50
// SubjectPermissionOption - a functional option type for configuring the SubjectPermissionEngine.
51
type SubjectPermissionOption func(engine *SubjectPermissionEngine)
52
53
// SubjectPermissionConcurrencyLimit - a functional option that sets the concurrency limit for the SubjectPermissionEngine.
54
func SubjectPermissionConcurrencyLimit(limit int) SubjectPermissionOption {
55
	return func(c *SubjectPermissionEngine) {
56
		c.concurrencyLimit = limit
57
	}
58
}
59
60
// joinResponseMetas - a helper function that merges multiple PermissionCheckResponseMetadata structs into one.
61
func joinResponseMetas(meta ...*base.PermissionCheckResponseMetadata) *base.PermissionCheckResponseMetadata {
62
	response := &base.PermissionCheckResponseMetadata{}
63
	for _, m := range meta {
64
		response.CheckCount += m.CheckCount
65
	}
66
	return response
67
}
68
69
// SubjectPermissionResponse - a struct that holds a SubjectPermissionResponse and an error for a single subject permission check result.
70
type SubjectPermissionResponse struct {
71
	permission string
72
	result     base.CheckResult
73
	err        error
74
}
75
76
// CheckResponse - a struct that holds a PermissionCheckResponse and an error for a single check function.
77
type CheckResponse struct {
78
	resp *base.PermissionCheckResponse
79
	err  error
80
}
81
82
// VisitsMap - a thread-safe map of ENR records.
83
type VisitsMap struct {
84
	er        sync.Map
85
	ea        sync.Map
86
	published sync.Map
87
}
88
89
func (s *VisitsMap) AddER(entity *base.Entity, relation string) bool {
90
	key := tuple.EntityAndRelationToString(entity, relation)
91
	_, existed := s.er.LoadOrStore(key, struct{}{})
92
	return !existed
93
}
94
95
func (s *VisitsMap) AddEA(entityType, attribute string) bool {
96
	key := fmt.Sprintf("%s$%s", entityType, attribute)
97
	_, existed := s.er.LoadOrStore(key, struct{}{})
98
	return !existed
99
}
100
101
func (s *VisitsMap) AddPublished(entity *base.Entity) bool {
102
	key := tuple.EntityToString(entity)
103
	_, existed := s.published.LoadOrStore(key, struct{}{})
104
	return !existed
105
}
106
107
// SubjectFilterResponse -
108
type SubjectFilterResponse struct {
109
	resp []string
110
	err  error
111
}
112
113
// getDuplicates is a function that accepts a slice of strings and returns a slice of duplicated strings in the input slice
114
func getDuplicates(s []string) []string {
115
	// "seen" will keep track of all strings we have encountered in the slice
116
	seen := make(map[string]bool)
117
118
	// "duplicates" will keep track of all strings that are duplicated in the slice
119
	duplicates := make(map[string]bool)
120
121
	// Iterate over every string in the input slice
122
	for _, str := range s {
123
		// If we have seen the string before, then it is a duplicate.
124
		// So, we add it to our duplicates map.
125
		if _, value := seen[str]; value {
126
			duplicates[str] = true
127
		} else {
128
			// If we haven't seen the string before, add it to the seen map.
129
			seen[str] = true
130
		}
131
	}
132
133
	// "duplicatesSlice" will eventually hold our results: all strings that are duplicated in the input slice
134
	duplicatesSlice := make([]string, 0, len(duplicates))
135
136
	// Now, duplicates map contains all the duplicated strings.
137
	// We iterate over it and add each string to our result slice.
138
	for str := range duplicates {
139
		duplicatesSlice = append(duplicatesSlice, str)
140
	}
141
142
	// Return the slice that contains all the duplicated strings
143
	return duplicatesSlice
144
}
145
146
// getEmptyValueForType is a helper function that takes a string representation of a type
147
// and returns an "empty" value for that type.
148
// An empty value is a value that is generally considered a default or initial state for a variable of a given type.
149
// The purpose of this function is to be able to initialize a variable of a given type without knowing the type in advance.
150
// The function uses a switch statement to handle different possible type values and returns a corresponding empty value.
151
func getEmptyValueForType(typ base.AttributeType) interface{} {
152
	switch typ {
153
	case base.AttributeType_ATTRIBUTE_TYPE_STRING:
154
		// In the case of a string type, an empty string "" is considered the empty value.
155
		return ""
156
	case base.AttributeType_ATTRIBUTE_TYPE_STRING_ARRAY:
157
		// In the case of a string type, an empty string "" is considered the empty value.
158
		return []string{}
159
	case base.AttributeType_ATTRIBUTE_TYPE_INTEGER:
160
		// In the case of an integer type, zero (0) is considered the empty value.
161
		return 0
162
	case base.AttributeType_ATTRIBUTE_TYPE_INTEGER_ARRAY:
163
		// In the case of an integer type, zero (0) is considered the empty value.
164
		return []int32{}
165
	case base.AttributeType_ATTRIBUTE_TYPE_DOUBLE:
166
		// In the case of a double (or floating point) type, zero (0.0) is considered the empty value.
167
		return 0.0
168
	case base.AttributeType_ATTRIBUTE_TYPE_DOUBLE_ARRAY:
169
		// In the case of a double (or floating point) type, zero (0.0) is considered the empty value.
170
		return []float64{}
171
	case base.AttributeType_ATTRIBUTE_TYPE_BOOLEAN:
172
		// In the case of a boolean type, false is considered the empty value.
173
		return false
174
	case base.AttributeType_ATTRIBUTE_TYPE_BOOLEAN_ARRAY:
175
		// In the case of a boolean type, false is considered the empty value.
176
		return []bool{}
177
	default:
178
		// For any other types that are not explicitly handled, the function returns nil.
179
		// This may need to be adjusted if there are other types that need specific empty values.
180
		return nil
181
	}
182
}
183
184
// getEmptyProtoValueForType returns an empty protobuf value of the specified type.
185
// It takes a base.AttributeType as input and generates an empty protobuf Any message
186
// containing a default value for that type.
187
func getEmptyProtoValueForType(typ base.AttributeType) (*anypb.Any, error) {
188
	switch typ {
189
	case base.AttributeType_ATTRIBUTE_TYPE_STRING:
190
		// Create an empty protobuf String message
191
		value, err := anypb.New(&base.StringValue{Data: ""})
192
		if err != nil {
193
			return nil, err
194
		}
195
		return value, nil
196
197
	case base.AttributeType_ATTRIBUTE_TYPE_STRING_ARRAY:
198
		// Create an empty protobuf StringArray message
199
		value, err := anypb.New(&base.StringArrayValue{Data: []string{}})
200
		if err != nil {
201
			return nil, err
202
		}
203
		return value, nil
204
205
	case base.AttributeType_ATTRIBUTE_TYPE_INTEGER:
206
		// Create an empty protobuf Integer message
207
		value, err := anypb.New(&base.IntegerValue{Data: 0})
208
		if err != nil {
209
			return nil, err
210
		}
211
		return value, nil
212
213
	case base.AttributeType_ATTRIBUTE_TYPE_INTEGER_ARRAY:
214
		// Create an empty protobuf IntegerArray message
215
		value, err := anypb.New(&base.IntegerArrayValue{Data: []int32{}})
216
		if err != nil {
217
			return nil, err
218
		}
219
		return value, nil
220
221
	case base.AttributeType_ATTRIBUTE_TYPE_DOUBLE:
222
		// Create an empty protobuf Double message
223
		value, err := anypb.New(&base.DoubleValue{Data: 0.0})
224
		if err != nil {
225
			return nil, err
226
		}
227
		return value, nil
228
229
	case base.AttributeType_ATTRIBUTE_TYPE_DOUBLE_ARRAY:
230
		// Create an empty protobuf DoubleArray message
231
		value, err := anypb.New(&base.DoubleArrayValue{Data: []float64{}})
232
		if err != nil {
233
			return nil, err
234
		}
235
		return value, nil
236
237
	case base.AttributeType_ATTRIBUTE_TYPE_BOOLEAN:
238
		// Create an empty protobuf Boolean message with a default value of false
239
		value, err := anypb.New(&base.BooleanValue{Data: false})
240
		if err != nil {
241
			return nil, err
242
		}
243
		return value, nil
244
245
	case base.AttributeType_ATTRIBUTE_TYPE_BOOLEAN_ARRAY:
246
		// Create an empty protobuf BooleanArray message
247
		value, err := anypb.New(&base.BooleanArrayValue{Data: []bool{}})
248
		if err != nil {
249
			return nil, err
250
		}
251
		return value, nil
252
253
	default:
254
		// Handle the case where the provided attribute type is unknown
255
		return nil, errors.New("unknown type")
256
	}
257
}
258
259
// ConvertToAnyPB is a function to convert various basic Go types into *anypb.Any.
260
// It supports conversion from bool, int, float64, and string.
261
// It uses a type switch to detect the type of the input value.
262
// If the type is unsupported or unknown, it returns an error.
263
func ConvertToAnyPB(value interface{}) (*anypb.Any, error) {
264
	// anyValue will store the converted value, err will store any error occurred during conversion.
265
	var anyValue *anypb.Any
266
	var err error
267
268
	// Use a type switch to handle different types of value.
269
	switch v := value.(type) {
270
	case bool:
271
		anyValue, err = anypb.New(&base.BooleanValue{Data: v})
272
	case []bool:
273
		anyValue, err = anypb.New(&base.BooleanArrayValue{Data: v})
274
	case int:
275
		anyValue, err = anypb.New(&base.IntegerValue{Data: int32(v)})
276
	case []int32:
277
		anyValue, err = anypb.New(&base.IntegerArrayValue{Data: v})
278
	case float64:
279
		anyValue, err = anypb.New(&base.DoubleValue{Data: v})
280
	case []float64:
281
		anyValue, err = anypb.New(&base.DoubleArrayValue{Data: v})
282
	case string:
283
		anyValue, err = anypb.New(&base.StringValue{Data: v})
284
	case []string:
285
		anyValue, err = anypb.New(&base.StringArrayValue{Data: v})
286
	default:
287
		// In case of an unsupported or unknown type, we return an error.
288
		return nil, errors.New("unknown type")
289
	}
290
291
	// If there was an error during the conversion, return the error.
292
	if err != nil {
293
		return nil, err
294
	}
295
296
	// If the conversion was successful, return the converted value.
297
	return anyValue, nil
298
}
299
300
// GenerateKey function takes a PermissionCheckRequest and generates a unique key
301
// Key format: check|{tenant_id}|{schema_version}|{snap_token}|{context}|{entity:id#permission(optional_arguments)@subject:id#optional_relation}
302
func GenerateKey(key *base.PermissionCheckRequest, isRelational bool) string {
303
	// Initialize the parts slice with the string "check"
304
	parts := []string{"check"}
305
306
	// If tenantId is not empty, append it to parts
307
	if tenantId := key.GetTenantId(); tenantId != "" {
308
		parts = append(parts, tenantId)
309
	}
310
311
	// If Metadata exists, extract schema version and snap token and append them to parts if they are not empty
312
	if meta := key.GetMetadata(); meta != nil {
313
		if version := meta.GetSchemaVersion(); version != "" {
314
			parts = append(parts, version)
315
		}
316
		if token := meta.GetSnapToken(); token != "" {
317
			parts = append(parts, token)
318
		}
319
	}
320
321
	// If Context exists, convert it to string and append it to parts
322
	if ctx := key.GetContext(); ctx != nil {
323
		parts = append(parts, ContextToString(ctx))
324
	}
325
326
	if isRelational {
327
		// Convert entity and relation to string with any optional arguments and append to parts
328
		entityRelationString := tuple.EntityAndRelationToString(key.GetEntity(), key.GetPermission())
329
330
		subjectString := tuple.SubjectToString(key.GetSubject())
331
332
		if entityRelationString != "" {
333
			parts = append(parts, fmt.Sprintf("%s@%s", entityRelationString, subjectString))
334
		}
335
	} else {
336
		parts = append(parts, attribute.EntityAndCallOrAttributeToString(
337
			key.GetEntity(),
338
			key.GetPermission(),
339
			key.GetArguments()...,
340
		))
341
	}
342
343
	// Join all parts with "|" delimiter to generate the final key
344
	return strings.Join(parts, "|")
345
}
346
347
// ContextToString function takes a Context object and converts it into a string
348
func ContextToString(context *base.Context) string {
349
	// Initialize an empty slice to store parts of the context
350
	var parts []string
351
352
	// For each Tuple in the Context, convert it to a string and append to parts
353
	for _, tup := range context.GetTuples() {
354
		parts = append(parts, tuple.ToString(tup)) // replace with your function
355
	}
356
357
	// For each Attribute in the Context, convert it to a string and append to parts
358
	for _, attr := range context.GetAttributes() {
359
		parts = append(parts, attribute.ToString(attr)) // replace with your function
360
	}
361
362
	// If Data exists in the Context, convert it to JSON string and append to parts
363
	if data := context.GetData(); data != nil {
364
		parts = append(parts, mapToString(data.AsMap()))
365
	}
366
367
	// Join all parts with "," delimiter to generate the final context string
368
	return strings.Join(parts, ",")
369
}
370
371
// mapToString function takes a map[string]interface{} and converts it into a string
372
func mapToString(m map[string]interface{}) string {
373
	var keys []string
374
	for key := range m {
375
		keys = append(keys, key)
376
	}
377
	sort.Strings(keys)
378
379
	var parts []string
380
	for _, key := range keys {
381
		value := m[key]
382
		parts = append(parts, fmt.Sprintf("%s:%v", key, value))
383
	}
384
	return strings.Join(parts, ",")
385
}
386
387
// IsRelational determines if a given permission corresponds to a relational attribute
388
// in the provided entity definition.
389
func IsRelational(en *base.EntityDefinition, permission string) bool {
390
	// Default to non-relational
391
	isRelational := false
392
393
	// Attempt to get the type of reference for the given permission in the entity definition
394
	tor, err := schema.GetTypeOfReferenceByNameInEntityDefinition(en, permission)
395
	if err == nil && tor != base.EntityDefinition_REFERENCE_ATTRIBUTE {
396
		// If the type of reference is anything other than REFERENCE_ATTRIBUTE,
397
		// treat it as a relational attribute
398
		isRelational = true
399
	}
400
401
	return isRelational
402
}
403