-
Notifications
You must be signed in to change notification settings - Fork 1
/
enforcer.go
314 lines (289 loc) · 9.69 KB
/
enforcer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
package scitokens
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"github.com/lestrrat-go/jwx/jwt"
"github.com/scitokens/scitokens-go/issuer"
)
// Enforcer verifies that SciTokens https://scitokens.org are valid, from a
// certain issuer, and that they allow the requested resource.
type Enforcer interface {
AddIssuer(context.Context, string) error
RequireAudience(string) error
RequireScope(Scope) error
RequireGroup(string) error
RequireValidator(Validator) error
Validate(SciToken, ...Validator) error
ValidateToken([]byte, ...Validator) (SciToken, error)
ValidateTokenString(string, ...Validator) (SciToken, error)
ValidateTokenReader(io.Reader, ...Validator) (SciToken, error)
ValidateTokenEnvironment(...Validator) (SciToken, error)
ValidateTokenForm(url.Values, string, ...Validator) (SciToken, error)
ValidateTokenHeader(http.Header, string, ...Validator) (SciToken, error)
ValidateTokenRequest(*http.Request, ...Validator) (SciToken, error)
}
type issuerSet map[string]bool
func newIssuerSet(issuers ...string) issuerSet {
s := make(map[string]bool)
for _, i := range issuers {
s[i] = true
}
return s
}
func (s issuerSet) add(issuer string) {
s[issuer] = true
}
func (s issuerSet) has(issuer string) bool {
_, ok := s[issuer]
return ok
}
type stdEnforcer struct {
issuers issuerSet
keys issuer.KeyProvider
validators []Validator
}
// NewEnforcer initializes a new enforcer for validating SciTokens from the
// provided issuer(s). Keys are fetched on-demand when a token is verified. Use
// NewEnforcerDaemon() for long-running processes.
func NewEnforcer(issuers ...string) (Enforcer, error) {
if len(issuers) == 0 {
return nil, errors.New("must accept at least one issuer")
}
e := &stdEnforcer{
issuers: newIssuerSet(issuers...),
keys: issuer.NewKeyFetcher(issuers...),
validators: make([]Validator, 0),
}
return e, nil
}
// NewEnforcerDaemon initializes a new enforcer for validating SciTokens from
// the provided issuer(s), caching and refreshing keys periodically. The context
// object should be cancelled when the process is done with the enforcer.
func NewEnforcerDaemon(ctx context.Context, issuers ...string) (Enforcer, error) {
if len(issuers) == 0 {
return nil, errors.New("must accept at least one issuer")
}
e := &stdEnforcer{
issuers: newIssuerSet(issuers...),
keys: issuer.NewKeyManager(ctx),
validators: make([]Validator, 0),
}
for _, i := range issuers {
if err := e.AddIssuer(context.Background(), i); err != nil {
return nil, err
}
}
return e, nil
}
// AddIssuer adds an accepted issuer and fetches its signing keys.
func (e *stdEnforcer) AddIssuer(ctx context.Context, issuer string) error {
err := e.keys.AddIssuer(ctx, issuer)
if err != nil {
return fmt.Errorf("failed to fetch keyset for issuer %s: %w", issuer, err)
}
e.issuers.add(issuer)
return nil
}
// RequireAudience adds aud to audiences to validate.
func (e *stdEnforcer) RequireAudience(aud string) error {
return e.RequireValidator(WithAudience(aud))
}
// RequireScope adds s to scopes to validate.
func (e *stdEnforcer) RequireScope(s Scope) error {
return e.RequireValidator(WithScope(s))
}
// RequireGroup adds group to the WLCG groups to validate. The leading slash is
// optional.
func (e *stdEnforcer) RequireGroup(group string) error {
return e.RequireValidator(WithGroup(group))
}
// RequireValidator adds a general constraint.
func (e *stdEnforcer) RequireValidator(v Validator) error {
e.validators = append(e.validators, v)
return nil
}
func (e *stdEnforcer) parseOptions() []jwt.ParseOption {
return []jwt.ParseOption{
jwt.WithKeySetProvider(e.keys),
jwt.InferAlgorithmFromKey(true),
}
}
// ValidateToken parses and validates that the SciToken in the provided byte
// slice is valid and meets all basic constraints imposed by the Enforcer along
// with the extra optional constraints, which can be defined using WithScope,
// WithGroup, etc.
//
// The token is returned and can be re-validated with Validate().
func (e *stdEnforcer) ValidateToken(token []byte, constraints ...Validator) (SciToken, error) {
t, err := jwt.Parse(token, e.parseOptions()...)
if err != nil {
return nil, err
}
st, err := NewSciToken(t)
if err != nil {
return nil, err
}
return st, e.Validate(st, constraints...)
}
// ValidateTokenString parses and validates that the SciToken in the provided
// string is valid and meets all constraints imposed by the Enforcer.
// See ValidateToken.
func (e *stdEnforcer) ValidateTokenString(tokenstring string, constraints ...Validator) (SciToken, error) {
t, err := jwt.ParseString(tokenstring, e.parseOptions()...)
if err != nil {
return nil, err
}
st, err := NewSciToken(t)
if err != nil {
return nil, err
}
return st, e.Validate(st, constraints...)
}
// ValidateTokenReader parses and validates that the SciToken read from the
// provided io.Reader is valid and meets all constraints imposed by the
// Enforcer. See ValidateToken.
func (e *stdEnforcer) ValidateTokenReader(r io.Reader, constraints ...Validator) (SciToken, error) {
t, err := jwt.ParseReader(r, e.parseOptions()...)
if err != nil {
return nil, err
}
st, err := NewSciToken(t)
if err != nil {
return nil, err
}
return st, e.Validate(st, constraints...)
}
// ValidateTokenForm parses and validates that the SciToken read from the
// provided url value is valid and meets all constraints imposed by the
// Enforcer. See ValidateToken.
func (e *stdEnforcer) ValidateTokenForm(values url.Values, name string, constraints ...Validator) (SciToken, error) {
t, err := jwt.ParseForm(values, name, e.parseOptions()...)
if err != nil {
return nil, err
}
st, err := NewSciToken(t)
if err != nil {
return nil, err
}
return st, e.Validate(st, constraints...)
}
// ValidateTokenHeader parses and validates that the SciToken read from the
// provided http.Header is valid and meets all constraints imposed by the
// Enforcer. See ValidateToken.
func (e *stdEnforcer) ValidateTokenHeader(hdr http.Header, name string, constraints ...Validator) (SciToken, error) {
t, err := jwt.ParseHeader(hdr, name, e.parseOptions()...)
if err != nil {
return nil, err
}
st, err := NewSciToken(t)
if err != nil {
return nil, err
}
return st, e.Validate(st, constraints...)
}
// ValidateTokenRequest parses and validates that the SciToken read from the
// provided http.Request is valid and meets all constraints imposed by the
// Enforcer. See ValidateToken.
func (e *stdEnforcer) ValidateTokenRequest(r *http.Request, constraints ...Validator) (SciToken, error) {
t, err := jwt.ParseRequest(r, e.parseOptions()...)
if err != nil {
return nil, err
}
st, err := NewSciToken(t)
if err != nil {
return nil, err
}
return st, e.Validate(st, constraints...)
}
// ValidateTokenEnvironment searches for a SciToken in the execution
// environment, per the following rules (https://doi.org/10.5281/zenodo.3937438),
// then parses and validates it meets all constraints imposed by the Enforcer:
//
// 1. If the BEARER_TOKEN environment variable is set, then the value is taken
// to be the token contents.
//
// 2. If the BEARER_TOKEN_FILE environment variable is set, then its value is
// interpreted as a filename. The contents of the specified file are taken to
// be the token contents.
//
// 3. If the XDG_RUNTIME_DIR environment variable is set, then take the token
// from the contents of $XDG_RUNTIME_DIR/bt_u$ID.
//
// 4. Otherwise, take the token from /$TMP/bt_u$ID, where $TMP is TMPDIR if
// set, or /tmp or other OS-appropriate temp directory (see os.Tempdir())
//
// If no token is found in any of these locations, a TokenNotFoundError is
// returned.
func (e *stdEnforcer) ValidateTokenEnvironment(constraints ...Validator) (SciToken, error) {
var data []byte
if ts, ok := os.LookupEnv("BEARER_TOKEN"); ok {
data = []byte(ts)
} else {
fname := tokenFilename()
if fname == "" {
return nil, TokenNotFoundError
}
var err error
data, err = os.ReadFile(fname)
if err != nil {
return nil, fmt.Errorf("unable to read token from file %s: %w", fname, err)
}
}
return e.ValidateToken(data, constraints...)
}
func tokenFilename() string {
if f, ok := os.LookupEnv("BEARER_TOKEN_FILE"); ok {
return f
}
if d, ok := os.LookupEnv("XDG_RUNTIME_DIR"); ok {
f := filepath.Join(d, fmt.Sprintf("/bt_u%d", os.Getuid()))
if _, err := os.Stat(f); err == nil {
return f
}
}
f := filepath.Join(os.TempDir(), fmt.Sprintf("/bt_u%d", os.Getuid()))
if _, err := os.Stat(f); err == nil {
return f
}
return ""
}
// Validate checks that the SciToken is valid and meets all constraints imposed
// by the Enforcer, namely:
//
// * the issuer is accepted (via AddIssuer) and the token was signed by it
//
// * all audiences added by RequireAudience or one of the recognized "any"
// audiences are present in the aud claim.
//
// * all scopes added by RequireScope are present in the scope claim
//
// * all groups added by RequireGroup are present in the wlcg.groups claim
//
// This can be called multiple times, e.g. to test the token against different
// scopes.
func (e *stdEnforcer) Validate(t SciToken, constraints ...Validator) error {
// validate standard claims
if !e.issuers.has(t.Issuer()) {
return &TokenValidationError{issuer.UntrustedIssuerError}
}
opts := make([]jwt.ValidateOption, len(e.validators)+len(constraints))
for i, v := range e.validators {
opts[i] = jwt.WithValidator(v)
}
for i, v := range constraints {
opts[i+len(e.validators)] = jwt.WithValidator(v)
}
if err := jwt.Validate(t, opts...); err != nil {
// It doesn't appear that Validate can return a non-validation error
// (i.e. some internal error), and there's no way to differentiate if so
// (besides error message parsing, bleh).
return &TokenValidationError{err}
}
return nil
}