-
Notifications
You must be signed in to change notification settings - Fork 2
/
scope.go
374 lines (306 loc) · 10 KB
/
scope.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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
package auth
import (
"sync"
"time"
log "github.com/cihub/seelog"
"github.com/hailo-platform/H2O/protobuf/proto"
"github.com/hailo-platform/H2O/platform/client"
"github.com/hailo-platform/H2O/platform/errors"
"github.com/hailo-platform/H2O/platform/multiclient"
inst "github.com/hailo-platform/H2O/service/instrumentation"
loginproto "github.com/hailo-platform/H2O/login-service/proto"
authproto "github.com/hailo-platform/H2O/login-service/proto/auth"
sessdelproto "github.com/hailo-platform/H2O/login-service/proto/deletesession"
sessreadproto "github.com/hailo-platform/H2O/login-service/proto/readsession"
)
const (
loginService = "com.hailocab.service.login"
readSessionEndpoint = "readsession"
deleteSessionEndpoint = "deletesession"
authEndpoint = "auth"
badCredentialsErrCode = "com.hailocab.service.login.auth.badCredentials"
)
// Scope represents some session witin which we may know about a user who has
// somehow identified themselves to us, or some service that has identified
// itself to us (and we trust)
type Scope interface {
RpcScope(scoper multiclient.Scoper) Scope
Clean() Scope
RecoverSession(sessId string) error
RecoverService(toEndpoint, fromService string) error
Auth(mech, device string, creds map[string]string) error
IsAuth() bool
AuthUser() *User
HasAccess(role string) bool
SignOut(user *User) error
HasTriedAuth() bool
Authorised() bool
SetAuthorised(authorised bool)
}
type realScope struct {
sync.RWMutex
authUser *User // user auth scope
toEndpoint, fromService string // service-to-service auth scope
rpcScoper multiclient.Scoper // the scope we should use when making requests to login service (mainly useful for tracing)
userCache Cacher // userCacher is able to cache sess->token lookups
triedAuth bool // we set to true if/when we attempt to recover session scope
authorised bool // whether the request has been authorised
}
// New mints a new scope
func New() Scope {
return &realScope{
rpcScoper: multiclient.ExplicitScoper(), // a blank scope - client should override
userCache: &memcacheCacher{},
}
}
// RpcScope sets up how we scope RPC requests to the login service
// This is primarily useful so that requests are traced correctly
func (s *realScope) RpcScope(scoper multiclient.Scoper) Scope {
s.Lock()
defer s.Unlock()
s.rpcScoper = scoper
return s
}
// getRpcScope fetches this property, protected by RLock
func (s *realScope) getRpcScope() multiclient.Scoper {
s.RLock()
defer s.RUnlock()
return s.rpcScoper
}
// Clean wipes out any knowledge of who is authenticated within this scope
func (s *realScope) Clean() Scope {
s.Lock()
defer s.Unlock()
s.authUser = nil
s.triedAuth = false
return s
}
// RecoverSession will try to turn a sessId into a valid user/token, if possible
// error will be non-nil if something goes wrong during this process - if we
// can't find any valid user with this sessId that is *not* an error
// If there is an error, the current state of the scope *will not have been changed*
// If there is no error, then the state will be updated, either to the recovered
// user *or* to nil, if no user was recovered
func (s *realScope) RecoverSession(sessId string) error {
t := time.Now()
u, err := s.doRecoverSession(sessId)
instTiming("auth.recoverSession", err, t)
if s.IsAuth() {
inst.Counter(0.01, "auth.recoverSession.recovered", 1)
} else {
inst.Counter(1.0, "auth.recoverSession.badCredentials", 1)
}
s.Lock()
defer s.Unlock()
s.authUser = u
if err == nil {
s.triedAuth = true
}
return err
}
// doRecoverSession is the meat and veg for RecoverSession
func (s *realScope) doRecoverSession(sessId string) (*User, error) {
// Check cache; ignore errors (will have impact on service performance, but not functionality)
queryLogin := false
u, hit, err := s.userCache.Fetch(sessId)
if err != nil {
log.Warnf("[Auth] Error fetching session from cache (will call login service): %v", err)
queryLogin = true
} else if u != nil && u.ExpiryTs.Before(time.Now()) && u.CanAutoRenew() { // Cached token has expired
log.Infof("[Auth] Cache-recovered token has expired (%s); will call login service", u.ExpiryTs.String())
queryLogin = true
} else {
queryLogin = u == nil && !hit
}
if queryLogin {
cl := multiclient.New().DefaultScopeFrom(s.getRpcScope())
rsp := &sessreadproto.Response{}
cl.AddScopedReq(&multiclient.ScopedReq{
Uid: "readsess",
Service: loginService,
Endpoint: readSessionEndpoint,
Req: &sessreadproto.Request{
SessId: proto.String(sessId),
},
Rsp: rsp,
})
if cl.Execute().AnyErrorsIgnoring([]string{errors.ErrorNotFound}, nil) {
err := cl.Succeeded("readsess")
log.Errorf("[Auth] Auth scope recovery error [%s: %s] %v", err.Type(), err.Code(), err.Description())
return nil, err
}
// found a session?
if rsp.GetSessId() == "" && rsp.GetToken() == "" {
log.Debugf("[Auth] Session '%s' not found (not valid) when trying to recover from login service", sessId)
// @todo we could cache this (at least for a short time) to prevent repeated hammering of login service
} else {
u, err = FromSessionToken(rsp.GetSessId(), rsp.GetToken())
if err != nil {
log.Errorf("[Auth] Error getting user from session: %v", err)
} else {
log.Tracef("[Auth] Auth scope - recovered user '%s' from session '%s'", u.Id, rsp.GetSessId())
}
}
// ignore errors; just means we have no user
if u != nil {
s.userCache.Store(u)
}
}
return u, nil
}
// RecoverService will try to add the calling service to our auth scope
// @todo eventually this should crytographically verify the service (which might
// have to change from string)
// NOTE: it's the fromService we don't want to trust since this has come from some
// remote source
func (s *realScope) RecoverService(toEndpoint, fromService string) error {
s.Lock()
defer s.Unlock()
s.toEndpoint = toEndpoint
s.fromService = fromService
return nil
}
// Auth will pass the supplied details onto the login service in an attempt
// to authenticate a brand new session
func (s *realScope) Auth(mech, device string, creds map[string]string) error {
t := time.Now()
u, err := s.doAuth(mech, device, creds)
instTiming("auth.auth", err, t)
if s.IsAuth() {
inst.Counter(0.01, "auth.authenticate.recovered", 1)
} else {
inst.Counter(1.0, "auth.authenticate.badCredentials", 1)
}
s.Lock()
defer s.Unlock()
s.authUser = u
if err == nil || err == BadCredentialsError {
s.triedAuth = true
}
return err
}
func (s *realScope) doAuth(mech, device string, creds map[string]string) (*User, error) {
reqProto := &authproto.Request{
Mech: proto.String(mech),
DeviceType: proto.String(device),
Meta: make([]*loginproto.KeyValue, 0),
}
for k, v := range creds {
switch k {
case "username":
reqProto.Username = proto.String(v)
case "password":
reqProto.Password = proto.String(v)
case "newPassword":
reqProto.NewPassword = proto.String(v)
case "application":
reqProto.Application = proto.String(v)
default:
// Add additional fields to Meta, such as DeviceId, osVersion, appVersion
reqProto.Meta = append(reqProto.Meta, &loginproto.KeyValue{
Key: proto.String(k),
Value: proto.String(v),
})
}
}
cl := multiclient.New().DefaultScopeFrom(s.getRpcScope())
rsp := &authproto.Response{}
cl.AddScopedReq(&multiclient.ScopedReq{
Uid: "auth",
Service: loginService,
Endpoint: authEndpoint,
Req: reqProto,
Rsp: rsp,
Options: client.Options{"retries": 0},
})
if cl.Execute().AnyErrors() {
// specfically map out bad credentials error
err := cl.Succeeded("auth")
if err.Code() == badCredentialsErrCode {
return nil, BadCredentialsError
}
return nil, err
}
// recover this user
u, err := FromSessionToken(rsp.GetSessId(), rsp.GetToken())
if err != nil {
return nil, err
}
if err := s.userCache.Store(u); err != nil {
log.Errorf("[Auth] Error caching session: %v", err)
}
return u, nil
}
// IsAuth tests to see if this scope is currently authenticated
func (s *realScope) IsAuth() bool {
s.RLock()
defer s.RUnlock()
return s.authUser != nil
}
// AuthUser returns the details about the currently auth'd user (if IsAuth)
// or nil (if !IsAuth)
func (s *realScope) AuthUser() *User {
s.RLock()
defer s.RUnlock()
return s.authUser
}
// HasAccess tests if the current authentication scope has access to the given role
// This can be satisfied through either service-to-service authentication OR from a user role
func (s *realScope) HasAccess(role string) bool {
s.RLock()
defer s.RUnlock()
// auth against user
if s.authUser != nil && s.authUser.HasRole(role) {
return true
}
// auth against service
// TODO delete when removing s2s rules
if assume := defaultS2S.assumedRole(s.toEndpoint, s.fromService); assume != "" {
if matchRoleAgainstSet(role, []string{assume}) {
return true
}
}
// check whether the request has been marked as authorised
return s.authUser == nil && s.Authorised()
}
// SignOut destroys the current session so that it cannot be used again
func (s *realScope) SignOut(user *User) error {
cl := multiclient.New().DefaultScopeFrom(s.getRpcScope())
cl.AddScopedReq(&multiclient.ScopedReq{
Uid: "deletesess",
Service: loginService,
Endpoint: deleteSessionEndpoint,
Req: &sessdelproto.Request{
SessId: proto.String(user.SessId),
},
Rsp: &sessdelproto.Response{},
})
if cl.Execute().AnyErrors() {
return cl.Succeeded("deletesess")
}
if err := s.userCache.Purge(user.SessId); err != nil {
log.Errorf("[Auth] Error purging session cache: %v", err)
}
s.Lock()
defer s.Unlock()
s.authUser = nil
s.triedAuth = false
return nil
}
// HasTriedAuth returns whether we have tried to auth a token or session ID
// This is useful to determine "unknown user, who hasn't tried to auth"
func (s *realScope) HasTriedAuth() bool {
s.RLock()
defer s.RUnlock()
return s.triedAuth
}
func (s *realScope) Authorised() bool {
s.RLock()
defer s.RUnlock()
return s.authorised
}
func (s *realScope) SetAuthorised(authorised bool) {
s.Lock()
defer s.Unlock()
s.authorised = authorised
}