-
Notifications
You must be signed in to change notification settings - Fork 175
/
prxauth.go
301 lines (274 loc) · 7.9 KB
/
prxauth.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
// Package ais provides core functionality for the AIStore object storage.
/*
* Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
*/
package ais
import (
"fmt"
"net/http"
"os"
"sync"
"time"
"github.com/NVIDIA/aistore/api/apc"
"github.com/NVIDIA/aistore/api/authn"
"github.com/NVIDIA/aistore/api/env"
"github.com/NVIDIA/aistore/cmd/authn/tok"
"github.com/NVIDIA/aistore/cmn"
"github.com/NVIDIA/aistore/cmn/cos"
"github.com/NVIDIA/aistore/cmn/debug"
"github.com/NVIDIA/aistore/cmn/nlog"
"github.com/NVIDIA/aistore/core/meta"
"github.com/NVIDIA/aistore/memsys"
)
type (
tokenList authn.TokenList // token strings
tkList map[string]*tok.Token // tk structs
authManager struct {
// cache of decrypted tokens
tkList tkList
// list of invalid tokens(revoked or of deleted users)
// Authn sends these tokens to primary for broadcasting
revokedTokens map[string]bool
version int64
// signing key secret
secret string
// lock
sync.Mutex
}
)
/////////////////
// authManager //
/////////////////
func newAuthManager(config *cmn.Config) *authManager {
return &authManager{
tkList: make(tkList),
revokedTokens: make(map[string]bool), // TODO: preallocate
version: 1,
secret: cos.Right(config.Auth.Secret, os.Getenv(env.AuthN.SecretKey)), // environment override
}
}
// Add tokens to the list of invalid ones and clean up the list from expired tokens.
func (a *authManager) updateRevokedList(newRevoked *tokenList) (allRevoked *tokenList) {
a.Lock()
defer a.Unlock()
switch {
case newRevoked.Version == 0: // Manually revoked tokens
a.version
case newRevoked.Version > a.version:
a.version = newRevoked.Version
default:
nlog.Errorf("Current token list v%d is greater than received v%d", a.version, newRevoked.Version)
return
}
// Add new revoked tokens and remove them from the valid token list.
for _, token := range newRevoked.Tokens {
a.revokedTokens[token] = true
delete(a.tkList, token)
}
allRevoked = &tokenList{
Tokens: make([]string, 0, len(a.revokedTokens)),
Version: a.version,
}
// Clean up expired tokens from the revoked list.
now := time.Now()
for token := range a.revokedTokens {
tk, err := tok.DecryptToken(token, a.secret)
debug.AssertNoErr(err)
if tk.Expires.Before(now) {
delete(a.revokedTokens, token)
} else {
allRevoked.Tokens = append(allRevoked.Tokens, token)
}
}
if len(allRevoked.Tokens) == 0 {
allRevoked = nil
}
return
}
func (a *authManager) revokedTokenList() (allRevoked *tokenList) {
a.Lock()
l := len(a.revokedTokens)
if l == 0 {
a.Unlock()
return
}
allRevoked = &tokenList{Tokens: make([]string, 0, l), Version: a.version}
for token := range a.revokedTokens {
allRevoked.Tokens = append(allRevoked.Tokens, token)
}
a.Unlock()
return
}
// Checks if a token is valid:
// - must not be revoked one
// - must not be expired
// - must have all mandatory fields: userID, creds, issued, expires
//
// Returns decrypted token information if it is valid
func (a *authManager) validateToken(token string) (tk *tok.Token, err error) {
a.Lock()
if _, ok := a.revokedTokens[token]; ok {
tk, err = nil, fmt.Errorf("%v: %s", tok.ErrTokenRevoked, tk)
} else {
tk, err = a.validateAddRm(token, time.Now())
}
a.Unlock()
return
}
// Decrypts and validates token. Adds it to authManager.token if not found. Removes if expired.
// Must be called under lock.
func (a *authManager) validateAddRm(token string, now time.Time) (*tok.Token, error) {
tk, ok := a.tkList[token]
if !ok || tk == nil {
var err error
if tk, err = tok.DecryptToken(token, a.secret); err != nil {
nlog.Errorln(err)
return nil, tok.ErrInvalidToken
}
a.tkList[token] = tk
}
if tk.Expires.Before(now) {
delete(a.tkList, token)
return nil, fmt.Errorf("%v: %s", tok.ErrTokenExpired, tk)
}
return tk, nil
}
///////////////
// tokenList //
///////////////
// interface guard
var _ revs = (*tokenList)(nil)
func (*tokenList) tag() string { return revsTokenTag }
func (t *tokenList) version() int64 { return t.Version } // no versioning: receivers keep adding tokens to their lists
func (*tokenList) uuid() string { return "" } // TODO: add
func (t *tokenList) marshal() []byte { return cos.MustMarshal(t) }
func (t *tokenList) jit(_ *proxy) revs { return t }
func (*tokenList) sgl() *memsys.SGL { return nil }
func (t *tokenList) String() string { return fmt.Sprintf("TokenList v%d", t.Version) }
//
// proxy cont-ed
//
// [METHOD] /v1/tokens
func (p *proxy) tokenHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
p.validateSecret(w, r)
case http.MethodDelete:
p.delToken(w, r)
default:
cmn.WriteErr405(w, r, http.MethodDelete)
}
}
func (p *proxy) validateSecret(w http.ResponseWriter, r *http.Request) {
if _, err := p.parseURL(w, r, apc.URLPathTokens.L, 0, false); err != nil {
return
}
cksum := cos.NewCksumHash(cos.ChecksumSHA256)
cksum.H.Write([]byte(p.authn.secret))
cksum.Finalize()
cluConf := &authn.ServerConf{}
if err := cmn.ReadJSON(w, r, cluConf); err != nil {
return
}
if cksum.Val() != cluConf.Secret {
p.writeErrf(w, r, "%s: invalid secret sha256(%q)", p, cos.SHead(cluConf.Secret))
}
}
func (p *proxy) delToken(w http.ResponseWriter, r *http.Request) {
if _, err := p.parseURL(w, r, apc.URLPathTokens.L, 0, false); err != nil {
return
}
if p.forwardCP(w, r, nil, "revoke token") {
return
}
tokenList := &tokenList{}
if err := cmn.ReadJSON(w, r, tokenList); err != nil {
return
}
allRevoked := p.authn.updateRevokedList(tokenList)
if allRevoked != nil && p.owner.smap.get().isPrimary(p.si) {
msg := p.newAmsgStr(apc.ActNewPrimary, nil)
_ = p.metasyncer.sync(revsPair{allRevoked, msg})
}
}
// Validates a token from the request header
func (p *proxy) validateToken(hdr http.Header) (*tok.Token, error) {
token, err := tok.ExtractToken(hdr)
if err != nil {
return nil, err
}
tk, err := p.authn.validateToken(token)
if err != nil {
nlog.Errorf("invalid token: %v", err)
return nil, err
}
return tk, nil
}
// When AuthN is on, accessing a bucket requires two permissions:
// - access to the bucket is granted to a user
// - bucket ACL allows the required operation
// Exception: a superuser can always PATCH the bucket/Set ACL
//
// If AuthN is off, only bucket permissions are checked.
//
// Exceptions:
// - read-only access to a bucket is always granted
// - PATCH cannot be forbidden
func (p *proxy) checkAccess(w http.ResponseWriter, r *http.Request, bck *meta.Bck, ace apc.AccessAttrs) (err error) {
if err = p.access(r.Header, bck, ace); err != nil {
p.writeErr(w, r, err, aceErrToCode(err))
}
return
}
func aceErrToCode(err error) (status int) {
switch err {
case nil:
case tok.ErrNoToken, tok.ErrInvalidToken:
status = http.StatusUnauthorized
default:
status = http.StatusForbidden
}
return status
}
func (p *proxy) access(hdr http.Header, bck *meta.Bck, ace apc.AccessAttrs) (err error) {
var (
tk *tok.Token
bucket *cmn.Bck
)
if p.checkIntraCall(hdr, false /*from primary*/) == nil {
return nil
}
if cmn.Rom.AuthEnabled() { // config.Auth.Enabled
tk, err = p.validateToken(hdr)
if err != nil {
// NOTE: making exception to allow 3rd party clients read remote ht://bucket
if err == tok.ErrNoToken && bck != nil && bck.IsHT() {
err = nil
}
return err
}
uid := p.owner.smap.Get().UUID
if bck != nil {
bucket = bck.Bucket()
}
if err := tk.CheckPermissions(uid, bucket, ace); err != nil {
return err
}
}
if bck == nil {
// cluster ACL: create/list buckets, node management, etc.
return nil
}
// bucket access conventions:
// - without AuthN: read-only access, PATCH, and ACL
// - with AuthN: superuser can PATCH and change ACL
if !cmn.Rom.AuthEnabled() {
ace &^= (apc.AcePATCH | apc.AceBckSetACL | apc.AccessRO)
} else if tk.IsAdmin {
ace &^= (apc.AcePATCH | apc.AceBckSetACL)
}
if ace == 0 {
return nil
}
return bck.Allow(ace)
}