Passed
Push — master ( b5d61d...0d5e57 )
by Tolga
01:08 queued 13s
created

invoke.*DirectInvoker.SubjectPermission   B

Complexity

Conditions 5

Size

Total Lines 48
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 26
nop 2
dl 0
loc 48
rs 8.7893
c 0
b 0
f 0
1
package invoke
2
3
import (
4
	"context"
5
	"time"
6
7
	"go.opentelemetry.io/otel"
8
	"go.opentelemetry.io/otel/attribute"
9
	otelCodes "go.opentelemetry.io/otel/codes"
10
	api "go.opentelemetry.io/otel/metric"
11
	"go.opentelemetry.io/otel/trace"
12
13
	"github.com/Permify/permify/internal/storage"
14
	base "github.com/Permify/permify/pkg/pb/base/v1"
15
	"github.com/Permify/permify/pkg/token"
16
	"github.com/Permify/permify/pkg/tuple"
17
)
18
19
var tracer = otel.Tracer("invoke")
20
var meter = otel.Meter("invoke")
21
22
// Invoker is an interface that groups multiple permission-related interfaces.
23
// It is used to define a common contract for invoking various permission operations.
24
type Invoker interface {
25
	Check
26
	Expand
27
	Lookup
28
	SubjectPermission
29
}
30
31
// Check is an interface that defines a method for checking permissions.
32
// It requires an implementation of InvokeCheck that takes a context and a PermissionCheckRequest,
33
// and returns a PermissionCheckResponse and an error if any.
34
type Check interface {
35
	Check(ctx context.Context, request *base.PermissionCheckRequest) (response *base.PermissionCheckResponse, err error)
36
}
37
38
// Expand is an interface that defines a method for expanding permissions.
39
// It requires an implementation of InvokeExpand that takes a context and a PermissionExpandRequest,
40
// and returns a PermissionExpandResponse and an error if any.
41
type Expand interface {
42
	Expand(ctx context.Context, request *base.PermissionExpandRequest) (response *base.PermissionExpandResponse, err error)
43
}
44
45
type Lookup interface {
46
	LookupEntity(ctx context.Context, request *base.PermissionLookupEntityRequest) (response *base.PermissionLookupEntityResponse, err error)
47
	LookupEntityStream(ctx context.Context, request *base.PermissionLookupEntityRequest, server base.Permission_LookupEntityStreamServer) (err error)
48
	LookupSubject(ctx context.Context, request *base.PermissionLookupSubjectRequest) (response *base.PermissionLookupSubjectResponse, err error)
49
}
50
51
// SubjectPermission -
52
type SubjectPermission interface {
53
	SubjectPermission(ctx context.Context, request *base.PermissionSubjectPermissionRequest) (response *base.PermissionSubjectPermissionResponse, err error)
54
}
55
56
// DirectInvoker is a struct that implements the Invoker interface.
57
// It holds references to various engines needed for permission-related operations.
58
type DirectInvoker struct {
59
	// schemaReader is responsible for reading schema information
60
	schemaReader storage.SchemaReader
61
	// relationshipReader is responsible for reading relationship information
62
	dataReader storage.DataReader
63
	// Check engine for permission checks
64
	cc Check
65
	// Expand engine for expanding permissions
66
	ec Expand
67
	// LookupEntity engine for looking up entities with permissions
68
	lo Lookup
69
	// LookupSubject
70
	sp SubjectPermission
71
72
	// Metrics
73
	checkCounter             api.Int64Counter
74
	lookupEntityCounter      api.Int64Counter
75
	lookupSubjectCounter     api.Int64Counter
76
	subjectPermissionCounter api.Int64Counter
77
78
	checkDurationHistogram             api.Int64Histogram
79
	lookupEntityDurationHistogram      api.Int64Histogram
80
	lookupSubjectDurationHistogram     api.Int64Histogram
81
	subjectPermissionDurationHistogram api.Int64Histogram
82
}
83
84
// NewDirectInvoker is a constructor for DirectInvoker.
85
// It takes pointers to CheckEngine, ExpandEngine, LookupSchemaEngine, and LookupEntityEngine as arguments
86
// and returns an Invoker instance.
87
func NewDirectInvoker(
88
	schemaReader storage.SchemaReader,
89
	dataReader storage.DataReader,
90
	cc Check,
91
	ec Expand,
92
	lo Lookup,
93
	sp SubjectPermission,
94
) *DirectInvoker {
95
	// Check Counter
96
	checkCounter, err := meter.Int64Counter("check_count", api.WithDescription("Number of permission checks performed"))
97
	if err != nil {
98
		panic(err)
99
	}
100
101
	// Lookup Entity Counter
102
	lookupEntityCounter, err := meter.Int64Counter("lookup_entity_count", api.WithDescription("Number of permission lookup entity performed"))
103
	if err != nil {
104
		panic(err)
105
	}
106
107
	// Lookup Subject Counter
108
	lookupSubjectCounter, err := meter.Int64Counter("lookup_subject_count", api.WithDescription("Number of permission lookup subject performed"))
109
	if err != nil {
110
		panic(err)
111
	}
112
113
	// Subject Permission Counter
114
	subjectPermissionCounter, err := meter.Int64Counter("subject_permission_count", api.WithDescription("Number of subject permission performed"))
115
	if err != nil {
116
		panic(err)
117
	}
118
119
	checkDurationHistogram, err := meter.Int64Histogram(
120
		"check_duration",
121
		api.WithUnit("microseconds"),
122
		api.WithDescription("Duration of check duration in microseconds"),
123
	)
124
	if err != nil {
125
		panic(err)
126
	}
127
128
	lookupEntityDurationHistogram, err := meter.Int64Histogram(
129
		"lookup_entity_duration",
130
		api.WithUnit("microseconds"),
131
		api.WithDescription("Duration of lookup entity duration in microseconds"),
132
	)
133
	if err != nil {
134
		panic(err)
135
	}
136
137
	lookupSubjectDurationHistogram, err := meter.Int64Histogram(
138
		"lookup_subject_duration",
139
		api.WithUnit("microseconds"),
140
		api.WithDescription("Duration of lookup subject duration in microseconds"),
141
	)
142
	if err != nil {
143
		panic(err)
144
	}
145
146
	subjectPermissionDurationHistogram, err := meter.Int64Histogram(
147
		"subject_permission_duration",
148
		api.WithUnit("microseconds"),
149
		api.WithDescription("Duration of subject permission duration in microseconds"),
150
	)
151
	if err != nil {
152
		panic(err)
153
	}
154
155
	return &DirectInvoker{
156
		schemaReader:                       schemaReader,
157
		dataReader:                         dataReader,
158
		cc:                                 cc,
159
		ec:                                 ec,
160
		lo:                                 lo,
161
		sp:                                 sp,
162
		checkCounter:                       checkCounter,
163
		lookupEntityCounter:                lookupEntityCounter,
164
		lookupSubjectCounter:               lookupSubjectCounter,
165
		subjectPermissionCounter:           subjectPermissionCounter,
166
		checkDurationHistogram:             checkDurationHistogram,
167
		lookupEntityDurationHistogram:      lookupEntityDurationHistogram,
168
		lookupSubjectDurationHistogram:     lookupSubjectDurationHistogram,
169
		subjectPermissionDurationHistogram: subjectPermissionDurationHistogram,
170
	}
171
}
172
173
// Check is a method that implements the Check interface.
174
// It calls the Run method of the CheckEngine with the provided context and PermissionCheckRequest,
175
// and returns a PermissionCheckResponse and an error if any.
176
func (invoker *DirectInvoker) Check(ctx context.Context, request *base.PermissionCheckRequest) (response *base.PermissionCheckResponse, err error) {
177
	ctx, span := tracer.Start(ctx, "check", trace.WithAttributes(
178
		attribute.KeyValue{Key: "tenant_id", Value: attribute.StringValue(request.GetTenantId())},
179
		attribute.KeyValue{Key: "entity", Value: attribute.StringValue(tuple.EntityToString(request.GetEntity()))},
180
		attribute.KeyValue{Key: "permission", Value: attribute.StringValue(request.GetPermission())},
181
		attribute.KeyValue{Key: "subject", Value: attribute.StringValue(tuple.SubjectToString(request.GetSubject()))},
182
	))
183
	defer span.End()
184
185
	start := time.Now()
186
187
	// Validate the depth of the request.
188
	err = checkDepth(request)
189
	if err != nil {
190
		span.RecordError(err)
191
		span.SetStatus(otelCodes.Error, err.Error())
192
		span.SetAttributes(attribute.KeyValue{Key: "can", Value: attribute.StringValue(base.CheckResult_CHECK_RESULT_DENIED.String())})
193
		return &base.PermissionCheckResponse{
194
			Can: base.CheckResult_CHECK_RESULT_DENIED,
195
			Metadata: &base.PermissionCheckResponseMetadata{
196
				CheckCount: 0,
197
			},
198
		}, err
199
	}
200
201
	// Set the SnapToken if it's not provided in the request.
202
	if request.GetMetadata().GetSnapToken() == "" {
203
		var st token.SnapToken
204
		st, err = invoker.dataReader.HeadSnapshot(ctx, request.GetTenantId())
205
		if err != nil {
206
			span.RecordError(err)
207
			span.SetStatus(otelCodes.Error, err.Error())
208
			span.SetAttributes(attribute.KeyValue{Key: "can", Value: attribute.StringValue(base.CheckResult_CHECK_RESULT_DENIED.String())})
209
			return &base.PermissionCheckResponse{
210
				Can: base.CheckResult_CHECK_RESULT_DENIED,
211
				Metadata: &base.PermissionCheckResponseMetadata{
212
					CheckCount: 0,
213
				},
214
			}, err
215
		}
216
		request.Metadata.SnapToken = st.Encode().String()
217
	}
218
219
	// Set the SchemaVersion if it's not provided in the request.
220
	if request.GetMetadata().GetSchemaVersion() == "" {
221
		request.Metadata.SchemaVersion, err = invoker.schemaReader.HeadVersion(ctx, request.GetTenantId())
222
		if err != nil {
223
			span.RecordError(err)
224
			span.SetStatus(otelCodes.Error, err.Error())
225
			span.SetAttributes(attribute.KeyValue{Key: "can", Value: attribute.StringValue(base.CheckResult_CHECK_RESULT_DENIED.String())})
226
			return &base.PermissionCheckResponse{
227
				Can: base.CheckResult_CHECK_RESULT_DENIED,
228
				Metadata: &base.PermissionCheckResponseMetadata{
229
					CheckCount: 0,
230
				},
231
			}, err
232
		}
233
	}
234
235
	// Decrease the depth of the request metadata.
236
	request.Metadata = decreaseDepth(request.GetMetadata())
237
238
	// Perform the actual permission check using the provided request.
239
	response, err = invoker.cc.Check(ctx, request)
240
	if err != nil {
241
		span.RecordError(err)
242
		span.SetStatus(otelCodes.Error, err.Error())
243
		span.SetAttributes(attribute.KeyValue{Key: "can", Value: attribute.StringValue(base.CheckResult_CHECK_RESULT_DENIED.String())})
244
		return &base.PermissionCheckResponse{
245
			Can: base.CheckResult_CHECK_RESULT_DENIED,
246
			Metadata: &base.PermissionCheckResponseMetadata{
247
				CheckCount: 0,
248
			},
249
		}, err
250
	}
251
	duration := time.Now().Sub(start)
252
253
	invoker.checkDurationHistogram.Record(ctx, duration.Microseconds())
254
255
	// Increase the check count in the response metadata.
256
	response.Metadata = increaseCheckCount(response.Metadata)
257
258
	// Increase the check count in the metrics.
259
	invoker.checkCounter.Add(ctx, 1)
260
261
	span.SetAttributes(attribute.KeyValue{Key: "can", Value: attribute.StringValue(response.GetCan().String())})
262
	return
263
}
264
265
// Expand is a method that implements the Expand interface.
266
// It calls the Run method of the ExpandEngine with the provided context and PermissionExpandRequest,
267
// and returns a PermissionExpandResponse and an error if any.
268
func (invoker *DirectInvoker) Expand(ctx context.Context, request *base.PermissionExpandRequest) (response *base.PermissionExpandResponse, err error) {
269
	ctx, span := tracer.Start(ctx, "expand", trace.WithAttributes(
270
		attribute.KeyValue{Key: "tenant_id", Value: attribute.StringValue(request.GetTenantId())},
271
		attribute.KeyValue{Key: "entity", Value: attribute.StringValue(tuple.EntityToString(request.GetEntity()))},
272
		attribute.KeyValue{Key: "permission", Value: attribute.StringValue(request.GetPermission())},
273
	))
274
	defer span.End()
275
276
	if request.GetMetadata().GetSnapToken() == "" {
277
		var st token.SnapToken
278
		st, err = invoker.dataReader.HeadSnapshot(ctx, request.GetTenantId())
279
		if err != nil {
280
			span.RecordError(err)
281
			span.SetStatus(otelCodes.Error, err.Error())
282
			return response, err
283
		}
284
		request.Metadata.SnapToken = st.Encode().String()
285
	}
286
287
	if request.GetMetadata().GetSchemaVersion() == "" {
288
		request.Metadata.SchemaVersion, err = invoker.schemaReader.HeadVersion(ctx, request.GetTenantId())
289
		if err != nil {
290
			span.RecordError(err)
291
			span.SetStatus(otelCodes.Error, err.Error())
292
			return response, err
293
		}
294
	}
295
296
	return invoker.ec.Expand(ctx, request)
297
}
298
299
// LookupEntity is a method that implements the LookupEntity interface.
300
// It calls the Run method of the LookupEntityEngine with the provided context and PermissionLookupEntityRequest,
301
// and returns a PermissionLookupEntityResponse and an error if any.
302
func (invoker *DirectInvoker) LookupEntity(ctx context.Context, request *base.PermissionLookupEntityRequest) (response *base.PermissionLookupEntityResponse, err error) {
303
	ctx, span := tracer.Start(ctx, "lookup-entity", trace.WithAttributes(
304
		attribute.KeyValue{Key: "tenant_id", Value: attribute.StringValue(request.GetTenantId())},
305
		attribute.KeyValue{Key: "entity_type", Value: attribute.StringValue(request.GetEntityType())},
306
		attribute.KeyValue{Key: "permission", Value: attribute.StringValue(request.GetPermission())},
307
		attribute.KeyValue{Key: "subject", Value: attribute.StringValue(tuple.SubjectToString(request.GetSubject()))},
308
	))
309
	defer span.End()
310
311
	start := time.Now()
312
313
	// Set SnapToken if not provided
314
	if request.GetMetadata().GetSnapToken() == "" { // Check if the request has a SnapToken.
315
		var st token.SnapToken
316
		st, err = invoker.dataReader.HeadSnapshot(ctx, request.GetTenantId()) // Retrieve the head snapshot from the relationship reader.
317
		if err != nil {
318
			span.RecordError(err)
319
			span.SetStatus(otelCodes.Error, err.Error())
320
			return response, err
321
		}
322
		request.Metadata.SnapToken = st.Encode().String() // Set the SnapToken in the request metadata.
323
	}
324
325
	// Set SchemaVersion if not provided
326
	if request.GetMetadata().GetSchemaVersion() == "" { // Check if the request has a SchemaVersion.
327
		request.Metadata.SchemaVersion, err = invoker.schemaReader.HeadVersion(ctx, request.GetTenantId()) // Retrieve the head schema version from the schema reader.
328
		if err != nil {
329
			span.RecordError(err)
330
			span.SetStatus(otelCodes.Error, err.Error())
331
			return response, err
332
		}
333
	}
334
335
	resp, err := invoker.lo.LookupEntity(ctx, request)
336
337
	duration := time.Now().Sub(start)
338
	invoker.lookupEntityDurationHistogram.Record(ctx, duration.Microseconds())
339
340
	// Increase the lookup entity count in the metrics.
341
	invoker.lookupEntityCounter.Add(ctx, 1)
342
343
	return resp, err
344
}
345
346
// LookupEntityStream is a method that implements the LookupEntityStream interface.
347
// It calls the Stream method of the LookupEntityEngine with the provided context, PermissionLookupEntityRequest, and Permission_LookupEntityStreamServer,
348
// and returns an error if any.
349
func (invoker *DirectInvoker) LookupEntityStream(ctx context.Context, request *base.PermissionLookupEntityRequest, server base.Permission_LookupEntityStreamServer) (err error) {
350
	ctx, span := tracer.Start(ctx, "lookup-entity-stream", trace.WithAttributes(
351
		attribute.KeyValue{Key: "tenant_id", Value: attribute.StringValue(request.GetTenantId())},
352
		attribute.KeyValue{Key: "entity_type", Value: attribute.StringValue(request.GetEntityType())},
353
		attribute.KeyValue{Key: "permission", Value: attribute.StringValue(request.GetPermission())},
354
		attribute.KeyValue{Key: "subject", Value: attribute.StringValue(tuple.SubjectToString(request.GetSubject()))},
355
	))
356
	defer span.End()
357
358
	start := time.Now()
359
360
	// Set SnapToken if not provided
361
	if request.GetMetadata().GetSnapToken() == "" { // Check if the request has a SnapToken.
362
		var st token.SnapToken
363
		st, err = invoker.dataReader.HeadSnapshot(ctx, request.GetTenantId()) // Retrieve the head snapshot from the relationship reader.
364
		if err != nil {
365
			span.RecordError(err)
366
			span.SetStatus(otelCodes.Error, err.Error())
367
			return err
368
		}
369
		request.Metadata.SnapToken = st.Encode().String() // Set the SnapToken in the request metadata.
370
	}
371
372
	// Set SchemaVersion if not provided
373
	if request.GetMetadata().GetSchemaVersion() == "" { // Check if the request has a SchemaVersion.
374
		request.Metadata.SchemaVersion, err = invoker.schemaReader.HeadVersion(ctx, request.GetTenantId()) // Retrieve the head schema version from the schema reader.
375
		if err != nil {
376
			span.RecordError(err)
377
			span.SetStatus(otelCodes.Error, err.Error())
378
			return err
379
		}
380
	}
381
382
	resp := invoker.lo.LookupEntityStream(ctx, request, server)
383
384
	duration := time.Now().Sub(start)
385
	invoker.lookupEntityDurationHistogram.Record(ctx, duration.Microseconds())
386
387
	// Increase the lookup entity count in the metrics.
388
	invoker.lookupEntityCounter.Add(ctx, 1)
389
390
	return resp
391
}
392
393
// LookupSubject is a method of the DirectInvoker structure. It handles the task of looking up subjects
394
// and returning the results in a response.
395
func (invoker *DirectInvoker) LookupSubject(ctx context.Context, request *base.PermissionLookupSubjectRequest) (response *base.PermissionLookupSubjectResponse, err error) {
396
	ctx, span := tracer.Start(ctx, "lookup-subject", trace.WithAttributes(
397
		attribute.KeyValue{Key: "tenant_id", Value: attribute.StringValue(request.GetTenantId())},
398
		attribute.KeyValue{Key: "entity", Value: attribute.StringValue(tuple.EntityToString(request.GetEntity()))},
399
		attribute.KeyValue{Key: "permission", Value: attribute.StringValue(request.GetPermission())},
400
		attribute.KeyValue{Key: "subject_reference", Value: attribute.StringValue(tuple.ReferenceToString(request.GetSubjectReference()))},
401
	))
402
	defer span.End()
403
404
	start := time.Now()
405
406
	// Check if the request has a SnapToken. If not, a SnapToken is set.
407
	if request.GetMetadata().GetSnapToken() == "" {
408
		// Create an instance of SnapToken
409
		var st token.SnapToken
410
		// Retrieve the head snapshot from the relationship reader
411
		st, err = invoker.dataReader.HeadSnapshot(ctx, request.GetTenantId())
412
		// If there's an error retrieving the snapshot, return the response and the error
413
		if err != nil {
414
			span.RecordError(err)
415
			span.SetStatus(otelCodes.Error, err.Error())
416
			return response, err
417
		}
418
		// Set the SnapToken in the request metadata
419
		request.Metadata.SnapToken = st.Encode().String()
420
	}
421
422
	// Similar to SnapToken, check if the request has a SchemaVersion. If not, a SchemaVersion is set.
423
	if request.GetMetadata().GetSchemaVersion() == "" {
424
		// Retrieve the head schema version from the schema reader
425
		request.Metadata.SchemaVersion, err = invoker.schemaReader.HeadVersion(ctx, request.GetTenantId())
426
		// If there's an error retrieving the schema version, return the response and the error
427
		if err != nil {
428
			span.RecordError(err)
429
			span.SetStatus(otelCodes.Error, err.Error())
430
			return response, err
431
		}
432
	}
433
434
	resp, err := invoker.lo.LookupSubject(ctx, request)
435
436
	duration := time.Now().Sub(start)
437
	invoker.lookupSubjectDurationHistogram.Record(ctx, duration.Microseconds())
438
439
	// Increase the lookup subject count in the metrics.
440
	invoker.lookupSubjectCounter.Add(ctx, 1)
441
442
	// Call the LookupSubject function of the ls field in the invoker, pass the context and request,
443
	// and return its response and error
444
	return resp, err
445
}
446
447
// SubjectPermission is a method of the DirectInvoker structure. It handles the task of subject's permissions
448
// and returning the results in a response.
449
func (invoker *DirectInvoker) SubjectPermission(ctx context.Context, request *base.PermissionSubjectPermissionRequest) (response *base.PermissionSubjectPermissionResponse, err error) {
450
	ctx, span := tracer.Start(ctx, "subject-permission", trace.WithAttributes(
451
		attribute.KeyValue{Key: "tenant_id", Value: attribute.StringValue(request.GetTenantId())},
452
		attribute.KeyValue{Key: "entity", Value: attribute.StringValue(tuple.EntityToString(request.GetEntity()))},
453
		attribute.KeyValue{Key: "subject", Value: attribute.StringValue(tuple.SubjectToString(request.GetSubject()))},
454
	))
455
	defer span.End()
456
457
	start := time.Now()
458
459
	// Check if the request has a SnapToken. If not, a SnapToken is set.
460
	if request.GetMetadata().GetSnapToken() == "" {
461
		// Create an instance of SnapToken
462
		var st token.SnapToken
463
		// Retrieve the head snapshot from the relationship reader
464
		st, err = invoker.dataReader.HeadSnapshot(ctx, request.GetTenantId())
465
		// If there's an error retrieving the snapshot, return the response and the error
466
		if err != nil {
467
			span.RecordError(err)
468
			span.SetStatus(otelCodes.Error, err.Error())
469
			return response, err
470
		}
471
		// Set the SnapToken in the request metadata
472
		request.Metadata.SnapToken = st.Encode().String()
473
	}
474
475
	// Similar to SnapToken, check if the request has a SchemaVersion. If not, a SchemaVersion is set.
476
	if request.GetMetadata().GetSchemaVersion() == "" {
477
		// Retrieve the head schema version from the schema reader
478
		request.Metadata.SchemaVersion, err = invoker.schemaReader.HeadVersion(ctx, request.GetTenantId())
479
		// If there's an error retrieving the schema version, return the response and the error
480
		if err != nil {
481
			span.RecordError(err)
482
			span.SetStatus(otelCodes.Error, err.Error())
483
			return response, err
484
		}
485
	}
486
	resp, err := invoker.sp.SubjectPermission(ctx, request)
487
488
	duration := time.Now().Sub(start)
489
	invoker.subjectPermissionDurationHistogram.Record(ctx, duration.Microseconds())
490
491
	// Increase the subject permission count in the metrics.
492
	invoker.subjectPermissionCounter.Add(ctx, 1)
493
494
	// Call the SubjectPermission function of the ls field in the invoker, pass the context and request,
495
	// and return its response and error
496
	return resp, err
497
}
498