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  }