github.com/vmware/govmomi@v0.37.2/simulator/session_manager.go (about)

     1  /*
     2  Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package simulator
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/http"
    23  	"net/url"
    24  	"os"
    25  	"reflect"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	"github.com/google/uuid"
    31  
    32  	"github.com/vmware/govmomi/session"
    33  	"github.com/vmware/govmomi/vim25/methods"
    34  	"github.com/vmware/govmomi/vim25/mo"
    35  	"github.com/vmware/govmomi/vim25/soap"
    36  	"github.com/vmware/govmomi/vim25/types"
    37  )
    38  
    39  type SessionManager struct {
    40  	mo.SessionManager
    41  	nopLocker
    42  
    43  	ServiceHostName string
    44  	TLSCert         func() string
    45  	ValidLogin      func(*types.Login) bool
    46  
    47  	sessions map[string]Session
    48  }
    49  
    50  func (m *SessionManager) init(*Registry) {
    51  	m.sessions = make(map[string]Session)
    52  }
    53  
    54  var (
    55  	// SessionIdleTimeout duration used to expire idle sessions
    56  	SessionIdleTimeout time.Duration
    57  
    58  	sessionMutex sync.Mutex
    59  
    60  	// secureCookies enables Set-Cookie.Secure=true
    61  	// We can't do this by default as simulator.Service defaults to no TLS by default and
    62  	// Go's cookiejar does not send Secure cookies unless the URL scheme is https.
    63  	secureCookies = os.Getenv("VCSIM_SECURE_COOKIES") == "true"
    64  )
    65  
    66  func createSession(ctx *Context, name string, locale string) types.UserSession {
    67  	now := time.Now().UTC()
    68  
    69  	if locale == "" {
    70  		locale = session.Locale
    71  	}
    72  
    73  	session := Session{
    74  		UserSession: types.UserSession{
    75  			Key:              uuid.New().String(),
    76  			UserName:         name,
    77  			FullName:         name,
    78  			LoginTime:        now,
    79  			LastActiveTime:   now,
    80  			Locale:           locale,
    81  			MessageLocale:    locale,
    82  			ExtensionSession: types.NewBool(false),
    83  		},
    84  		Registry: NewRegistry(),
    85  	}
    86  
    87  	ctx.SetSession(session, true)
    88  
    89  	return ctx.Session.UserSession
    90  }
    91  
    92  func (m *SessionManager) getSession(id string) (Session, bool) {
    93  	sessionMutex.Lock()
    94  	defer sessionMutex.Unlock()
    95  	s, ok := m.sessions[id]
    96  	return s, ok
    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) Login(ctx *Context, req *types.Login) soap.HasFault {
   125  	body := new(methods.LoginBody)
   126  
   127  	if ctx.Session == nil && s.Authenticate(*ctx.svc.Listen, req) {
   128  		body.Res = &types.LoginResponse{
   129  			Returnval: createSession(ctx, req.UserName, req.Locale),
   130  		}
   131  	} else {
   132  		body.Fault_ = invalidLogin
   133  	}
   134  
   135  	return body
   136  }
   137  
   138  func (s *SessionManager) LoginExtensionByCertificate(ctx *Context, req *types.LoginExtensionByCertificate) soap.HasFault {
   139  	body := new(methods.LoginExtensionByCertificateBody)
   140  
   141  	if ctx.req.TLS == nil || len(ctx.req.TLS.PeerCertificates) == 0 {
   142  		body.Fault_ = Fault("", new(types.NoClientCertificate))
   143  		return body
   144  	}
   145  
   146  	if req.ExtensionKey == "" || ctx.Session != nil {
   147  		body.Fault_ = invalidLogin
   148  	} else {
   149  		body.Res = &types.LoginExtensionByCertificateResponse{
   150  			Returnval: createSession(ctx, req.ExtensionKey, req.Locale),
   151  		}
   152  	}
   153  
   154  	return body
   155  }
   156  
   157  func (s *SessionManager) LoginByToken(ctx *Context, req *types.LoginByToken) soap.HasFault {
   158  	body := new(methods.LoginByTokenBody)
   159  
   160  	if ctx.Session != nil {
   161  		body.Fault_ = invalidLogin
   162  	} else {
   163  		var subject struct {
   164  			ID string `xml:"Assertion>Subject>NameID"`
   165  		}
   166  
   167  		if s, ok := ctx.Header.Security.(*Element); ok {
   168  			_ = s.Decode(&subject)
   169  		}
   170  
   171  		if subject.ID == "" {
   172  			body.Fault_ = invalidLogin
   173  			return body
   174  		}
   175  
   176  		body.Res = &types.LoginByTokenResponse{
   177  			Returnval: createSession(ctx, subject.ID, req.Locale),
   178  		}
   179  	}
   180  
   181  	return body
   182  }
   183  
   184  func (s *SessionManager) Logout(ctx *Context, _ *types.Logout) soap.HasFault {
   185  	session := ctx.Session
   186  	s.delSession(session.Key)
   187  	pc := ctx.Map.content().PropertyCollector
   188  
   189  	for ref, obj := range ctx.Session.Registry.objects {
   190  		if ref == pc {
   191  			continue // don't unregister the PropertyCollector singleton
   192  		}
   193  		if _, ok := obj.(RegisterObject); ok {
   194  			ctx.Map.Remove(ctx, ref) // Remove RegisterObject handlers
   195  		}
   196  	}
   197  
   198  	ctx.postEvent(&types.UserLogoutSessionEvent{
   199  		IpAddress: session.IpAddress,
   200  		UserAgent: session.UserAgent,
   201  		SessionId: session.Key,
   202  		LoginTime: &session.LoginTime,
   203  	})
   204  
   205  	return &methods.LogoutBody{Res: new(types.LogoutResponse)}
   206  }
   207  
   208  func (s *SessionManager) TerminateSession(ctx *Context, req *types.TerminateSession) soap.HasFault {
   209  	body := new(methods.TerminateSessionBody)
   210  
   211  	for _, id := range req.SessionId {
   212  		if id == ctx.Session.Key {
   213  			body.Fault_ = Fault("", new(types.InvalidArgument))
   214  			return body
   215  		}
   216  		if _, ok := s.getSession(id); !ok {
   217  			body.Fault_ = Fault("", new(types.NotFound))
   218  			return body
   219  		}
   220  		s.delSession(id)
   221  	}
   222  
   223  	body.Res = new(types.TerminateSessionResponse)
   224  	return body
   225  }
   226  
   227  func (s *SessionManager) SessionIsActive(ctx *Context, req *types.SessionIsActive) soap.HasFault {
   228  	body := new(methods.SessionIsActiveBody)
   229  
   230  	if ctx.Map.IsESX() {
   231  		body.Fault_ = Fault("", new(types.NotImplemented))
   232  		return body
   233  	}
   234  
   235  	body.Res = new(types.SessionIsActiveResponse)
   236  
   237  	if session, exists := s.getSession(req.SessionID); exists {
   238  		body.Res.Returnval = session.UserName == req.UserName
   239  	}
   240  
   241  	return body
   242  }
   243  
   244  func (s *SessionManager) AcquireCloneTicket(ctx *Context, _ *types.AcquireCloneTicket) soap.HasFault {
   245  	session := *ctx.Session
   246  	session.Key = uuid.New().String()
   247  	s.putSession(session)
   248  
   249  	return &methods.AcquireCloneTicketBody{
   250  		Res: &types.AcquireCloneTicketResponse{
   251  			Returnval: session.Key,
   252  		},
   253  	}
   254  }
   255  
   256  func (s *SessionManager) CloneSession(ctx *Context, ticket *types.CloneSession) soap.HasFault {
   257  	body := new(methods.CloneSessionBody)
   258  
   259  	session, exists := s.getSession(ticket.CloneTicket)
   260  
   261  	if exists {
   262  		s.delSession(ticket.CloneTicket) // A clone ticket can only be used once
   263  		session.Key = uuid.New().String()
   264  		ctx.SetSession(session, true)
   265  
   266  		body.Res = &types.CloneSessionResponse{
   267  			Returnval: session.UserSession,
   268  		}
   269  	} else {
   270  		body.Fault_ = invalidLogin
   271  	}
   272  
   273  	return body
   274  }
   275  
   276  func (s *SessionManager) AcquireGenericServiceTicket(ticket *types.AcquireGenericServiceTicket) soap.HasFault {
   277  	return &methods.AcquireGenericServiceTicketBody{
   278  		Res: &types.AcquireGenericServiceTicketResponse{
   279  			Returnval: types.SessionManagerGenericServiceTicket{
   280  				Id:       uuid.New().String(),
   281  				HostName: s.ServiceHostName,
   282  			},
   283  		},
   284  	}
   285  }
   286  
   287  var invalidLogin = Fault("Login failure", new(types.InvalidLogin))
   288  
   289  // Context provides per-request Session management.
   290  type Context struct {
   291  	req *http.Request
   292  	res http.ResponseWriter
   293  	svc *Service
   294  
   295  	context.Context
   296  	Session *Session
   297  	Header  soap.Header
   298  	Caller  *types.ManagedObjectReference
   299  	Map     *Registry
   300  }
   301  
   302  // mapSession maps an HTTP cookie to a Session.
   303  func (c *Context) mapSession() {
   304  	if cookie, err := c.req.Cookie(soap.SessionCookieName); err == nil {
   305  		if val, ok := c.svc.sm.getSession(cookie.Value); ok {
   306  			c.SetSession(val, false)
   307  		}
   308  	}
   309  }
   310  
   311  func (m *SessionManager) expiredSession(id string, now time.Time) bool {
   312  	expired := true
   313  
   314  	s, ok := m.getSession(id)
   315  	if ok {
   316  		expired = now.Sub(s.LastActiveTime) > SessionIdleTimeout
   317  		if expired {
   318  			m.delSession(id)
   319  		}
   320  	}
   321  
   322  	return expired
   323  }
   324  
   325  // SessionIdleWatch starts a goroutine that calls func expired() at SessionIdleTimeout intervals.
   326  // The goroutine exits if the func returns true.
   327  func SessionIdleWatch(ctx context.Context, id string, expired func(string, time.Time) bool) {
   328  	if SessionIdleTimeout == 0 {
   329  		return
   330  	}
   331  
   332  	go func() {
   333  		for t := time.NewTimer(SessionIdleTimeout); ; {
   334  			select {
   335  			case <-ctx.Done():
   336  				return
   337  			case now := <-t.C:
   338  				if expired(id, now) {
   339  					return
   340  				}
   341  				t.Reset(SessionIdleTimeout)
   342  			}
   343  		}
   344  	}()
   345  }
   346  
   347  // SetSession should be called after successful authentication.
   348  func (c *Context) SetSession(session Session, login bool) {
   349  	session.UserAgent = c.req.UserAgent()
   350  	session.IpAddress = strings.Split(c.req.RemoteAddr, ":")[0]
   351  	session.LastActiveTime = time.Now()
   352  	session.CallCount++
   353  
   354  	c.svc.sm.putSession(session)
   355  	c.Session = &session
   356  
   357  	if login {
   358  		http.SetCookie(c.res, &http.Cookie{
   359  			Name:     soap.SessionCookieName,
   360  			Value:    session.Key,
   361  			Secure:   secureCookies,
   362  			HttpOnly: true,
   363  		})
   364  
   365  		c.postEvent(&types.UserLoginSessionEvent{
   366  			SessionId: session.Key,
   367  			IpAddress: session.IpAddress,
   368  			UserAgent: session.UserAgent,
   369  			Locale:    session.Locale,
   370  		})
   371  
   372  		SessionIdleWatch(c.Context, session.Key, c.svc.sm.expiredSession)
   373  	}
   374  }
   375  
   376  // WithLock holds a lock for the given object while the given function is run.
   377  // It will skip locking if this context already holds the given object's lock.
   378  func (c *Context) WithLock(obj mo.Reference, f func()) {
   379  	// TODO: This is not always going to be correct. An object should
   380  	// really be locked by the registry that "owns it", which is not always
   381  	// Map. This function will need to take the Registry as an additional
   382  	// argument to accomplish this.
   383  	// Basic mutex locking will work even if obj doesn't belong to Map, but
   384  	// if obj implements sync.Locker, that custom locking will not be used.
   385  	c.Map.WithLock(c, obj, f)
   386  }
   387  
   388  // postEvent wraps EventManager.PostEvent for internal use, with a lock on the EventManager.
   389  func (c *Context) postEvent(events ...types.BaseEvent) {
   390  	m := c.Map.EventManager()
   391  	c.WithLock(m, func() {
   392  		for _, event := range events {
   393  			m.PostEvent(c, &types.PostEvent{EventToPost: event})
   394  		}
   395  	})
   396  }
   397  
   398  // Session combines a UserSession and a Registry for per-session managed objects.
   399  type Session struct {
   400  	types.UserSession
   401  	*Registry
   402  }
   403  
   404  func (s *Session) setReference(item mo.Reference) {
   405  	ref := item.Reference()
   406  	if ref.Value == "" {
   407  		ref.Value = fmt.Sprintf("session[%s]%s", s.Key, uuid.New())
   408  	}
   409  	if ref.Type == "" {
   410  		ref.Type = typeName(item)
   411  	}
   412  	s.Registry.setReference(item, ref)
   413  }
   414  
   415  // Put wraps Registry.Put, setting the moref value to include the session key.
   416  func (s *Session) Put(item mo.Reference) mo.Reference {
   417  	s.setReference(item)
   418  	return s.Registry.Put(item)
   419  }
   420  
   421  // Get wraps Registry.Get, session-izing singleton objects such as SessionManager and the root PropertyCollector.
   422  func (s *Session) Get(ref types.ManagedObjectReference) mo.Reference {
   423  	obj := s.Registry.Get(ref)
   424  	if obj != nil {
   425  		return obj
   426  	}
   427  
   428  	// Return a session "view" of certain singleton objects
   429  	switch ref.Type {
   430  	case "SessionManager":
   431  		// Clone SessionManager so the PropertyCollector can properly report CurrentSession
   432  		m := *Map.SessionManager()
   433  		m.CurrentSession = &s.UserSession
   434  
   435  		// TODO: we could maintain SessionList as part of the SessionManager singleton
   436  		sessionMutex.Lock()
   437  		for _, session := range m.sessions {
   438  			m.SessionList = append(m.SessionList, session.UserSession)
   439  		}
   440  		sessionMutex.Unlock()
   441  
   442  		return &m
   443  	case "PropertyCollector":
   444  		if ref == Map.content().PropertyCollector {
   445  			// Per-session instance of the PropertyCollector singleton.
   446  			// Using reflection here as PropertyCollector might be wrapped with a custom type.
   447  			obj = Map.Get(ref)
   448  			pc := reflect.New(reflect.TypeOf(obj).Elem())
   449  			obj = pc.Interface().(mo.Reference)
   450  			s.Registry.setReference(obj, ref)
   451  			return s.Put(obj)
   452  		}
   453  	}
   454  
   455  	return Map.Get(ref)
   456  }