Repository: vmware/govmomi
Pull request #3051 (2023-02-20 17:27:56)

vcsim: add ssoadmin user and group management
## Description

The ssoadmin endpoint is now used for authentication for both SOAP and REST endpoints. Previous behavior is retained:
- By default, any non-empty username + password is authenticated
- If vcsim '-username' and '-password' flags are given, only those credentials can be used for authentication

Additional users can be added via the ssoadmin APIs or using govc for example:
```console
  % govc sso.user.create -p password govc
```

Closes:  #2852

## Type of change

Please mark options that are relevant:

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to
  not work as expected)
- [ ] This change requires a documentation update
- [ ] Build related change

## How Has This Been Tested?

Added bats tests

## Checklist:

- [x] My code follows the `CONTRIBUTION`
  [guidelines](https://github.com/vmware/govmomi/blob/main/CONTRIBUTING.md) of
  this project
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged


Patch:
diff --git a/simulator/session_manager.go b/simulator/session_manager.go
index 169ea3c4..e9fbfcb6 100644
--- a/simulator/session_manager.go
+++ b/simulator/session_manager.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2017-2018 VMware, Inc. All Rights Reserved.
+Copyright (c) 2017-2023 VMware, Inc. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-    http://www.apache.org/licenses/LICENSE-2.0
+http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,6 +20,7 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"net/url"
 	"os"
 	"reflect"
 	"strings"
@@ -41,6 +42,7 @@ type SessionManager struct {
 
 	ServiceHostName string
 	TLSCert         func() string
+	ValidLogin      func(*types.Login) bool
 
 	sessions map[string]Session
 }
@@ -106,22 +108,23 @@ func (m *SessionManager) putSession(s Session) {
 	m.sessions[s.Key] = s
 }
 
-func (s *SessionManager) validLogin(ctx *Context, req *types.Login) bool {
-	if ctx.Session != nil {
-		return false
-	}
-	user := ctx.svc.Listen.User
-	if user == nil || user == DefaultLogin {
+func (s *SessionManager) Authenticate(u url.URL, req *types.Login) bool {
+	if u.User == nil || u.User == DefaultLogin {
 		return req.UserName != "" && req.Password != ""
 	}
-	pass, _ := user.Password()
-	return req.UserName == user.Username() && req.Password == pass
+
+	if s.ValidLogin != nil {
+		return s.ValidLogin(req)
+	}
+
+	pass, _ := u.User.Password()
+	return req.UserName == u.User.Username() && req.Password == pass
 }
 
 func (s *SessionManager) Login(ctx *Context, req *types.Login) soap.HasFault {
 	body := new(methods.LoginBody)
 
-	if s.validLogin(ctx, req) {
+	if ctx.Session == nil && s.Authenticate(*ctx.svc.Listen, req) {
 		body.Res = &types.LoginResponse{
 			Returnval: createSession(ctx, req.UserName, req.Locale),
 		}
diff --git a/ssoadmin/simulator/simulator.go b/ssoadmin/simulator/simulator.go
index e4564840..814ef532 100644
--- a/ssoadmin/simulator/simulator.go
+++ b/ssoadmin/simulator/simulator.go
@@ -1,22 +1,25 @@
 /*
-   Copyright (c) 2022 VMware, Inc. All Rights Reserved.
+Copyright (c) 2022-2023 VMware, Inc. All Rights Reserved.
 
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
 
 http://www.apache.org/licenses/LICENSE-2.0
 
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
 */
 
 package simulator
 
 import (
+	"net/url"
+	"strings"
+
 	"github.com/vmware/govmomi/simulator"
 	"github.com/vmware/govmomi/ssoadmin"
 	"github.com/vmware/govmomi/ssoadmin/methods"
@@ -28,7 +31,7 @@ import (
 func init() {
 	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
 		if r.IsVPX() {
-			s.RegisterSDK(New())
+			s.RegisterSDK(New(r, s.Listen))
 		}
 	})
 }
@@ -49,6 +52,11 @@ var content = types.AdminServiceContent{
 	ReplicationService:              vim.ManagedObjectReference{Type: "SsoAdminReplicationService", Value: "replicationService"},
 }
 
+var groupcheckContent = types.GroupcheckServiceContent{
+	SessionManager:    vim.ManagedObjectReference{Type: "SsoSessionManager", Value: "ssoSessionManager"},
+	GroupCheckService: vim.ManagedObjectReference{Type: "SsoGroupcheckGroupCheckService", Value: "groupCheckService"},
+}
+
 var groupcheckServiceInstance = vim.ManagedObjectReference{
 	Type: "SsoGroupcheckServiceInstance", Value: "ServiceInstance",
 }
@@ -93,6 +101,12 @@ type GroupcheckServiceInstance struct {
 	vim.ManagedObjectReference
 }
 
+type GroupcheckService struct {
+	vim.ManagedObjectReference
+
+	m *PrincipalManagementService
+}
+
 type SessionManager struct {
 	vim.ManagedObjectReference
 }
@@ -101,7 +115,27 @@ type IdentitySourceManagementService struct {
 	vim.ManagedObjectReference
 }
 
-func New() *simulator.Registry {
+type PrincipalManagementService struct {
+	vim.ManagedObjectReference
+
+	dir map[types.PrincipalId]principal
+}
+
+type PrincipalDiscoveryService struct {
+	vim.ManagedObjectReference
+
+	m *PrincipalManagementService
+}
+
+type principal struct {
+	group    *types.AdminGroup
+	members  map[types.PrincipalId]principal
+	solution *types.AdminSolutionUser
+	person   *types.AdminPersonUser
+	password string
+}
+
+func New(vc *simulator.Registry, u *url.URL) *simulator.Registry {
 	r := simulator.NewRegistry()
 	r.Namespace = ssoadmin.Namespace
 	r.Path = ssoadmin.Path
@@ -122,6 +156,24 @@ func New() *simulator.Registry {
 		ManagedObjectReference: content.IdentitySourceManagementService,
 	})
 
+	m := &PrincipalManagementService{
+		ManagedObjectReference: content.PrincipalManagementService,
+		dir:                    make(map[types.PrincipalId]principal),
+	}
+	r.Put(m)
+	m.createDefaultUser(u.User)
+	vc.SessionManager().ValidLogin = m.validLogin
+
+	r.Put(&PrincipalDiscoveryService{
+		ManagedObjectReference: content.PrincipalDiscoveryService,
+		m:                      m,
+	})
+
+	r.Put(&GroupcheckService{
+		ManagedObjectReference: groupcheckContent.GroupCheckService,
+		m:                      m,
+	})
+
 	return r
 }
 
@@ -136,11 +188,40 @@ func (s *ServiceInstance) SsoAdminServiceInstance(ctx *simulator.Context, _ *typ
 func (s *GroupcheckServiceInstance) SsoGroupcheckServiceInstance(ctx *simulator.Context, _ *types.SsoGroupcheckServiceInstance) soap.HasFault {
 	return &methods.SsoGroupcheckServiceInstanceBody{
 		Res: &types.SsoGroupcheckServiceInstanceResponse{
-			Returnval: types.GroupcheckServiceContent{},
+			Returnval: groupcheckContent,
 		},
 	}
 }
 
+func (s *GroupcheckService) FindAllParentGroups(ctx *simulator.Context, req *types.FindAllParentGroups) soap.HasFault {
+	body := new(methods.FindAllParentGroupsBody)
+
+	p, ok := s.m.dir[req.UserId]
+	if !ok || p.person == nil {
+		body.Fault_ = simulator.Fault("", new(vim.NotFound))
+		return body
+	}
+
+	body.Res = new(types.FindAllParentGroupsResponse)
+
+	for gid, p := range s.m.dir {
+		if p.group == nil {
+			continue
+		}
+
+		for id, m := range p.members {
+			if m.person == nil {
+				continue
+			}
+			if id == req.UserId {
+				body.Res.Returnval = append(body.Res.Returnval, gid)
+			}
+		}
+	}
+
+	return body
+}
+
 func (s *SessionManager) Login(ctx *simulator.Context, req *types.Login) soap.HasFault {
 	body := new(methods.LoginBody)
 
@@ -179,3 +260,488 @@ func (s *IdentitySourceManagementService) Get(ctx *simulator.Context, _ *types.G
 		},
 	}
 }
+
+func (s *PrincipalDiscoveryService) FindUser(ctx *simulator.Context, req *types.FindUser) soap.HasFault {
+	body := &methods.FindUserBody{
+		Res: new(types.FindUserResponse),
+	}
+
+	p, ok := s.m.dir[req.UserId]
+	if !ok {
+		return body
+	}
+
+	var user *types.AdminUser
+
+	switch {
+	case p.person != nil:
+		user = &types.AdminUser{
+			Kind:        "person",
+			Id:          p.person.Id,
+			Description: p.person.Details.Description,
+		}
+	case p.solution != nil:
+		user = &types.AdminUser{
+			Kind:        "solution",
+			Id:          p.solution.Id,
+			Description: p.solution.Details.Description,
+		}
+
+	}
+
+	body.Res.Returnval = user
+
+	return body
+}
+
+func (s *PrincipalDiscoveryService) FindPersonUser(ctx *simulator.Context, req *types.FindPersonUser) soap.HasFault {
+	body := &methods.FindPersonUserBody{
+		Res: new(types.FindPersonUserResponse),
+	}
+
+	p, ok := s.m.dir[req.UserId]
+	if ok {
+		body.Res.Returnval = p.person
+	}
+
+	return body
+}
+
+func (s *PrincipalDiscoveryService) FindPersonUsers(ctx *simulator.Context, req *types.FindPersonUsers) soap.HasFault {
+	body := &methods.FindPersonUsersBody{
+		Res: &types.FindPersonUsersResponse{},
+	}
+
+	for _, p := range s.m.dir {
+		if p.person == nil {
+			continue
+		}
+
+		if domain := req.Criteria.Domain; domain != "" {
+			if p.person.Id.Domain != domain {
+				continue
+			}
+		}
+
+		if search := req.Criteria.SearchString; search != "" {
+			if !strings.Contains(p.person.Id.Name, search) {
+				continue
+			}
+		}
+
+		body.Res.Returnval = append(body.Res.Returnval, *p.person)
+	}
+
+	return body
+}
+
+func (s *PrincipalManagementService) createDefaultUser(u *url.Userinfo) {
+	user := &types.AdminPersonUser{
+		Id: types.PrincipalId{
+			Name:   u.Username(),
+			Domain: IdentitySources.System.Name,
+		},
+	}
+
+	pass, _ := u.Password()
+	s.dir[user.Id] = principal{
+		person:   user,
+		password: pass,
+	}
+}
+
+func (s *PrincipalManagementService) validLogin(login *vim.Login) bool {
+	id := parseID(login.UserName)
+
+	if p, ok := s.dir[id]; ok && p.person != nil {
+		return p.password == login.Password
+	}
+
+	return false
+}
+
+func (s *PrincipalDiscoveryService) FindSolutionUser(ctx *simulator.Context, req *types.FindSolutionUser) soap.HasFault {
+	body := &methods.FindSolutionUserBody{
+		Res: new(types.FindSolutionUserResponse),
+	}
+
+	id := parseID(req.UserName)
+	p, ok := s.m.dir[id]
+	if ok {
+		body.Res.Returnval = p.solution
+	}
+
+	return body
+}
+
+func (s *PrincipalDiscoveryService) FindSolutionUsers(ctx *simulator.Context, req *types.FindSolutionUsers) soap.HasFault {
+	body := &methods.FindSolutionUsersBody{
+		Res: new(types.FindSolutionUsersResponse),
+	}
+
+	for _, p := range s.m.dir {
+		if p.solution == nil {
+			continue
+		}
+
+		if search := req.SearchString; search != "" {
+			if !strings.Contains(p.solution.Id.Name, search) {
+				continue
+			}
+		}
+
+		body.Res.Returnval = append(body.Res.Returnval, *p.solution)
+	}
+
+	return body
+}
+
+func (s *PrincipalManagementService) CreateLocalPersonUser(ctx *simulator.Context, req *types.CreateLocalPersonUser) soap.HasFault {
+	body := new(methods.CreateLocalPersonUserBody)
+
+	user := &types.AdminPersonUser{
+		Id: types.PrincipalId{
+			Name:   req.UserName,
+			Domain: IdentitySources.System.Name,
+		},
+		Details: req.UserDetails,
+	}
+
+	if _, ok := s.dir[user.Id]; ok {
+		body.Fault_ = simulator.Fault("", new(vim.DuplicateName))
+		return body
+	}
+
+	s.dir[user.Id] = principal{
+		person:   user,
+		password: req.Password,
+	}
+
+	body.Res = new(types.CreateLocalPersonUserResponse)
+
+	return body
+}
+
+func (s *PrincipalManagementService) UpdateLocalPersonUserDetails(ctx *simulator.Context, req *types.UpdateLocalPersonUserDetails) soap.HasFault {
+	body := &methods.UpdateLocalPersonUserDetailsBody{
+		Res: new(types.UpdateLocalPersonUserDetailsResponse),
+	}
+
+	id := parseID(req.UserName)
+	p, ok := s.dir[id]
+	if !ok || p.person == nil {
+		body.Fault_ = simulator.Fault("", new(vim.NotFound))
+		return body
+	}
+
+	p.person.Details = req.UserDetails
+
+	return body
+}
+
+func (s *PrincipalManagementService) ResetLocalPersonUserPassword(ctx *simulator.Context, req *types.ResetLocalPersonUserPassword) soap.HasFault {
+	body := &methods.ResetLocalPersonUserPasswordBody{
+		Res: new(types.ResetLocalPersonUserPasswordResponse),
+	}
+
+	id := parseID(req.UserName)
+	p, ok := s.dir[id]
+	if !ok || p.person == nil {
+		body.Fault_ = simulator.Fault("", new(vim.NotFound))
+		return body
+	}
+
+	p.password = req.NewPassword
+
+	return body
+}
+
+func (s *PrincipalManagementService) CreateLocalSolutionUser(ctx *simulator.Context, req *types.CreateLocalSolutionUser) soap.HasFault {
+	body := new(methods.CreateLocalSolutionUserBody)
+
+	user := &types.AdminSolutionUser{
+		Id: types.PrincipalId{
+			Name:   req.UserName,
+			Domain: IdentitySources.System.Name,
+		},
+		Details: req.UserDetails,
+	}
+
+	if _, ok := s.dir[user.Id]; ok {
+		body.Fault_ = simulator.Fault("", new(vim.DuplicateName))
+		return body
+	}
+
+	s.dir[user.Id] = principal{
+		solution: user,
+	}
+
+	body.Res = new(types.CreateLocalSolutionUserResponse)
+
+	return body
+}
+
+func (s *PrincipalManagementService) UpdateLocalSolutionUserDetails(ctx *simulator.Context, req *types.UpdateLocalSolutionUserDetails) soap.HasFault {
+	body := &methods.UpdateLocalSolutionUserDetailsBody{
+		Res: new(types.UpdateLocalSolutionUserDetailsResponse),
+	}
+
+	id := parseID(req.UserName)
+	p, ok := s.dir[id]
+	if !ok || p.solution == nil {
+		body.Fault_ = simulator.Fault("", new(vim.NotFound))
+		return body
+	}
+
+	p.solution.Details = req.UserDetails
+
+	return body
+}
+
+func (s *PrincipalManagementService) DeleteLocalPrincipal(ctx *simulator.Context, req *types.DeleteLocalPrincipal) soap.HasFault {
+	body := new(methods.DeleteLocalPrincipalBody)
+
+	id := parseID(req.PrincipalName)
+
+	if _, ok := s.dir[id]; ok {
+		delete(s.dir, id)
+		for _, p := range s.dir {
+			if p.group != nil {
+				delete(p.members, id)
+			}
+		}
+		body.Res = new(types.DeleteLocalPrincipalResponse)
+	} else {
+		body.Fault_ = simulator.Fault("", new(vim.NotFound))
+	}
+
+	return body
+}
+
+func parseID(name string) types.PrincipalId {
+	p := strings.SplitN(name, "@", 2)
+	id := types.PrincipalId{Name: p[0]}
+	if len(p) == 2 {
+		id.Domain = p[1]
+	} else {
+		id.Domain = IdentitySources.System.Name
+	}
+	return id
+}
+
+func (s *PrincipalDiscoveryService) FindGroup(ctx *simulator.Context, req *types.FindGroup) soap.HasFault {
+	body := &methods.FindGroupBody{
+		Res: new(types.FindGroupResponse),
+	}
+
+	p, ok := s.m.dir[req.GroupId]
+	if !ok {
+		return body
+	}
+
+	body.Res.Returnval = p.group
+
+	return body
+}
+
+func (s *PrincipalDiscoveryService) FindGroups(ctx *simulator.Context, req *types.FindGroups) soap.HasFault {
+	body := &methods.FindGroupsBody{
+		Res: &types.FindGroupsResponse{},
+	}
+
+	for _, p := range s.m.dir {
+		if p.group == nil {
+			continue
+		}
+
+		if domain := req.Criteria.Domain; domain != "" {
+			if p.group.Id.Domain != domain {
+				continue
+			}
+		}
+
+		if search := req.Criteria.SearchString; search != "" {
+			if !strings.Contains(p.group.Id.Name, search) {
+				continue
+			}
+		}
+
+		body.Res.Returnval = append(body.Res.Returnval, *p.group)
+	}
+
+	return body
+}
+
+func (s *PrincipalDiscoveryService) FindGroupsInGroup(ctx *simulator.Context, req *types.FindGroupsInGroup) soap.HasFault {
+	body := &methods.FindGroupsInGroupBody{
+		Res: &types.FindGroupsInGroupResponse{},
+	}
+
+	for _, p := range s.m.dir {
+		if p.group == nil {
+			continue
+		}
+
+		for id := range p.members {
+			if search := req.SearchString; search != "" {
+				if !strings.Contains(id.Name, search) {
+					continue
+				}
+			}
+
+			body.Res.Returnval = append(body.Res.Returnval, *p.group)
+		}
+	}
+
+	return body
+}
+
+func (s *PrincipalManagementService) CreateLocalGroup(ctx *simulator.Context, req *types.CreateLocalGroup) soap.HasFault {
+	body := new(methods.CreateLocalGroupBody)
+
+	group := &types.AdminGroup{
+		Id: types.PrincipalId{
+			Name:   req.GroupName,
+			Domain: IdentitySources.System.Name,
+		},
+		Details: req.GroupDetails,
+	}
+
+	if _, ok := s.dir[group.Id]; ok {
+		body.Fault_ = simulator.Fault("", new(vim.DuplicateName))
+		return body
+	}
+
+	s.dir[group.Id] = principal{
+		group:   group,
+		members: make(map[types.PrincipalId]principal),
+	}
+
+	body.Res = new(types.CreateLocalGroupResponse)
+
+	return body
+}
+
+func (s *PrincipalManagementService) UpdateLocalGroupDetails(ctx *simulator.Context, req *types.UpdateLocalGroupDetails) soap.HasFault {
+	body := &methods.UpdateLocalGroupDetailsBody{
+		Res: new(types.UpdateLocalGroupDetailsResponse),
+	}
+
+	id := parseID(req.GroupName)
+	p, ok := s.dir[id]
+	if !ok || p.group == nil {
+		body.Fault_ = simulator.Fault("", new(vim.NotFound))
+		return body
+	}
+
+	p.group.Details = req.GroupDetails
+
+	return body
+}
+
+func (s *PrincipalManagementService) AddUsersToLocalGroup(ctx *simulator.Context, req *types.AddUsersToLocalGroup) soap.HasFault {
+	body := &methods.AddUsersToLocalGroupBody{
+		Res: new(types.AddUsersToLocalGroupResponse),
+	}
+
+	id := parseID(req.GroupName)
+	g, ok := s.dir[id]
+	if !ok || g.group == nil {
+		body.Fault_ = simulator.Fault("", new(vim.NotFound))
+		return body
+	}
+
+	res := new(types.AddUsersToLocalGroupResponse)
+
+	for _, id := range req.UserIds {
+		p, ok := s.dir[id]
+		if !ok || p.person == nil {
+			body.Fault_ = simulator.Fault("", new(vim.NotFound))
+			return body
+		}
+
+		added := false
+		if _, ok := g.members[id]; !ok {
+			g.members[id] = p
+		} else {
+			added = true
+		}
+
+		res.Returnval = append(res.Returnval, added)
+	}
+
+	body.Res = res
+
+	return body
+}
+
+func (s *PrincipalManagementService) AddGroupsToLocalGroup(ctx *simulator.Context, req *types.AddGroupsToLocalGroup) soap.HasFault {
+	body := &methods.AddGroupsToLocalGroupBody{
+		Res: new(types.AddGroupsToLocalGroupResponse),
+	}
+
+	id := parseID(req.GroupName)
+	g, ok := s.dir[id]
+	if !ok || g.group == nil {
+		body.Fault_ = simulator.Fault("", new(vim.NotFound))
+		return body
+	}
+
+	res := new(types.AddGroupsToLocalGroupResponse)
+
+	for _, id := range req.GroupIds {
+		p, ok := s.dir[id]
+		if !ok || p.group == nil {
+			body.Fault_ = simulator.Fault("", new(vim.NotFound))
+			return body
+		}
+
+		added := false
+		if _, ok := g.members[id]; !ok {
+			g.members[id] = p
+		} else {
+			added = true
+		}
+
+		res.Returnval = append(res.Returnval, added)
+	}
+
+	body.Res = res
+
+	return body
+}
+
+func (s *PrincipalManagementService) RemovePrincipalsFromLocalGroup(ctx *simulator.Context, req *types.RemovePrincipalsFromLocalGroup) soap.HasFault {
+	body := &methods.RemovePrincipalsFromLocalGroupBody{
+		Res: new(types.RemovePrincipalsFromLocalGroupResponse),
+	}
+
+	id := parseID(req.GroupName)
+	g, ok := s.dir[id]
+	if !ok || g.group == nil {
+		body.Fault_ = simulator.Fault("", new(vim.NotFound))
+		return body
+	}
+
+	res := new(types.RemovePrincipalsFromLocalGroupResponse)
+
+	for _, id := range req.PrincipalsIds {
+		_, ok := s.dir[id]
+		if !ok {
+			body.Fault_ = simulator.Fault("", new(vim.NotFound))
+			return body
+		}
+
+		removed := false
+		if _, ok := g.members[id]; ok {
+			delete(g.members, id)
+			removed = true
+		}
+
+		res.Returnval = append(res.Returnval, removed)
+	}
+
+	body.Res = res
+
+	return body
+}
diff --git a/vapi/simulator/simulator.go b/vapi/simulator/simulator.go
index 3ef5233e..1ceb51df 100644
--- a/vapi/simulator/simulator.go
+++ b/vapi/simulator/simulator.go
@@ -1,11 +1,11 @@
 /*
-Copyright (c) 2018-2022 VMware, Inc. All Rights Reserved.
+Copyright (c) 2018-2023 VMware, Inc. All Rights Reserved.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-    http://www.apache.org/licenses/LICENSE-2.0
+http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -88,6 +88,7 @@ type download struct {
 
 type handler struct {
 	sync.Mutex
+	sm          *simulator.SessionManager
 	ServeMux    *http.ServeMux
 	URL         url.URL
 	Category    map[string]*tags.Category
@@ -104,7 +105,7 @@ type handler struct {
 func init() {
 	simulator.RegisterEndpoint(func(s *simulator.Service, r *simulator.Registry) {
 		if r.IsVPX() {
-			patterns, h := New(s.Listen, r.OptionManager().Setting)
+			patterns, h := New(s.Listen, r)
 			for _, p := range patterns {
 				s.Handle(p, h)
 			}
@@ -113,8 +114,9 @@ func init() {
 }
 
 // New creates a vAPI simulator.
-func New(u *url.URL, settings []vim.BaseOptionValue) ([]string, http.Handler) {
+func New(u *url.URL, r *simulator.Registry) ([]string, http.Handler) {
 	s := &handler{
+		sm:          r.SessionManager(),
 		ServeMux:    http.NewServeMux(),
 		URL:         *u,
 		Category:    make(map[string]*tags.Category),
@@ -237,10 +239,7 @@ func (s *handler) isAuthorized(r *http.Request) bool {
 func (s *handler) hasAuthorization(r *http.Request) (string, bool) {
 	u, p, ok := r.BasicAuth()
 	if ok { // user+pass auth
-		if u == "" || p == "" {
-			return u, false
-		}
-		return u, true
+		return u, s.sm.Authenticate(s.URL, &vim.Login{UserName: u, Password: p})
 	}
 	auth := r.Header.Get("Authorization")
 	return "TODO", strings.HasPrefix(auth, "SIGN ") // token auth


Test patch:
diff --git a/govc/test/sso.bats b/govc/test/sso.bats
index d03c34e9..1d126fe1 100755
--- a/govc/test/sso.bats
+++ b/govc/test/sso.bats
@@ -60,3 +60,87 @@ load test_helper
   assert_matches "Local OS"
   assert_matches "ActiveDirectory"
 }
+
+@test "sso.user" {
+  vcsim_env
+
+  run govc sso.user.ls
+  assert_success
+
+  run govc sso.user.create -p password govc
+  assert_success
+
+  run govc sso.user.ls
+  assert_success
+  assert_matches govc
+
+  run govc sso.user.ls -s
+  assert_success ""
+
+  run govc sso.user.create -p password govc
+  assert_failure # duplicate name
+
+  run govc sso.user.update -p newpassword govc
+  assert_success
+
+  run govc sso.user.rm govc
+  assert_success
+
+  run govc sso.user.rm govc
+  assert_failure # does not exist
+
+  run govc sso.user.create -C dummy-cert govc
+  assert_success
+
+  run govc sso.user.update -C new-cert govc
+  assert_success
+
+  run govc sso.user.ls -s
+  assert_success
+  assert_matches govc
+}
+
+@test "sso.group" {
+  vcsim_env
+
+  run govc sso.group.ls
+  assert_success
+
+  run govc sso.group.create bats
+  assert_success
+
+  run govc sso.group.create -d "govc CLI" govc
+  assert_success
+
+  run govc sso.group.ls
+  assert_success
+  assert_matches "govc CLI"
+
+  run govc sso.group.update -d "govmomi/govc CLI" govc
+  assert_success
+  run govc sso.group.ls
+  assert_success
+  assert_matches "govmomi/govc CLI"
+
+  run govc sso.group.update -a user govc
+  assert_success
+
+  govc sso.user.id | grep "groups=govc"
+
+  run govc sso.group.update -r user govc
+  assert_success
+  govc sso.user.id | grep -v "groups=govc"
+
+  run govc sso.group.update -g -a govc bats
+  assert_success
+
+  run govc sso.group.ls govc
+  assert_success
+  assert_matches bats
+
+  run govc sso.group.rm govc
+  assert_success
+
+  run govc sso.group.rm govc
+  assert_failure # does not exist
+}
diff --git a/govc/test/vcsim.bats b/govc/test/vcsim.bats
index 3ca249d1..47b8fca5 100755
--- a/govc/test/vcsim.bats
+++ b/govc/test/vcsim.bats
@@ -421,20 +421,48 @@ EOF
 }
 
 @test "vcsim auth" {
+  vcsim_start
+
+  run env GOVC_USERNAME=anybody GOVC_PASSWORD=anything govc ls -u "$(govc env GOVC_URL)"
+  assert_success
+
+  vcsim_stop
+
   vcsim_start -username nobody -password nothing
 
   run govc ls
   assert_success
 
+  run govc tags.ls
+  assert_success
+
   run env GOVC_USERNAME=nobody GOVC_PASSWORD=nothing govc ls -u "$(govc env GOVC_URL)"
   assert_success
 
+  run env GOVC_USERNAME=nobody GOVC_PASSWORD=nothing govc tags.ls -u "$(govc env GOVC_URL)"
+  assert_success
+
   run govc ls -u "user:pass@$(govc env GOVC_URL)"
   assert_failure
 
+  run govc tags.ls -u "user:pass@$(govc env GOVC_URL)"
+  assert_failure
+
   run env GOVC_USERNAME=user GOVC_PASSWORD=pass govc ls -u "$(govc env GOVC_URL)"
   assert_failure
 
+  run env GOVC_USERNAME=user GOVC_PASSWORD=pass govc tags.ls -u "$(govc env GOVC_URL)"
+  assert_failure
+
+  run govc sso.user.create -p pass user
+  assert_success
+
+  run govc ls -u "user:pass@$(govc env GOVC_URL)"
+  assert_success
+
+  run govc tags.ls -u "user:pass@$(govc env GOVC_URL)"
+  assert_success
+
   vcsim_stop
 
   dir=$($mktemp --tmpdir -d govc-test-XXXXX 2>/dev/null || $mktemp -d -t govc-test-XXXXX)