6
6
"sync"
7
7
"time"
8
8
9
+ "github.com/open-policy-agent/opa/ast"
9
10
"github.com/open-policy-agent/opa/rego"
10
11
"github.com/prometheus/client_golang/prometheus"
11
12
"github.com/prometheus/client_golang/prometheus/promauto"
@@ -18,6 +19,21 @@ import (
18
19
"github.com/coder/coder/coderd/util/slice"
19
20
)
20
21
22
+ // Action represents the allowed actions to be done on an object.
23
+ type Action string
24
+
25
+ const (
26
+ ActionCreate Action = "create"
27
+ ActionRead Action = "read"
28
+ ActionUpdate Action = "update"
29
+ ActionDelete Action = "delete"
30
+ )
31
+
32
+ // AllActions is a helper function to return all the possible actions types.
33
+ func AllActions () []Action {
34
+ return []Action {ActionCreate , ActionRead , ActionUpdate , ActionDelete }
35
+ }
36
+
21
37
// Subject is a struct that contains all the elements of a subject in an rbac
22
38
// authorize.
23
39
type Subject struct {
@@ -329,3 +345,190 @@ func (a RegoAuthorizer) Prepare(ctx context.Context, subject Subject, action Act
329
345
a .prepareHist .Observe (time .Since (start ).Seconds ())
330
346
return prepared , nil
331
347
}
348
+
349
+ // PartialAuthorizer is a prepared authorizer with the subject, action, and
350
+ // resource type fields already filled in. This speeds up authorization
351
+ // when authorizing the same type of object numerous times.
352
+ // See rbac.Filter for example usage.
353
+ type PartialAuthorizer struct {
354
+ // partialQueries is mainly used for unit testing to assert our rego policy
355
+ // can always be compressed into a set of queries.
356
+ partialQueries * rego.PartialQueries
357
+
358
+ // input is used purely for debugging and logging.
359
+ subjectInput Subject
360
+ subjectAction Action
361
+ subjectResourceType Object
362
+
363
+ // preparedQueries are the compiled set of queries after partial evaluation.
364
+ // Cache these prepared queries to avoid re-compiling the queries.
365
+ // If alwaysTrue is true, then ignore these.
366
+ preparedQueries []rego.PreparedEvalQuery
367
+ // alwaysTrue is if the subject can always perform the action on the
368
+ // resource type, regardless of the unknown fields.
369
+ alwaysTrue bool
370
+ }
371
+
372
+ var _ PreparedAuthorized = (* PartialAuthorizer )(nil )
373
+
374
+ // CompileToSQL converts the remaining rego queries into SQL WHERE clauses.
375
+ func (pa * PartialAuthorizer ) CompileToSQL (ctx context.Context , cfg regosql.ConvertConfig ) (string , error ) {
376
+ _ , span := tracing .StartSpan (ctx , trace .WithAttributes (
377
+ // Query count is a rough indicator of the complexity of the query
378
+ // that needs to be converted into SQL.
379
+ attribute .Int ("query_count" , len (pa .preparedQueries )),
380
+ attribute .Bool ("always_true" , pa .alwaysTrue ),
381
+ ))
382
+ defer span .End ()
383
+
384
+ filter , err := Compile (cfg , pa )
385
+ if err != nil {
386
+ return "" , xerrors .Errorf ("compile: %w" , err )
387
+ }
388
+ return filter .SQLString (), nil
389
+ }
390
+
391
+ func (pa * PartialAuthorizer ) Authorize (ctx context.Context , object Object ) error {
392
+ if pa .alwaysTrue {
393
+ return nil
394
+ }
395
+
396
+ // If we have no queries, then no queries can return 'true'.
397
+ // So the result is always 'false'.
398
+ if len (pa .preparedQueries ) == 0 {
399
+ return ForbiddenWithInternal (xerrors .Errorf ("policy disallows request" ),
400
+ pa .subjectInput , pa .subjectAction , pa .subjectResourceType , nil )
401
+ }
402
+
403
+ parsed , err := ast .InterfaceToValue (map [string ]interface {}{
404
+ "object" : object ,
405
+ })
406
+ if err != nil {
407
+ return xerrors .Errorf ("parse object: %w" , err )
408
+ }
409
+
410
+ // How to interpret the results of the partial queries.
411
+ // We have a list of queries that are along the lines of:
412
+ // `input.object.org_owner = ""; "me" = input.object.owner`
413
+ // `input.object.org_owner in {"feda2e52-8bf1-42ce-ad75-6c5595cb297a"} `
414
+ // All these queries are joined by an 'OR'. So we need to run through each
415
+ // query, and evaluate it.
416
+ //
417
+ // In each query, we have a list of the expressions, which should be
418
+ // all boolean expressions. In the above 1st example, there are 2.
419
+ // These expressions within a single query are `AND` together by rego.
420
+ EachQueryLoop:
421
+ for _ , q := range pa .preparedQueries {
422
+ // We need to eval each query with the newly known fields.
423
+ results , err := q .Eval (ctx , rego .EvalParsedInput (parsed ))
424
+ if err != nil {
425
+ continue EachQueryLoop
426
+ }
427
+
428
+ // If there are no results, then the query is false. This is because rego
429
+ // treats false queries as "undefined". So if any expression is false, the
430
+ // result is an empty list.
431
+ if len (results ) == 0 {
432
+ continue EachQueryLoop
433
+ }
434
+
435
+ // If there is more than 1 result, that means there is more than 1 rule.
436
+ // This should not happen, because our query should always be an expression.
437
+ // If this every occurs, it is likely the original query was not an expression.
438
+ if len (results ) > 1 {
439
+ continue EachQueryLoop
440
+ }
441
+
442
+ // Our queries should be simple, and should not yield any bindings.
443
+ // A binding is something like 'x := 1'. This binding as an expression is
444
+ // 'true', but in our case is unhelpful. We are not analyzing this ast to
445
+ // map bindings. So just error out. Similar to above, our queries should
446
+ // always be boolean expressions.
447
+ if len (results [0 ].Bindings ) > 0 {
448
+ continue EachQueryLoop
449
+ }
450
+
451
+ // We have a valid set of boolean expressions! All expressions are 'AND'd
452
+ // together. This is automatic by rego, so we should not actually need to
453
+ // inspect this any further. But just in case, we will verify each expression
454
+ // did resolve to 'true'. This is purely defensive programming.
455
+ for _ , exp := range results [0 ].Expressions {
456
+ if v , ok := exp .Value .(bool ); ! ok || ! v {
457
+ continue EachQueryLoop
458
+ }
459
+ }
460
+
461
+ return nil
462
+ }
463
+
464
+ return ForbiddenWithInternal (xerrors .Errorf ("policy disallows request" ),
465
+ pa .subjectInput , pa .subjectAction , pa .subjectResourceType , nil )
466
+ }
467
+
468
+ func (a RegoAuthorizer ) newPartialAuthorizer (ctx context.Context , subject Subject , action Action , objectType string ) (* PartialAuthorizer , error ) {
469
+ if subject .Roles == nil {
470
+ return nil , xerrors .Errorf ("subject must have roles" )
471
+ }
472
+ if subject .Scope == nil {
473
+ return nil , xerrors .Errorf ("subject must have a scope" )
474
+ }
475
+
476
+ input , err := regoPartialInputValue (subject , action , objectType )
477
+ if err != nil {
478
+ return nil , xerrors .Errorf ("prepare input: %w" , err )
479
+ }
480
+
481
+ partialQueries , err := a .partialQuery .Partial (ctx , rego .EvalParsedInput (input ))
482
+ if err != nil {
483
+ return nil , xerrors .Errorf ("prepare: %w" , err )
484
+ }
485
+
486
+ pAuth := & PartialAuthorizer {
487
+ partialQueries : partialQueries ,
488
+ preparedQueries : []rego.PreparedEvalQuery {},
489
+ subjectInput : subject ,
490
+ subjectResourceType : Object {
491
+ Type : objectType ,
492
+ ID : "prepared-object" ,
493
+ },
494
+ subjectAction : action ,
495
+ }
496
+
497
+ // Prepare each query to optimize the runtime when we iterate over the objects.
498
+ preparedQueries := make ([]rego.PreparedEvalQuery , 0 , len (partialQueries .Queries ))
499
+ for _ , q := range partialQueries .Queries {
500
+ if q .String () == "" {
501
+ // No more work needed. An empty query is the same as
502
+ // 'WHERE true'
503
+ // This is likely an admin. We don't even need to use rego going
504
+ // forward.
505
+ pAuth .alwaysTrue = true
506
+ preparedQueries = []rego.PreparedEvalQuery {}
507
+ break
508
+ }
509
+ results , err := rego .New (
510
+ rego .ParsedQuery (q ),
511
+ ).PrepareForEval (ctx )
512
+ if err != nil {
513
+ return nil , xerrors .Errorf ("prepare query %s: %w" , q .String (), err )
514
+ }
515
+ preparedQueries = append (preparedQueries , results )
516
+ }
517
+ pAuth .preparedQueries = preparedQueries
518
+
519
+ return pAuth , nil
520
+ }
521
+
522
+ // rbacTraceAttributes are the attributes that are added to all spans created by
523
+ // the rbac package. These attributes should help to debug slow spans.
524
+ func rbacTraceAttributes (actor Subject , action Action , objectType string , extra ... attribute.KeyValue ) trace.SpanStartOption {
525
+ return trace .WithAttributes (
526
+ append (extra ,
527
+ attribute .StringSlice ("subject_roles" , actor .SafeRoleNames ()),
528
+ attribute .Int ("num_subject_roles" , len (actor .SafeRoleNames ())),
529
+ attribute .Int ("num_groups" , len (actor .Groups )),
530
+ attribute .String ("scope" , actor .SafeScopeName ()),
531
+ attribute .String ("action" , string (action )),
532
+ attribute .String ("object_type" , objectType ),
533
+ )... )
534
+ }
0 commit comments