Passed
Pull Request — master (#2536)
by Tolga
02:59
created

internal/engines/utils.go   F

Size/Duplication

Total Lines 367
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 70
eloc 195
dl 0
loc 367
rs 2.8
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A engines.LookupConcurrencyLimit 0 3 2
A engines.joinResponseMetas 0 6 2
A engines.SubjectPermissionConcurrencyLimit 0 3 2
A engines.CheckConcurrencyLimit 0 3 2
A engines.*VisitsMap.AddER 0 4 1
A engines.SubjectFilterConcurrencyLimit 0 3 2
A engines.*VisitsMap.AddPublished 0 4 1
A engines.*VisitsMap.AddEA 0 4 1
F engines.getEmptyProtoValueForType 0 69 18
C engines.ConvertToAnyPB 0 35 11
A engines.ContextToString 0 21 4
C engines.getEmptyValueForType 0 30 10
A engines.IsRelational 0 13 3
A engines.mapToString 0 13 3
B engines.GenerateKey 0 43 8
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
// SubjectFilterOption - a functional option type for configuring the LookupSubjectEngine.
41
type SubjectFilterOption func(engine *SubjectFilter)
42
43
// SubjectFilterConcurrencyLimit - a functional option that sets the concurrency limit for the LookupSubjectEngine.
44
func SubjectFilterConcurrencyLimit(limit int) SubjectFilterOption {
45
	return func(c *SubjectFilter) {
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
	published sync.Map
86
}
87
88
func (s *VisitsMap) AddER(entity *base.Entity, relation string) bool {
89
	key := tuple.EntityAndRelationToString(entity, relation)
90
	_, existed := s.er.LoadOrStore(key, struct{}{})
91
	return !existed
92
}
93
94
func (s *VisitsMap) AddEA(entityType, attribute string) bool {
95
	key := fmt.Sprintf("%s$%s", entityType, attribute)
96
	_, existed := s.er.LoadOrStore(key, struct{}{})
97
	return !existed
98
}
99
100
func (s *VisitsMap) AddPublished(entity *base.Entity) bool {
101
	key := tuple.EntityToString(entity)
102
	_, existed := s.published.LoadOrStore(key, struct{}{})
103
	return !existed
104
}
105
106
// SubjectFilterResponse -
107
type SubjectFilterResponse struct {
108
	resp []string
109
	err  error
110
}
111
112
// getEmptyValueForType is a helper function that takes a string representation of a type
113
// and returns an "empty" value for that type.
114
// An empty value is a value that is generally considered a default or initial state for a variable of a given type.
115
// The purpose of this function is to be able to initialize a variable of a given type without knowing the type in advance.
116
// The function uses a switch statement to handle different possible type values and returns a corresponding empty value.
117
func getEmptyValueForType(typ base.AttributeType) interface{} {
118
	switch typ {
119
	case base.AttributeType_ATTRIBUTE_TYPE_STRING:
120
		// In the case of a string type, an empty string "" is considered the empty value.
121
		return ""
122
	case base.AttributeType_ATTRIBUTE_TYPE_STRING_ARRAY:
123
		// In the case of a string type, an empty string "" is considered the empty value.
124
		return []string{}
125
	case base.AttributeType_ATTRIBUTE_TYPE_INTEGER:
126
		// In the case of an integer type, zero (0) is considered the empty value.
127
		return 0
128
	case base.AttributeType_ATTRIBUTE_TYPE_INTEGER_ARRAY:
129
		// In the case of an integer type, zero (0) is considered the empty value.
130
		return []int32{}
131
	case base.AttributeType_ATTRIBUTE_TYPE_DOUBLE:
132
		// In the case of a double (or floating point) type, zero (0.0) is considered the empty value.
133
		return 0.0
134
	case base.AttributeType_ATTRIBUTE_TYPE_DOUBLE_ARRAY:
135
		// In the case of a double (or floating point) type, zero (0.0) is considered the empty value.
136
		return []float64{}
137
	case base.AttributeType_ATTRIBUTE_TYPE_BOOLEAN:
138
		// In the case of a boolean type, false is considered the empty value.
139
		return false
140
	case base.AttributeType_ATTRIBUTE_TYPE_BOOLEAN_ARRAY:
141
		// In the case of a boolean type, false is considered the empty value.
142
		return []bool{}
143
	default:
144
		// For any other types that are not explicitly handled, the function returns nil.
145
		// This may need to be adjusted if there are other types that need specific empty values.
146
		return nil
147
	}
148
}
149
150
// getEmptyProtoValueForType returns an empty protobuf value of the specified type.
151
// It takes a base.AttributeType as input and generates an empty protobuf Any message
152
// containing a default value for that type.
153
func getEmptyProtoValueForType(typ base.AttributeType) (*anypb.Any, error) {
154
	switch typ {
155
	case base.AttributeType_ATTRIBUTE_TYPE_STRING:
156
		// Create an empty protobuf String message
157
		value, err := anypb.New(&base.StringValue{Data: ""})
158
		if err != nil {
159
			return nil, err
160
		}
161
		return value, nil
162
163
	case base.AttributeType_ATTRIBUTE_TYPE_STRING_ARRAY:
164
		// Create an empty protobuf StringArray message
165
		value, err := anypb.New(&base.StringArrayValue{Data: []string{}})
166
		if err != nil {
167
			return nil, err
168
		}
169
		return value, nil
170
171
	case base.AttributeType_ATTRIBUTE_TYPE_INTEGER:
172
		// Create an empty protobuf Integer message
173
		value, err := anypb.New(&base.IntegerValue{Data: 0})
174
		if err != nil {
175
			return nil, err
176
		}
177
		return value, nil
178
179
	case base.AttributeType_ATTRIBUTE_TYPE_INTEGER_ARRAY:
180
		// Create an empty protobuf IntegerArray message
181
		value, err := anypb.New(&base.IntegerArrayValue{Data: []int32{}})
182
		if err != nil {
183
			return nil, err
184
		}
185
		return value, nil
186
187
	case base.AttributeType_ATTRIBUTE_TYPE_DOUBLE:
188
		// Create an empty protobuf Double message
189
		value, err := anypb.New(&base.DoubleValue{Data: 0.0})
190
		if err != nil {
191
			return nil, err
192
		}
193
		return value, nil
194
195
	case base.AttributeType_ATTRIBUTE_TYPE_DOUBLE_ARRAY:
196
		// Create an empty protobuf DoubleArray message
197
		value, err := anypb.New(&base.DoubleArrayValue{Data: []float64{}})
198
		if err != nil {
199
			return nil, err
200
		}
201
		return value, nil
202
203
	case base.AttributeType_ATTRIBUTE_TYPE_BOOLEAN:
204
		// Create an empty protobuf Boolean message with a default value of false
205
		value, err := anypb.New(&base.BooleanValue{Data: false})
206
		if err != nil {
207
			return nil, err
208
		}
209
		return value, nil
210
211
	case base.AttributeType_ATTRIBUTE_TYPE_BOOLEAN_ARRAY:
212
		// Create an empty protobuf BooleanArray message
213
		value, err := anypb.New(&base.BooleanArrayValue{Data: []bool{}})
214
		if err != nil {
215
			return nil, err
216
		}
217
		return value, nil
218
219
	default:
220
		// Handle the case where the provided attribute type is unknown
221
		return nil, errors.New("unknown type")
222
	}
223
}
224
225
// ConvertToAnyPB is a function to convert various basic Go types into *anypb.Any.
226
// It supports conversion from bool, int, float64, and string.
227
// It uses a type switch to detect the type of the input value.
228
// If the type is unsupported or unknown, it returns an error.
229
func ConvertToAnyPB(value interface{}) (*anypb.Any, error) {
230
	// anyValue will store the converted value, err will store any error occurred during conversion.
231
	var anyValue *anypb.Any
232
	var err error
233
234
	// Use a type switch to handle different types of value.
235
	switch v := value.(type) {
236
	case bool:
237
		anyValue, err = anypb.New(&base.BooleanValue{Data: v})
238
	case []bool:
239
		anyValue, err = anypb.New(&base.BooleanArrayValue{Data: v})
240
	case int:
241
		anyValue, err = anypb.New(&base.IntegerValue{Data: int32(v)})
242
	case []int32:
243
		anyValue, err = anypb.New(&base.IntegerArrayValue{Data: v})
244
	case float64:
245
		anyValue, err = anypb.New(&base.DoubleValue{Data: v})
246
	case []float64:
247
		anyValue, err = anypb.New(&base.DoubleArrayValue{Data: v})
248
	case string:
249
		anyValue, err = anypb.New(&base.StringValue{Data: v})
250
	case []string:
251
		anyValue, err = anypb.New(&base.StringArrayValue{Data: v})
252
	default:
253
		// In case of an unsupported or unknown type, we return an error.
254
		return nil, errors.New("unknown type")
255
	}
256
257
	// If there was an error during the conversion, return the error.
258
	if err != nil {
259
		return nil, err
260
	}
261
262
	// If the conversion was successful, return the converted value.
263
	return anyValue, nil
264
}
265
266
// GenerateKey function takes a PermissionCheckRequest and generates a unique key
267
// Key format: check|{tenant_id}|{schema_version}|{snap_token}|{context}|{entity:id#permission(optional_arguments)@subject:id#optional_relation}
268
func GenerateKey(key *base.PermissionCheckRequest, isRelational bool) string {
269
	// Initialize the parts slice with the string "check"
270
	parts := []string{"check"}
271
272
	// If tenantId is not empty, append it to parts
273
	if tenantId := key.GetTenantId(); tenantId != "" {
274
		parts = append(parts, tenantId)
275
	}
276
277
	// If Metadata exists, extract schema version and snap token and append them to parts if they are not empty
278
	if meta := key.GetMetadata(); meta != nil {
279
		if version := meta.GetSchemaVersion(); version != "" {
280
			parts = append(parts, version)
281
		}
282
		if token := meta.GetSnapToken(); token != "" {
283
			parts = append(parts, token)
284
		}
285
	}
286
287
	// If Context exists, convert it to string and append it to parts
288
	if ctx := key.GetContext(); ctx != nil {
289
		parts = append(parts, ContextToString(ctx))
290
	}
291
292
	if isRelational {
293
		// Convert entity and relation to string with any optional arguments and append to parts
294
		entityRelationString := tuple.EntityAndRelationToString(key.GetEntity(), key.GetPermission())
295
296
		subjectString := tuple.SubjectToString(key.GetSubject())
297
298
		if entityRelationString != "" {
299
			parts = append(parts, fmt.Sprintf("%s@%s", entityRelationString, subjectString))
300
		}
301
	} else {
302
		parts = append(parts, attribute.EntityAndCallOrAttributeToString(
303
			key.GetEntity(),
304
			key.GetPermission(),
305
			key.GetArguments()...,
306
		))
307
	}
308
309
	// Join all parts with "|" delimiter to generate the final key
310
	return strings.Join(parts, "|")
311
}
312
313
// ContextToString function takes a Context object and converts it into a string
314
func ContextToString(context *base.Context) string {
315
	// Initialize an empty slice to store parts of the context
316
	var parts []string
317
318
	// For each Tuple in the Context, convert it to a string and append to parts
319
	for _, tup := range context.GetTuples() {
320
		parts = append(parts, tuple.ToString(tup)) // replace with your function
321
	}
322
323
	// For each Attribute in the Context, convert it to a string and append to parts
324
	for _, attr := range context.GetAttributes() {
325
		parts = append(parts, attribute.ToString(attr)) // replace with your function
326
	}
327
328
	// If Data exists in the Context, convert it to JSON string and append to parts
329
	if data := context.GetData(); data != nil {
330
		parts = append(parts, mapToString(data.AsMap()))
331
	}
332
333
	// Join all parts with "," delimiter to generate the final context string
334
	return strings.Join(parts, ",")
335
}
336
337
// mapToString function takes a map[string]interface{} and converts it into a string
338
func mapToString(m map[string]interface{}) string {
339
	var keys []string
340
	for key := range m {
341
		keys = append(keys, key)
342
	}
343
	sort.Strings(keys)
344
345
	var parts []string
346
	for _, key := range keys {
347
		value := m[key]
348
		parts = append(parts, fmt.Sprintf("%s:%v", key, value))
349
	}
350
	return strings.Join(parts, ",")
351
}
352
353
// IsRelational determines if a given permission corresponds to a relational attribute
354
// in the provided entity definition.
355
func IsRelational(en *base.EntityDefinition, permission string) bool {
356
	// Default to non-relational
357
	isRelational := false
358
359
	// Attempt to get the type of reference for the given permission in the entity definition
360
	tor, err := schema.GetTypeOfReferenceByNameInEntityDefinition(en, permission)
361
	if err == nil && tor != base.EntityDefinition_REFERENCE_ATTRIBUTE {
362
		// If the type of reference is anything other than REFERENCE_ATTRIBUTE,
363
		// treat it as a relational attribute
364
		isRelational = true
365
	}
366
367
	return isRelational
368
}
369