github.com/vmware/govmomi@v0.51.0/simulator/session_manager.go (about) 1 // © Broadcom. All Rights Reserved. 2 // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. 3 // SPDX-License-Identifier: Apache-2.0 4 5 package simulator 6 7 import ( 8 "context" 9 "crypto/tls" 10 "encoding/base64" 11 "fmt" 12 "net/http" 13 "net/url" 14 "os" 15 "reflect" 16 "strings" 17 "sync" 18 "time" 19 20 "github.com/google/uuid" 21 22 "github.com/vmware/govmomi/session" 23 "github.com/vmware/govmomi/vim25" 24 "github.com/vmware/govmomi/vim25/methods" 25 "github.com/vmware/govmomi/vim25/mo" 26 "github.com/vmware/govmomi/vim25/soap" 27 "github.com/vmware/govmomi/vim25/types" 28 ) 29 30 type SessionManager struct { 31 mo.SessionManager 32 nopLocker 33 34 ServiceHostName string 35 TLS func() *tls.Config 36 ValidLogin func(*types.Login) bool 37 38 sessions map[string]Session 39 } 40 41 func (m *SessionManager) init(*Registry) { 42 m.sessions = make(map[string]Session) 43 } 44 45 var ( 46 sessionMutex sync.Mutex 47 48 // secureCookies enables Set-Cookie.Secure=true 49 // We can't do this by default as simulator.Service defaults to no TLS by default and 50 // Go's cookiejar does not send Secure cookies unless the URL scheme is https. 51 secureCookies = os.Getenv("VCSIM_SECURE_COOKIES") == "true" 52 ) 53 54 func createSession(ctx *Context, name string, locale string) types.UserSession { 55 now := time.Now().UTC() 56 57 if locale == "" { 58 locale = session.Locale 59 } 60 61 session := Session{ 62 UserSession: types.UserSession{ 63 Key: uuid.New().String(), 64 UserName: name, 65 FullName: name, 66 LoginTime: now, 67 LastActiveTime: now, 68 Locale: locale, 69 MessageLocale: locale, 70 ExtensionSession: types.NewBool(false), 71 }, 72 Registry: NewRegistry(), 73 Map: ctx.Map, 74 } 75 76 ctx.SetSession(session, true) 77 78 return ctx.Session.UserSession 79 } 80 81 func (m *SessionManager) getSession(id string) (Session, bool) { 82 sessionMutex.Lock() 83 defer sessionMutex.Unlock() 84 s, ok := m.sessions[id] 85 return s, ok 86 } 87 88 func (m *SessionManager) findSession(user string) (Session, bool) { 89 sessionMutex.Lock() 90 defer sessionMutex.Unlock() 91 for _, session := range m.sessions { 92 if session.UserName == user { 93 return session, true 94 } 95 } 96 return Session{}, false 97 } 98 99 func (m *SessionManager) delSession(id string) { 100 sessionMutex.Lock() 101 defer sessionMutex.Unlock() 102 delete(m.sessions, id) 103 } 104 105 func (m *SessionManager) putSession(s Session) { 106 sessionMutex.Lock() 107 defer sessionMutex.Unlock() 108 m.sessions[s.Key] = s 109 } 110 111 func (s *SessionManager) Authenticate(u url.URL, req *types.Login) bool { 112 if u.User == nil || u.User == DefaultLogin { 113 return req.UserName != "" && req.Password != "" 114 } 115 116 if s.ValidLogin != nil { 117 return s.ValidLogin(req) 118 } 119 120 pass, _ := u.User.Password() 121 return req.UserName == u.User.Username() && req.Password == pass 122 } 123 124 func (s *SessionManager) TLSCert() string { 125 if s.TLS == nil { 126 return "" 127 } 128 return base64.StdEncoding.EncodeToString(s.TLS().Certificates[0].Certificate[0]) 129 } 130 131 func (s *SessionManager) Login(ctx *Context, req *types.Login) soap.HasFault { 132 body := new(methods.LoginBody) 133 134 if ctx.Session == nil && s.Authenticate(*ctx.svc.Listen, req) { 135 body.Res = &types.LoginResponse{ 136 Returnval: createSession(ctx, req.UserName, req.Locale), 137 } 138 } else { 139 body.Fault_ = invalidLogin 140 } 141 142 return body 143 } 144 145 func (s *SessionManager) LoginExtensionByCertificate(ctx *Context, req *types.LoginExtensionByCertificate) soap.HasFault { 146 body := new(methods.LoginExtensionByCertificateBody) 147 148 if ctx.req.TLS == nil || len(ctx.req.TLS.PeerCertificates) == 0 { 149 body.Fault_ = Fault("", new(types.NoClientCertificate)) 150 return body 151 } 152 153 if req.ExtensionKey == "" || ctx.Session != nil { 154 body.Fault_ = invalidLogin 155 } else { 156 body.Res = &types.LoginExtensionByCertificateResponse{ 157 Returnval: createSession(ctx, req.ExtensionKey, req.Locale), 158 } 159 } 160 161 return body 162 } 163 164 func (s *SessionManager) LoginByToken(ctx *Context, req *types.LoginByToken) soap.HasFault { 165 body := new(methods.LoginByTokenBody) 166 167 if ctx.Session != nil { 168 body.Fault_ = invalidLogin 169 } else { 170 var subject struct { 171 ID string `xml:"Assertion>Subject>NameID"` 172 } 173 174 if s, ok := ctx.Header.Security.(*Element); ok { 175 _ = s.Decode(&subject) 176 } 177 178 if subject.ID == "" { 179 body.Fault_ = invalidLogin 180 return body 181 } 182 183 body.Res = &types.LoginByTokenResponse{ 184 Returnval: createSession(ctx, subject.ID, req.Locale), 185 } 186 } 187 188 return body 189 } 190 191 func (s *SessionManager) Logout(ctx *Context, _ *types.Logout) soap.HasFault { 192 session := ctx.Session 193 s.delSession(session.Key) 194 pc := ctx.Map.content().PropertyCollector 195 196 ctx.Session.Registry.m.Lock() 197 defer ctx.Session.Registry.m.Unlock() 198 199 for ref, obj := range ctx.Session.Registry.objects { 200 if ref == pc { 201 continue // don't unregister the PropertyCollector singleton 202 } 203 if _, ok := obj.(RegisterObject); ok { 204 ctx.Map.Remove(ctx, ref) // Remove RegisterObject handlers 205 } 206 } 207 208 ctx.postEvent(&types.UserLogoutSessionEvent{ 209 IpAddress: session.IpAddress, 210 UserAgent: session.UserAgent, 211 SessionId: session.Key, 212 LoginTime: &session.LoginTime, 213 }) 214 215 return &methods.LogoutBody{Res: new(types.LogoutResponse)} 216 } 217 218 func (s *SessionManager) TerminateSession(ctx *Context, req *types.TerminateSession) soap.HasFault { 219 body := new(methods.TerminateSessionBody) 220 221 for _, id := range req.SessionId { 222 if id == ctx.Session.Key { 223 body.Fault_ = Fault("", new(types.InvalidArgument)) 224 return body 225 } 226 if _, ok := s.getSession(id); !ok { 227 body.Fault_ = Fault("", new(types.NotFound)) 228 return body 229 } 230 s.delSession(id) 231 } 232 233 body.Res = new(types.TerminateSessionResponse) 234 return body 235 } 236 237 func (s *SessionManager) SessionIsActive(ctx *Context, req *types.SessionIsActive) soap.HasFault { 238 body := new(methods.SessionIsActiveBody) 239 240 if ctx.Map.IsESX() { 241 body.Fault_ = Fault("", new(types.NotImplemented)) 242 return body 243 } 244 245 body.Res = new(types.SessionIsActiveResponse) 246 247 if session, exists := s.getSession(req.SessionID); exists { 248 body.Res.Returnval = session.UserName == req.UserName 249 } 250 251 return body 252 } 253 254 func (s *SessionManager) AcquireCloneTicket(ctx *Context, _ *types.AcquireCloneTicket) soap.HasFault { 255 session := *ctx.Session 256 session.Key = uuid.New().String() 257 s.putSession(session) 258 259 return &methods.AcquireCloneTicketBody{ 260 Res: &types.AcquireCloneTicketResponse{ 261 Returnval: session.Key, 262 }, 263 } 264 } 265 266 func (s *SessionManager) CloneSession(ctx *Context, ticket *types.CloneSession) soap.HasFault { 267 body := new(methods.CloneSessionBody) 268 269 session, exists := s.getSession(ticket.CloneTicket) 270 271 if exists { 272 s.delSession(ticket.CloneTicket) // A clone ticket can only be used once 273 session.Key = uuid.New().String() 274 ctx.SetSession(session, true) 275 276 body.Res = &types.CloneSessionResponse{ 277 Returnval: session.UserSession, 278 } 279 } else { 280 body.Fault_ = invalidLogin 281 } 282 283 return body 284 } 285 286 func (s *SessionManager) ImpersonateUser(ctx *Context, req *types.ImpersonateUser) soap.HasFault { 287 body := new(methods.ImpersonateUserBody) 288 289 session, exists := s.findSession(req.UserName) 290 291 if exists { 292 session.Key = uuid.New().String() 293 ctx.SetSession(session, true) 294 295 body.Res = &types.ImpersonateUserResponse{ 296 Returnval: session.UserSession, 297 } 298 } else { 299 body.Fault_ = invalidLogin 300 } 301 302 return body 303 } 304 305 func (s *SessionManager) AcquireGenericServiceTicket(ticket *types.AcquireGenericServiceTicket) soap.HasFault { 306 return &methods.AcquireGenericServiceTicketBody{ 307 Res: &types.AcquireGenericServiceTicketResponse{ 308 Returnval: types.SessionManagerGenericServiceTicket{ 309 Id: uuid.New().String(), 310 HostName: s.ServiceHostName, 311 }, 312 }, 313 } 314 } 315 316 var invalidLogin = Fault("Login failure", new(types.InvalidLogin)) 317 318 // Context provides per-request Session management. 319 type Context struct { 320 req *http.Request 321 res http.ResponseWriter 322 svc *Service 323 324 context.Context 325 Session *Session 326 Header soap.Header 327 Caller *types.ManagedObjectReference 328 Map *Registry 329 } 330 331 func SOAPCookie(ctx *Context) string { 332 if cookie := ctx.Header.Cookie; cookie != nil { 333 return cookie.Value 334 } 335 return "" 336 } 337 338 func HTTPCookie(ctx *Context) string { 339 if cookie, err := ctx.req.Cookie(soap.SessionCookieName); err == nil { 340 return cookie.Value 341 } 342 return "" 343 } 344 345 func (c *Context) sessionManager() *SessionManager { 346 return c.svc.sdk[vim25.Path].SessionManager() 347 } 348 349 // mapSession maps an HTTP cookie to a Session. 350 func (c *Context) mapSession() { 351 cookie := c.Map.Cookie 352 if cookie == nil { 353 cookie = HTTPCookie 354 } 355 356 if val, ok := c.sessionManager().getSession(cookie(c)); ok { 357 c.SetSession(val, false) 358 } 359 } 360 361 func (m *SessionManager) expiredSession(id string, now time.Time, timeout time.Duration) bool { 362 expired := true 363 364 s, ok := m.getSession(id) 365 if ok { 366 expired = now.Sub(s.LastActiveTime) > timeout 367 if expired { 368 m.delSession(id) 369 } 370 } 371 372 return expired 373 } 374 375 // SessionIdleWatch starts a goroutine that calls func expired() at timeout intervals. 376 // The goroutine exits if the func returns true. 377 func SessionIdleWatch(ctx *Context, id string, expired func(string, time.Time, time.Duration) bool) { 378 opt := ctx.Map.OptionManager().find("config.vmacore.soap.sessionTimeout") 379 if opt == nil { 380 return 381 } 382 383 timeout, err := time.ParseDuration(opt.Value.(string)) 384 if err != nil { 385 panic(err) 386 } 387 388 go func() { 389 for t := time.NewTimer(timeout); ; { 390 select { 391 case <-ctx.Done(): 392 return 393 case now := <-t.C: 394 if expired(id, now, timeout) { 395 return 396 } 397 t.Reset(timeout) 398 } 399 } 400 }() 401 } 402 403 // SetSession should be called after successful authentication. 404 func (c *Context) SetSession(session Session, login bool) { 405 session.UserAgent = c.req.UserAgent() 406 session.IpAddress = strings.Split(c.req.RemoteAddr, ":")[0] 407 session.LastActiveTime = time.Now() 408 session.CallCount++ 409 410 m := c.sessionManager() 411 m.putSession(session) 412 c.Session = &session 413 414 if login { 415 http.SetCookie(c.res, &http.Cookie{ 416 Name: soap.SessionCookieName, 417 Value: session.Key, 418 Secure: secureCookies, 419 HttpOnly: true, 420 }) 421 422 c.postEvent(&types.UserLoginSessionEvent{ 423 SessionId: session.Key, 424 IpAddress: session.IpAddress, 425 UserAgent: session.UserAgent, 426 Locale: session.Locale, 427 }) 428 429 SessionIdleWatch(c, session.Key, m.expiredSession) 430 } 431 } 432 433 // For returns a Context with Registry Map for the given path. 434 // This is intended for calling into other namespaces internally, 435 // such as vslm simulator methods calling vim25 methods for example. 436 func (c *Context) For(path string) *Context { 437 clone := *c 438 clone.Map = c.svc.sdk[path] 439 return &clone 440 } 441 442 // WithLock holds a lock for the given object while the given function is run. 443 // It will skip locking if this context already holds the given object's lock. 444 func (c *Context) WithLock(obj mo.Reference, f func()) { 445 // TODO: This is not always going to be correct. An object should 446 // really be locked by the registry that "owns it", which is not always 447 // Map. This function will need to take the Registry as an additional 448 // argument to accomplish this. 449 // Basic mutex locking will work even if obj doesn't belong to Map, but 450 // if obj implements sync.Locker, that custom locking will not be used. 451 c.Map.WithLock(c, obj, f) 452 } 453 454 func (c *Context) Update(obj mo.Reference, changes []types.PropertyChange) { 455 c.Map.Update(c, obj, changes) 456 } 457 458 // postEvent wraps EventManager.PostEvent for internal use, with a lock on the EventManager. 459 func (c *Context) postEvent(events ...types.BaseEvent) { 460 m := c.Map.EventManager() 461 c.WithLock(m, func() { 462 for _, event := range events { 463 m.PostEvent(c, &types.PostEvent{EventToPost: event}) 464 } 465 }) 466 } 467 468 // Session combines a UserSession and a Registry for per-session managed objects. 469 type Session struct { 470 types.UserSession 471 *Registry 472 Map *Registry 473 } 474 475 func (s *Session) setReference(item mo.Reference) { 476 ref := item.Reference() 477 if ref.Value == "" { 478 ref.Value = fmt.Sprintf("session[%s]%s", s.Key, uuid.New()) 479 } 480 if ref.Type == "" { 481 ref.Type = typeName(item) 482 } 483 s.Registry.setReference(item, ref) 484 } 485 486 // Put wraps Registry.Put, setting the moref value to include the session key. 487 func (s *Session) Put(item mo.Reference) mo.Reference { 488 s.setReference(item) 489 return s.Registry.Put(item) 490 } 491 492 // Get wraps Registry.Get, session-izing singleton objects such as SessionManager and the root PropertyCollector. 493 func (s *Session) Get(ref types.ManagedObjectReference) mo.Reference { 494 obj := s.Registry.Get(ref) 495 if obj != nil { 496 return obj 497 } 498 499 // Return a session "view" of certain singleton objects 500 switch ref.Type { 501 case "SessionManager": 502 // Clone SessionManager so the PropertyCollector can properly report CurrentSession 503 m := *s.Map.SessionManager() 504 m.CurrentSession = &s.UserSession 505 506 // TODO: we could maintain SessionList as part of the SessionManager singleton 507 sessionMutex.Lock() 508 for _, session := range m.sessions { 509 m.SessionList = append(m.SessionList, session.UserSession) 510 } 511 sessionMutex.Unlock() 512 513 return &m 514 case "PropertyCollector": 515 if ref == s.Map.content().PropertyCollector { 516 // Per-session instance of the PropertyCollector singleton. 517 // Using reflection here as PropertyCollector might be wrapped with a custom type. 518 obj = s.Map.Get(ref) 519 pc := reflect.New(reflect.TypeOf(obj).Elem()) 520 obj = pc.Interface().(mo.Reference) 521 s.Registry.setReference(obj, ref) 522 return s.Put(obj) 523 } 524 } 525 526 return s.Map.Get(ref) 527 }