@@ -4,8 +4,11 @@ import (
4
4
"context"
5
5
_ "embed"
6
6
"sync"
7
+ "time"
7
8
8
9
"github.com/open-policy-agent/opa/rego"
10
+ "github.com/prometheus/client_golang/prometheus"
11
+ "github.com/prometheus/client_golang/prometheus/promauto"
9
12
"go.opentelemetry.io/otel/attribute"
10
13
"go.opentelemetry.io/otel/trace"
11
14
"golang.org/x/xerrors"
@@ -21,27 +24,37 @@ type Authorizer interface {
21
24
22
25
type PreparedAuthorized interface {
23
26
Authorize (ctx context.Context , object Object ) error
24
- CompileToSQL (cfg regosql.ConvertConfig ) (string , error )
27
+ CompileToSQL (ctx context. Context , cfg regosql.ConvertConfig ) (string , error )
25
28
}
26
29
27
30
// Filter takes in a list of objects, and will filter the list removing all
28
31
// the elements the subject does not have permission for. All objects must be
29
32
// of the same type.
33
+ //
34
+ // Ideally the 'CompileToSQL' is used instead for large sets. This cost scales
35
+ // linearly with the number of objects passed in.
30
36
func Filter [O Objecter ](ctx context.Context , auth Authorizer , subjID string , subjRoles []string , scope Scope , groups []string , action Action , objects []O ) ([]O , error ) {
31
- ctx , span := tracing .StartSpan (ctx , trace .WithAttributes (
32
- attribute .String ("subject_id" , subjID ),
33
- attribute .StringSlice ("subject_roles" , subjRoles ),
34
- attribute .Int ("num_objects" , len (objects )),
35
- ))
36
- defer span .End ()
37
-
38
37
if len (objects ) == 0 {
39
38
// Nothing to filter
40
39
return objects , nil
41
40
}
42
41
objectType := objects [0 ].RBACObject ().Type
43
42
filtered := make ([]O , 0 )
44
43
44
+ // Start the span after the object type is detected. If we are filtering 0
45
+ // objects, then the span is not interesting. It would just add excessive
46
+ // 0 time spans that provide no insight.
47
+ ctx , span := tracing .StartSpan (ctx ,
48
+ rbacTraceAttributes (subjRoles , len (groups ), scope , action , objectType ,
49
+ // For filtering, we are only measuring the total time for the entire
50
+ // set of objects. This and the 'PrepareByRoleName' span time
51
+ // is all that is required to measure the performance of this
52
+ // function on a per-object basis.
53
+ attribute .Int ("num_objects" , len (objects )),
54
+ ),
55
+ )
56
+ defer span .End ()
57
+
45
58
// Running benchmarks on this function, it is **always** faster to call
46
59
// auth.ByRoleName on <10 objects. This is because the overhead of
47
60
// 'PrepareByRoleName'. Once we cross 10 objects, then it starts to become
@@ -82,6 +95,9 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, sub
82
95
// RegoAuthorizer will use a prepared rego query for performing authorize()
83
96
type RegoAuthorizer struct {
84
97
query rego.PreparedEvalQuery
98
+
99
+ authorizeHist * prometheus.HistogramVec
100
+ prepareHist prometheus.Histogram
85
101
}
86
102
87
103
var _ Authorizer = (* RegoAuthorizer )(nil )
95
111
query rego.PreparedEvalQuery
96
112
)
97
113
98
- func NewAuthorizer () * RegoAuthorizer {
114
+ func NewAuthorizer (registry prometheus. Registerer ) * RegoAuthorizer {
99
115
queryOnce .Do (func () {
100
116
var err error
101
117
query , err = rego .New (
@@ -106,7 +122,51 @@ func NewAuthorizer() *RegoAuthorizer {
106
122
panic (xerrors .Errorf ("compile rego: %w" , err ))
107
123
}
108
124
})
109
- return & RegoAuthorizer {query : query }
125
+
126
+ // Register metrics to prometheus.
127
+ // These bucket values are based on the average time it takes to run authz
128
+ // being around 1ms. Anything under ~2ms is OK and does not need to be
129
+ // analyzed any further.
130
+ buckets := []float64 {
131
+ 0.0005 , // 0.5ms
132
+ 0.001 , // 1ms
133
+ 0.002 , // 2ms
134
+ 0.003 ,
135
+ 0.005 ,
136
+ 0.01 , // 10ms
137
+ 0.02 ,
138
+ 0.035 , // 35ms
139
+ 0.05 ,
140
+ 0.075 ,
141
+ 0.1 , // 100ms
142
+ 0.25 , // 250ms
143
+ 0.75 , // 750ms
144
+ 1 , // 1s
145
+ }
146
+
147
+ factory := promauto .With (registry )
148
+ authorizeHistogram := factory .NewHistogramVec (prometheus.HistogramOpts {
149
+ Namespace : "coderd" ,
150
+ Subsystem : "authz" ,
151
+ Name : "authorize_duration_seconds" ,
152
+ Help : "Duration of the 'Authorize' call in seconds. Only counts calls that succeed." ,
153
+ Buckets : buckets ,
154
+ }, []string {"allowed" })
155
+
156
+ prepareHistogram := factory .NewHistogram (prometheus.HistogramOpts {
157
+ Namespace : "coderd" ,
158
+ Subsystem : "authz" ,
159
+ Name : "prepare_authorize_duration_seconds" ,
160
+ Help : "Duration of the 'PrepareAuthorize' call in seconds." ,
161
+ Buckets : buckets ,
162
+ })
163
+
164
+ return & RegoAuthorizer {
165
+ query : query ,
166
+
167
+ authorizeHist : authorizeHistogram ,
168
+ prepareHist : prepareHistogram ,
169
+ }
110
170
}
111
171
112
172
type authSubject struct {
@@ -120,6 +180,18 @@ type authSubject struct {
120
180
// This is the function intended to be used outside this package.
121
181
// The role is fetched from the builtin map located in memory.
122
182
func (a RegoAuthorizer ) ByRoleName (ctx context.Context , subjectID string , roleNames []string , scope Scope , groups []string , action Action , object Object ) error {
183
+ start := time .Now ()
184
+ ctx , span := tracing .StartSpan (ctx ,
185
+ trace .WithTimestamp (start ), // Reuse the time.Now for metric and trace
186
+ rbacTraceAttributes (roleNames , len (groups ), scope , action , object .Type ,
187
+ // For authorizing a single object, this data is useful to know how
188
+ // complex our objects are getting.
189
+ attribute .Int ("object_num_groups" , len (object .ACLGroupList )),
190
+ attribute .Int ("object_num_users" , len (object .ACLUserList )),
191
+ ),
192
+ )
193
+ defer span .End ()
194
+
123
195
roles , err := RolesByNames (roleNames )
124
196
if err != nil {
125
197
return err
@@ -131,19 +203,20 @@ func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNa
131
203
}
132
204
133
205
err = a .Authorize (ctx , subjectID , roles , scopeRole , groups , action , object )
206
+ span .AddEvent ("authorized" , trace .WithAttributes (attribute .Bool ("authorized" , err == nil )))
207
+ dur := time .Since (start )
134
208
if err != nil {
209
+ a .authorizeHist .WithLabelValues ("false" ).Observe (dur .Seconds ())
135
210
return err
136
211
}
137
212
213
+ a .authorizeHist .WithLabelValues ("true" ).Observe (dur .Seconds ())
138
214
return nil
139
215
}
140
216
141
217
// Authorize allows passing in custom Roles.
142
218
// This is really helpful for unit testing, as we can create custom roles to exercise edge cases.
143
219
func (a RegoAuthorizer ) Authorize (ctx context.Context , subjectID string , roles []Role , scope Role , groups []string , action Action , object Object ) error {
144
- ctx , span := tracing .StartSpan (ctx )
145
- defer span .End ()
146
-
147
220
input := map [string ]interface {}{
148
221
"subject" : authSubject {
149
222
ID : subjectID ,
@@ -166,22 +239,12 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [
166
239
return nil
167
240
}
168
241
169
- // Prepare will partially execute the rego policy leaving the object fields unknown (except for the type).
170
- // This will vastly speed up performance if batch authorization on the same type of objects is needed.
171
- func (RegoAuthorizer ) Prepare (ctx context.Context , subjectID string , roles []Role , scope Role , groups []string , action Action , objectType string ) (* PartialAuthorizer , error ) {
172
- ctx , span := tracing .StartSpan (ctx )
173
- defer span .End ()
174
-
175
- auth , err := newPartialAuthorizer (ctx , subjectID , roles , scope , groups , action , objectType )
176
- if err != nil {
177
- return nil , xerrors .Errorf ("new partial authorizer: %w" , err )
178
- }
179
-
180
- return auth , nil
181
- }
182
-
183
242
func (a RegoAuthorizer ) PrepareByRoleName (ctx context.Context , subjectID string , roleNames []string , scope Scope , groups []string , action Action , objectType string ) (PreparedAuthorized , error ) {
184
- ctx , span := tracing .StartSpan (ctx )
243
+ start := time .Now ()
244
+ ctx , span := tracing .StartSpan (ctx ,
245
+ trace .WithTimestamp (start ),
246
+ rbacTraceAttributes (roleNames , len (groups ), scope , action , objectType ),
247
+ )
185
248
defer span .End ()
186
249
187
250
roles , err := RolesByNames (roleNames )
@@ -194,5 +257,29 @@ func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string,
194
257
return nil , err
195
258
}
196
259
197
- return a .Prepare (ctx , subjectID , roles , scopeRole , groups , action , objectType )
260
+ prepared , err := a .Prepare (ctx , subjectID , roles , scopeRole , groups , action , objectType )
261
+ if err != nil {
262
+ return nil , err
263
+ }
264
+
265
+ // Add attributes of the Prepare results. This will help understand the
266
+ // complexity of the roles and how it affects the time taken.
267
+ span .SetAttributes (
268
+ attribute .Int ("num_queries" , len (prepared .preparedQueries )),
269
+ attribute .Bool ("always_true" , prepared .alwaysTrue ),
270
+ )
271
+
272
+ a .prepareHist .Observe (time .Since (start ).Seconds ())
273
+ return prepared , nil
274
+ }
275
+
276
+ // Prepare will partially execute the rego policy leaving the object fields unknown (except for the type).
277
+ // This will vastly speed up performance if batch authorization on the same type of objects is needed.
278
+ func (RegoAuthorizer ) Prepare (ctx context.Context , subjectID string , roles []Role , scope Role , groups []string , action Action , objectType string ) (* PartialAuthorizer , error ) {
279
+ auth , err := newPartialAuthorizer (ctx , subjectID , roles , scope , groups , action , objectType )
280
+ if err != nil {
281
+ return nil , xerrors .Errorf ("new partial authorizer: %w" , err )
282
+ }
283
+
284
+ return auth , nil
198
285
}
0 commit comments