@@ -3,6 +3,7 @@ package rbac
3
3
import (
4
4
"context"
5
5
_ "embed"
6
+ "fmt"
6
7
7
8
"github.com/open-policy-agent/opa/rego"
8
9
"go.opentelemetry.io/otel/attribute"
@@ -70,12 +71,20 @@ var _ Authorizer = (*RegoAuthorizer)(nil)
70
71
//go:embed policy.rego
71
72
var policy string
72
73
74
+ const (
75
+ rolesOkCheck = "role_ok"
76
+ scopeOkCheck = "scope_ok"
77
+ )
78
+
73
79
func NewAuthorizer () (* RegoAuthorizer , error ) {
74
80
ctx := context .Background ()
75
81
query , err := rego .New (
76
- // allowed is the `allow` field from the prepared query. This is the field to check if authorization is
77
- // granted.
78
- rego .Query ("data.authz.allow" ),
82
+ // Bind the results to 2 variables for easy checking later.
83
+ rego .Query (
84
+ fmt .Sprintf ("%s := data.authz.role_allow " +
85
+ "%s := data.authz.scope_allow" ,
86
+ rolesOkCheck , scopeOkCheck ),
87
+ ),
79
88
rego .Module ("policy.rego" , policy ),
80
89
).PrepareForEval (ctx )
81
90
@@ -88,6 +97,7 @@ func NewAuthorizer() (*RegoAuthorizer, error) {
88
97
type authSubject struct {
89
98
ID string `json:"id"`
90
99
Roles []Role `json:"roles"`
100
+ Scope Role `json:"scope"`
91
101
}
92
102
93
103
// ByRoleName will expand all roleNames into roles before calling Authorize().
@@ -99,37 +109,30 @@ func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNa
99
109
return err
100
110
}
101
111
102
- err = a . Authorize ( ctx , subjectID , roles , action , object )
112
+ scopeRole , err := ScopeRole ( scope )
103
113
if err != nil {
104
114
return err
105
115
}
106
116
107
- // If the scope isn't "any", we need to check with the scope's role as well.
108
- if scope != ScopeAll {
109
- scopeRole , err := ScopeRole (scope )
110
- if err != nil {
111
- return err
112
- }
113
-
114
- err = a .Authorize (ctx , subjectID , []Role {scopeRole }, action , object )
115
- if err != nil {
116
- return err
117
- }
117
+ err = a .Authorize (ctx , subjectID , roles , scopeRole , action , object )
118
+ if err != nil {
119
+ return err
118
120
}
119
121
120
122
return nil
121
123
}
122
124
123
125
// Authorize allows passing in custom Roles.
124
126
// This is really helpful for unit testing, as we can create custom roles to exercise edge cases.
125
- func (a RegoAuthorizer ) Authorize (ctx context.Context , subjectID string , roles []Role , action Action , object Object ) error {
127
+ func (a RegoAuthorizer ) Authorize (ctx context.Context , subjectID string , roles []Role , scope Role , action Action , object Object ) error {
126
128
ctx , span := tracing .StartSpan (ctx )
127
129
defer span .End ()
128
130
129
131
input := map [string ]interface {}{
130
132
"subject" : authSubject {
131
133
ID : subjectID ,
132
134
Roles : roles ,
135
+ Scope : scope ,
133
136
},
134
137
"object" : object ,
135
138
"action" : action ,
@@ -140,16 +143,36 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [
140
143
return ForbiddenWithInternal (xerrors .Errorf ("eval rego: %w" , err ), input , results )
141
144
}
142
145
143
- if ! results .Allowed () {
144
- return ForbiddenWithInternal (xerrors .Errorf ("policy disallows request" ), input , results )
145
- }
146
+ // We expect only the 2 bindings for scopes and roles checks.
147
+ if len (results ) == 1 && len (results [0 ].Bindings ) == 2 {
148
+ roleCheck , ok := results [0 ].Bindings [rolesOkCheck ].(bool )
149
+ if ! ok || ! roleCheck {
150
+ return ForbiddenWithInternal (xerrors .Errorf ("policy disallows request" ), input , results )
151
+ }
146
152
147
- return nil
153
+ scopeCheck , ok := results [0 ].Bindings [scopeOkCheck ].(bool )
154
+ if ! ok || ! scopeCheck {
155
+ return ForbiddenWithInternal (xerrors .Errorf ("policy disallows request" ), input , results )
156
+ }
157
+
158
+ // This is purely defensive programming. The two above checks already
159
+ // check for 'true' expressions. This is just a sanity check to make
160
+ // sure we don't add non-boolean expressions to our query.
161
+ // This is super cheap to do, and just adds in some extra safety for
162
+ // programmer error.
163
+ for _ , exp := range results [0 ].Expressions {
164
+ if b , ok := exp .Value .(bool ); ! ok || ! b {
165
+ return ForbiddenWithInternal (xerrors .Errorf ("policy disallows request" ), input , results )
166
+ }
167
+ }
168
+ return nil
169
+ }
170
+ return ForbiddenWithInternal (xerrors .Errorf ("policy disallows request" ), input , results )
148
171
}
149
172
150
173
// Prepare will partially execute the rego policy leaving the object fields unknown (except for the type).
151
174
// This will vastly speed up performance if batch authorization on the same type of objects is needed.
152
- func (RegoAuthorizer ) Prepare (ctx context.Context , subjectID string , roles []Role , scope Scope , action Action , objectType string ) (* PartialAuthorizer , error ) {
175
+ func (RegoAuthorizer ) Prepare (ctx context.Context , subjectID string , roles []Role , scope Role , action Action , objectType string ) (* PartialAuthorizer , error ) {
153
176
ctx , span := tracing .StartSpan (ctx )
154
177
defer span .End ()
155
178
@@ -170,5 +193,10 @@ func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string,
170
193
return nil , err
171
194
}
172
195
173
- return a .Prepare (ctx , subjectID , roles , scope , action , objectType )
196
+ scopeRole , err := ScopeRole (scope )
197
+ if err != nil {
198
+ return nil , err
199
+ }
200
+
201
+ return a .Prepare (ctx , subjectID , roles , scopeRole , action , objectType )
174
202
}
0 commit comments