diff --git a/pkg/asset/installconfig/vsphere/mock/authmanager_generated.go b/pkg/asset/installconfig/vsphere/mock/authmanager_generated.go new file mode 100644 index 00000000000..641e91ce5bf --- /dev/null +++ b/pkg/asset/installconfig/vsphere/mock/authmanager_generated.go @@ -0,0 +1,51 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./permissions.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + types "github.com/vmware/govmomi/vim25/types" +) + +// MockAuthManager is a mock of AuthManager interface. +type MockAuthManager struct { + ctrl *gomock.Controller + recorder *MockAuthManagerMockRecorder +} + +// MockAuthManagerMockRecorder is the mock recorder for MockAuthManager. +type MockAuthManagerMockRecorder struct { + mock *MockAuthManager +} + +// NewMockAuthManager creates a new mock instance. +func NewMockAuthManager(ctrl *gomock.Controller) *MockAuthManager { + mock := &MockAuthManager{ctrl: ctrl} + mock.recorder = &MockAuthManagerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAuthManager) EXPECT() *MockAuthManagerMockRecorder { + return m.recorder +} + +// FetchUserPrivilegeOnEntities mocks base method. +func (m *MockAuthManager) FetchUserPrivilegeOnEntities(ctx context.Context, entities []types.ManagedObjectReference, userName string) ([]types.UserPrivilegeResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FetchUserPrivilegeOnEntities", ctx, entities, userName) + ret0, _ := ret[0].([]types.UserPrivilegeResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchUserPrivilegeOnEntities indicates an expected call of FetchUserPrivilegeOnEntities. +func (mr *MockAuthManagerMockRecorder) FetchUserPrivilegeOnEntities(ctx, entities, userName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchUserPrivilegeOnEntities", reflect.TypeOf((*MockAuthManager)(nil).FetchUserPrivilegeOnEntities), ctx, entities, userName) +} diff --git a/pkg/asset/installconfig/vsphere/mock/vsphereclient_mock.go b/pkg/asset/installconfig/vsphere/mock/vsphereclient_mock.go deleted file mode 100644 index 5913e60e4b3..00000000000 --- a/pkg/asset/installconfig/vsphere/mock/vsphereclient_mock.go +++ /dev/null @@ -1,170 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/openshift/installer/pkg/asset/installconfig/vsphere (interfaces: Finder) - -// Package mock is a generated GoMock package. -package mock - -import ( - context "context" - gomock "github.com/golang/mock/gomock" - object "github.com/vmware/govmomi/object" - reflect "reflect" -) - -// MockFinder is a mock of Finder interface -type MockFinder struct { - ctrl *gomock.Controller - recorder *MockFinderMockRecorder -} - -// MockFinderMockRecorder is the mock recorder for MockFinder -type MockFinderMockRecorder struct { - mock *MockFinder -} - -// NewMockFinder creates a new mock instance -func NewMockFinder(ctrl *gomock.Controller) *MockFinder { - mock := &MockFinder{ctrl: ctrl} - mock.recorder = &MockFinderMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use -func (m *MockFinder) EXPECT() *MockFinderMockRecorder { - return m.recorder -} - -// ClusterComputeResource mocks base method -func (m *MockFinder) ClusterComputeResource(arg0 context.Context, arg1 string) (*object.ClusterComputeResource, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ClusterComputeResource", arg0, arg1) - ret0, _ := ret[0].(*object.ClusterComputeResource) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ClusterComputeResource indicates an expected call of ClusterComputeResource -func (mr *MockFinderMockRecorder) ClusterComputeResource(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterComputeResource", reflect.TypeOf((*MockFinder)(nil).ClusterComputeResource), arg0, arg1) -} - -// ClusterComputeResourceList mocks base method -func (m *MockFinder) ClusterComputeResourceList(arg0 context.Context, arg1 string) ([]*object.ClusterComputeResource, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ClusterComputeResourceList", arg0, arg1) - ret0, _ := ret[0].([]*object.ClusterComputeResource) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ClusterComputeResourceList indicates an expected call of ClusterComputeResourceList -func (mr *MockFinderMockRecorder) ClusterComputeResourceList(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterComputeResourceList", reflect.TypeOf((*MockFinder)(nil).ClusterComputeResourceList), arg0, arg1) -} - -// Datacenter mocks base method -func (m *MockFinder) Datacenter(arg0 context.Context, arg1 string) (*object.Datacenter, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Datacenter", arg0, arg1) - ret0, _ := ret[0].(*object.Datacenter) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Datacenter indicates an expected call of Datacenter -func (mr *MockFinderMockRecorder) Datacenter(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Datacenter", reflect.TypeOf((*MockFinder)(nil).Datacenter), arg0, arg1) -} - -// DatacenterList mocks base method -func (m *MockFinder) DatacenterList(arg0 context.Context, arg1 string) ([]*object.Datacenter, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DatacenterList", arg0, arg1) - ret0, _ := ret[0].([]*object.Datacenter) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DatacenterList indicates an expected call of DatacenterList -func (mr *MockFinderMockRecorder) DatacenterList(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DatacenterList", reflect.TypeOf((*MockFinder)(nil).DatacenterList), arg0, arg1) -} - -// DatastoreList mocks base method -func (m *MockFinder) DatastoreList(arg0 context.Context, arg1 string) ([]*object.Datastore, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DatastoreList", arg0, arg1) - ret0, _ := ret[0].([]*object.Datastore) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DatastoreList indicates an expected call of DatastoreList -func (mr *MockFinderMockRecorder) DatastoreList(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DatastoreList", reflect.TypeOf((*MockFinder)(nil).DatastoreList), arg0, arg1) -} - -// Folder mocks base method -func (m *MockFinder) Folder(arg0 context.Context, arg1 string) (*object.Folder, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Folder", arg0, arg1) - ret0, _ := ret[0].(*object.Folder) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Folder indicates an expected call of Folder -func (mr *MockFinderMockRecorder) Folder(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Folder", reflect.TypeOf((*MockFinder)(nil).Folder), arg0, arg1) -} - -// Network mocks base method -func (m *MockFinder) Network(arg0 context.Context, arg1 string) (object.NetworkReference, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Network", arg0, arg1) - ret0, _ := ret[0].(object.NetworkReference) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Network indicates an expected call of Network -func (mr *MockFinderMockRecorder) Network(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Network", reflect.TypeOf((*MockFinder)(nil).Network), arg0, arg1) -} - -// NetworkList mocks base method -func (m *MockFinder) NetworkList(arg0 context.Context, arg1 string) ([]object.NetworkReference, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NetworkList", arg0, arg1) - ret0, _ := ret[0].([]object.NetworkReference) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// NetworkList indicates an expected call of NetworkList -func (mr *MockFinderMockRecorder) NetworkList(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NetworkList", reflect.TypeOf((*MockFinder)(nil).NetworkList), arg0, arg1) -} - -// ResourcePool mocks base method -func (m *MockFinder) ResourcePool(arg0 context.Context, arg1 string) (*object.ResourcePool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ResourcePool", arg0, arg1) - ret0, _ := ret[0].(*object.ResourcePool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ResourcePool indicates an expected call of ResourcePool -func (mr *MockFinderMockRecorder) ResourcePool(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResourcePool", reflect.TypeOf((*MockFinder)(nil).ResourcePool), arg0, arg1) -} diff --git a/pkg/asset/installconfig/vsphere/permission_test.go b/pkg/asset/installconfig/vsphere/permission_test.go new file mode 100644 index 00000000000..911cd092675 --- /dev/null +++ b/pkg/asset/installconfig/vsphere/permission_test.go @@ -0,0 +1,280 @@ +package vsphere + +import ( + "context" + "testing" + + "github.com/openshift/installer/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/vmware/govmomi/object" + vim25types "github.com/vmware/govmomi/vim25/types" + + "github.com/golang/mock/gomock" + "github.com/openshift/installer/pkg/asset/installconfig/vsphere/mock" +) + +func buildPermissionGroup(authManagerMock *mock.MockAuthManager, + managedObjectRef vim25types.ManagedObjectReference, + username string, + group PermissionGroupDefinition, + groupName permissionGroup, + overrideGroup *permissionGroup) { + permissionToApply := group.Permissions + if overrideGroup != nil && *overrideGroup == groupName { + permissionToApply = permissionToApply[:len(permissionToApply)-1] + } + authManagerMock.EXPECT().FetchUserPrivilegeOnEntities(gomock.Any(), []vim25types.ManagedObjectReference{ + managedObjectRef, + }, username).Return([]vim25types.UserPrivilegeResult{ + { + Privileges: permissionToApply, + }, + }, nil).AnyTimes() +} + +func buildAuthManagerClient(ctx context.Context, mockCtrl *gomock.Controller, finder Finder, username string, overrideGroup *permissionGroup) (*mock.MockAuthManager, error) { + authManagerClient := mock.NewMockAuthManager(mockCtrl) + for groupName, group := range permissions { + switch groupName { + case permissionVcenter: + vcenter, err := finder.Folder(ctx, "/") + if err != nil { + return nil, err + } + buildPermissionGroup(authManagerClient, vcenter.Reference(), username, group, groupName, overrideGroup) + case permissionDatacenter: + datacenters, err := finder.DatacenterList(ctx, "/...") + if err != nil { + return nil, err + } + for _, datacenter := range datacenters { + buildPermissionGroup(authManagerClient, datacenter.Reference(), username, group, groupName, overrideGroup) + } + case permissionDatastore: + datastores, err := finder.DatastoreList(ctx, "/...") + if err != nil { + return nil, err + } + for _, datastore := range datastores { + buildPermissionGroup(authManagerClient, datastore.Reference(), username, group, groupName, overrideGroup) + } + case permissionCluster: + clusters, err := finder.ClusterComputeResourceList(ctx, "/...") + if err != nil { + return nil, err + } + for _, cluster := range clusters { + buildPermissionGroup(authManagerClient, cluster.Reference(), username, group, groupName, overrideGroup) + } + case permissionPortgroup: + networks, err := finder.NetworkList(ctx, "/...") + if err != nil { + return nil, err + } + for _, network := range networks { + buildPermissionGroup(authManagerClient, network.Reference(), username, group, groupName, overrideGroup) + } + case permissionResourcePool: + resourcePool, err := finder.ResourcePool(ctx, "/DC0/host/DC0_C0/Resources/test-resourcepool") + if err != nil { + return nil, err + } + buildPermissionGroup(authManagerClient, resourcePool.Reference(), username, group, groupName, overrideGroup) + case permissionFolder: + var folders = []string{"/DC0/vm", "/DC0/vm/my-folder"} + for _, folder := range folders { + folder, err := finder.Folder(ctx, folder) + if err != nil { + return nil, err + } + buildPermissionGroup(authManagerClient, folder.Reference(), username, group, groupName, overrideGroup) + } + } + } + return authManagerClient, nil +} + +func TestPermissionValidate(t *testing.T) { + ctx := context.TODO() + server := mock.StartSimulator() + defer server.Close() + + client, _, err := mock.GetClient(server) + if err != nil { + t.Error(err) + return + } + + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + finder, err := mock.GetFinder(server) + if err != nil { + t.Error(err) + return + } + + rootFolder := object.NewRootFolder(client) + _, err = rootFolder.CreateFolder(ctx, "/DC0/vm/my-folder") + + resourcePools, err := finder.ResourcePoolList(ctx, "/DC0/host/DC0_C0") + if err != nil { + t.Error(err) + return + } + _, err = resourcePools[0].Create(ctx, "test-resourcepool", vim25types.DefaultResourceConfigSpec()) + if err != nil { + t.Error(err) + return + } + + validInstallConfig := validIPIInstallConfig("DC0", "") + userDefinedFolderInstallConfig := validIPIInstallConfig("DC0", "") + userDefinedFolderInstallConfig.VSphere.Folder = "/DC0/vm/my-folder" + + invalidDatacenterInstallConfig := validIPIInstallConfig("DC0", "") + invalidDatacenterInstallConfig.VSphere.Datacenter = "invalid" + + username := validInstallConfig.VSphere.Username + + validPermissionsAuthManagerClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, nil) + if err != nil { + t.Error(err) + return + } + missingPortgroupPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionPortgroup) + if err != nil { + t.Error(err) + return + } + + missingVCenterPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionVcenter) + if err != nil { + t.Error(err) + return + } + + missingClusterPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionCluster) + if err != nil { + t.Error(err) + return + } + + missingDatastorePermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionDatastore) + if err != nil { + t.Error(err) + return + } + + missingDatacenterPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionDatacenter) + if err != nil { + t.Error(err) + return + } + + missingFolderPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionFolder) + if err != nil { + t.Error(err) + return + } + + missingResourcePoolPermissionsClient, err := buildAuthManagerClient(ctx, mockCtrl, finder, username, &permissionResourcePool) + if err != nil { + t.Error(err) + return + } + + tests := []struct { + name string + installConfig *types.InstallConfig + validationMethod func(*validationContext, *types.InstallConfig) error + expectErr string + authManager AuthManager + }{ + { + name: "valid Permissions", + installConfig: validInstallConfig, + validationMethod: validateProvisioning, + authManager: validPermissionsAuthManagerClient, + }, + { + name: "missing portgroup Permissions", + installConfig: validInstallConfig, + validationMethod: validateProvisioning, + authManager: missingPortgroupPermissionsClient, + expectErr: "privileges missing for vSphere Port Group: Network.Assign", + }, + { + name: "missing vCenter Permissions", + installConfig: validInstallConfig, + validationMethod: validateProvisioning, + authManager: missingVCenterPermissionsClient, + expectErr: "privileges missing for vSphere vCenter: StorageProfile.View", + }, + { + name: "missing cluster Permissions", + installConfig: validInstallConfig, + validationMethod: validateProvisioning, + authManager: missingClusterPermissionsClient, + expectErr: "privileges missing for vSphere vCenter Cluster: VirtualMachine.Config.AddNewDisk", + }, + { + name: "missing datacenter Permissions", + installConfig: validInstallConfig, + validationMethod: validateProvisioning, + authManager: missingDatacenterPermissionsClient, + expectErr: "privileges missing for vSphere vCenter Datacenter: Folder.Delete", + }, + { + name: "missing datastore Permissions", + installConfig: validInstallConfig, + validationMethod: validateProvisioning, + authManager: missingDatastorePermissionsClient, + expectErr: "privileges missing for vSphere vCenter Datastore: InventoryService.Tagging.ObjectAttachable", + }, + { + name: "missing resource pool Permissions", + installConfig: validInstallConfig, + validationMethod: validateProvisioning, + authManager: missingResourcePoolPermissionsClient, + expectErr: "privileges missing for vSphere vCenter Resource Pool: VirtualMachine.Config.AddNewDisk", + }, + { + name: "missing user-defined folder Permissions but no folder defined", + installConfig: validInstallConfig, + validationMethod: validateProvisioning, + authManager: missingFolderPermissionsClient, + }, + { + name: "missing user-defined folder Permissions", + installConfig: userDefinedFolderInstallConfig, + validationMethod: validateProvisioning, + authManager: missingFolderPermissionsClient, + expectErr: "privileges missing for Pre-existing Virtual Machine Folder: VirtualMachine.Provisioning.DeployTemplate", + }, + { + name: "invalid defined datacenter", + installConfig: invalidDatacenterInstallConfig, + validationMethod: validateProvisioning, + authManager: validPermissionsAuthManagerClient, + expectErr: "platform.vsphere.datacenter: Invalid value: \"invalid\": datacenter 'invalid' not found", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + validationCtx := &validationContext{ + User: username, + AuthManager: test.authManager, + Finder: finder, + Client: client, + } + err := test.validationMethod(validationCtx, test.installConfig) + if test.expectErr == "" { + assert.NoError(t, err) + } else { + assert.Regexp(t, test.expectErr, err) + } + }) + } +} diff --git a/pkg/asset/installconfig/vsphere/permissions.go b/pkg/asset/installconfig/vsphere/permissions.go new file mode 100644 index 00000000000..3d56c66a769 --- /dev/null +++ b/pkg/asset/installconfig/vsphere/permissions.go @@ -0,0 +1,198 @@ +package vsphere + +import ( + "context" + + vspheretypes "github.com/openshift/installer/pkg/types/vsphere" + "github.com/pkg/errors" + "github.com/vmware/govmomi/object" + "github.com/vmware/govmomi/vim25" + vim25types "github.com/vmware/govmomi/vim25/types" +) + +//go:generate mockgen -source=./permissions.go -destination=./mock/authmanager_generated.go -package=mock + +// AuthManager defines an interface to an implementation of the AuthorizationManager to facilitate mocking +type AuthManager interface { + FetchUserPrivilegeOnEntities(ctx context.Context, entities []vim25types.ManagedObjectReference, userName string) ([]vim25types.UserPrivilegeResult, error) +} + +// permissionGroup is the group of permissions needed by cluster creation, operation, or teardown. +type permissionGroup string + +// PermissionGroupDefinition defines a group of permissions and a related human friendly description +type PermissionGroupDefinition struct { + /* Permissions array of privileges which correlate with the privileges listed in docs */ + Permissions []string + /* Description friendly description of privilege group */ + Description string +} + +// PrivilegeRelevanceFunc returns true if the associated privilege group privileges should be verified +type PrivilegeRelevanceFunc func(*vspheretypes.Platform) bool + +var ( + permissionVcenter permissionGroup = "vcenter" + permissionCluster permissionGroup = "cluster" + permissionPortgroup permissionGroup = "portgroup" + permissionDatacenter permissionGroup = "datacenter" + permissionDatastore permissionGroup = "datastore" + permissionResourcePool permissionGroup = "resourcepool" + permissionFolder permissionGroup = "folder" +) + +var permissions = map[permissionGroup]PermissionGroupDefinition{ + // Base set of permissions required for cluster creation + permissionVcenter: { + Permissions: []string{ + "Cns.Searchable", + "InventoryService.Tagging.AttachTag", + "InventoryService.Tagging.CreateCategory", + "InventoryService.Tagging.CreateTag", + "InventoryService.Tagging.DeleteCategory", + "InventoryService.Tagging.DeleteTag", + "InventoryService.Tagging.EditCategory", + "InventoryService.Tagging.EditTag", + "Sessions.ValidateSession", + "StorageProfile.Update", + "StorageProfile.View", + }, + Description: "vSphere vCenter", + }, + permissionCluster: { + Permissions: []string{ + "Resource.AssignVMToPool", + "VApp.AssignResourcePool", + "VApp.Import", + "VirtualMachine.Config.AddNewDisk", + }, + Description: "vSphere vCenter Cluster", + }, + permissionPortgroup: { + Permissions: []string{ + "Network.Assign", + }, + Description: "vSphere Port Group", + }, + permissionFolder: { + Permissions: []string{ + "Resource.AssignVMToPool", + "VApp.Import", + "VirtualMachine.Config.AddExistingDisk", + "VirtualMachine.Config.AddNewDisk", + "VirtualMachine.Config.AddRemoveDevice", + "VirtualMachine.Config.AdvancedConfig", + "VirtualMachine.Config.Annotation", + "VirtualMachine.Config.CPUCount", + "VirtualMachine.Config.DiskExtend", + "VirtualMachine.Config.DiskLease", + "VirtualMachine.Config.EditDevice", + "VirtualMachine.Config.Memory", + "VirtualMachine.Config.RemoveDisk", + "VirtualMachine.Config.Rename", + "VirtualMachine.Config.ResetGuestInfo", + "VirtualMachine.Config.Resource", + "VirtualMachine.Config.Settings", + "VirtualMachine.Config.UpgradeVirtualHardware", + "VirtualMachine.Interact.GuestControl", + "VirtualMachine.Interact.PowerOff", + "VirtualMachine.Interact.PowerOn", + "VirtualMachine.Interact.Reset", + "VirtualMachine.Inventory.Create", + "VirtualMachine.Inventory.CreateFromExisting", + "VirtualMachine.Inventory.Delete", + "VirtualMachine.Provisioning.Clone", + "VirtualMachine.Provisioning.MarkAsTemplate", + "VirtualMachine.Provisioning.DeployTemplate", + }, + Description: "Pre-existing Virtual Machine Folder", + }, + permissionDatacenter: { + Permissions: []string{ + "Resource.AssignVMToPool", + "VApp.Import", + "VirtualMachine.Config.AddExistingDisk", + "VirtualMachine.Config.AddNewDisk", + "VirtualMachine.Config.AddRemoveDevice", + "VirtualMachine.Config.AdvancedConfig", + "VirtualMachine.Config.Annotation", + "VirtualMachine.Config.CPUCount", + "VirtualMachine.Config.DiskExtend", + "VirtualMachine.Config.DiskLease", + "VirtualMachine.Config.EditDevice", + "VirtualMachine.Config.Memory", + "VirtualMachine.Config.RemoveDisk", + "VirtualMachine.Config.Rename", + "VirtualMachine.Config.ResetGuestInfo", + "VirtualMachine.Config.Resource", + "VirtualMachine.Config.Settings", + "VirtualMachine.Config.UpgradeVirtualHardware", + "VirtualMachine.Interact.GuestControl", + "VirtualMachine.Interact.PowerOff", + "VirtualMachine.Interact.PowerOn", + "VirtualMachine.Interact.Reset", + "VirtualMachine.Inventory.Create", + "VirtualMachine.Inventory.CreateFromExisting", + "VirtualMachine.Inventory.Delete", + "VirtualMachine.Provisioning.Clone", + "VirtualMachine.Provisioning.DeployTemplate", + "VirtualMachine.Provisioning.MarkAsTemplate", + "Folder.Create", + "Folder.Delete", + }, + Description: "vSphere vCenter Datacenter", + }, + permissionDatastore: { + Permissions: []string{ + "Datastore.AllocateSpace", + "Datastore.Browse", + "Datastore.FileManagement", + "InventoryService.Tagging.ObjectAttachable", + }, + Description: "vSphere vCenter Datastore", + }, + permissionResourcePool: { + Permissions: []string{ + "Resource.AssignVMToPool", + "VApp.AssignResourcePool", + "VApp.Import", + "VirtualMachine.Config.AddNewDisk", + }, + Description: "vSphere vCenter Resource Pool", + }, +} + +func newAuthManager(client *vim25.Client) AuthManager { + authManager := object.NewAuthorizationManager(client) + return authManager +} + +func comparePrivileges(ctx context.Context, validationCtx *validationContext, mo vim25types.ManagedObjectReference, permissionGroup PermissionGroupDefinition) error { + authManager := validationCtx.AuthManager + derived, err := authManager.FetchUserPrivilegeOnEntities(ctx, []vim25types.ManagedObjectReference{mo}, validationCtx.User) + + if err != nil { + return errors.Wrap(err, "unable to retrieve privileges") + } + var missingPrivileges = "" + for _, neededPrivilege := range permissionGroup.Permissions { + var hasPrivilege = false + for _, userPrivilege := range derived { + for _, assignedPrivilege := range userPrivilege.Privileges { + if assignedPrivilege == neededPrivilege { + hasPrivilege = true + } + } + } + if hasPrivilege == false { + if missingPrivileges != "" { + missingPrivileges = missingPrivileges + ", " + } + missingPrivileges = missingPrivileges + neededPrivilege + } + } + if missingPrivileges != "" { + return errors.Errorf("privileges missing for %s: %s", permissionGroup.Description, missingPrivileges) + } + return nil +} diff --git a/pkg/asset/installconfig/vsphere/validation.go b/pkg/asset/installconfig/vsphere/validation.go index 8a52a3d982e..5494b5401ce 100644 --- a/pkg/asset/installconfig/vsphere/validation.go +++ b/pkg/asset/installconfig/vsphere/validation.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/vim25" vim25types "github.com/vmware/govmomi/vim25/types" "k8s.io/apimachinery/pkg/util/validation/field" @@ -19,9 +20,10 @@ import ( ) type validationContext struct { - User string - Finder Finder - Client *vim25.Client + User string + AuthManager AuthManager + Finder Finder + Client *vim25.Client } // Validate executes platform-specific validation. @@ -43,9 +45,10 @@ func getVCenterClient(deploymentZone vsphere.DeploymentZone, ic *types.InstallCo vcenter.Password) validationCtx := validationContext{ - User: vcenter.Username, - Finder: find.NewFinder(vim25Client), - Client: vim25Client, + User: vcenter.Username, + AuthManager: newAuthManager(vim25Client), + Finder: find.NewFinder(vim25Client), + Client: vim25Client, } return &validationCtx, cleanup, err } @@ -114,7 +117,11 @@ func validateMultiZoneProvisioning(validationCtx *validationContext, failureDoma return append(allErrs, field.Invalid(topologyField.Child("computeCluster"), computeCluster, "full path of cluster is required")) } computeClusterName := clusterPathParts[2] - errs := computeClusterExists(validationCtx, computeCluster, topologyField.Child("computeCluster")) + errs := validateVcenterPrivileges(validationCtx, topologyField.Child("server")) + if len(errs) > 0 { + return append(allErrs, errs...) + } + errs = computeClusterExists(validationCtx, computeCluster, topologyField.Child("computeCluster")) if len(errs) > 0 { return append(allErrs, errs...) } @@ -155,9 +162,10 @@ func ValidateForProvisioning(ic *types.InstallConfig) error { finder := NewFinder(vim25Client) validationCtx := &validationContext{ - User: ic.VSphere.Username, - Finder: finder, - Client: vim25Client, + User: ic.VSphere.Username, + AuthManager: object.NewAuthorizationManager(vim25Client), + Finder: finder, + Client: vim25Client, } return validateProvisioning(validationCtx, ic) @@ -168,6 +176,7 @@ func validateProvisioning(validationCtx *validationContext, ic *types.InstallCon platform := ic.Platform.VSphere vsphereField := field.NewPath("platform").Child("vsphere") allErrs = append(allErrs, validation.ValidateForProvisioning(platform, vsphereField)...) + allErrs = append(allErrs, validateVcenterPrivileges(validationCtx, vsphereField.Child("vcenter"))...) allErrs = append(allErrs, folderExists(validationCtx, ic.VSphere.Folder, vsphereField.Child("folder"))...) allErrs = append(allErrs, resourcePoolExists(validationCtx, ic.VSphere.ResourcePool, vsphereField.Child("resourcePool"))...) @@ -215,10 +224,16 @@ func folderExists(validationCtx *validationContext, folderPath string, fldPath * ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second) defer cancel() - _, err := finder.Folder(ctx, folderPath) + folder, err := finder.Folder(ctx, folderPath) if err != nil { return append(allErrs, field.Invalid(fldPath, folderPath, err.Error())) } + permissionGroup := permissions[permissionFolder] + + err = comparePrivileges(ctx, validationCtx, folder.Reference(), permissionGroup) + if err != nil { + return append(allErrs, field.InternalError(fldPath, err)) + } return allErrs } @@ -245,10 +260,15 @@ func validateNetwork(validationCtx *validationContext, datacenterName string, cl // Remove any trailing backslash before getting networkMoID trimmedPath := strings.TrimPrefix(dataCenter.InventoryPath, "/") - _, err = GetNetworkMo(ctx, client, finder, trimmedPath, clusterName, networkName) + network, err := GetNetworkMo(ctx, client, finder, trimmedPath, clusterName, networkName) if err != nil { return field.ErrorList{field.Invalid(fldPath, networkName, err.Error())} } + permissionGroup := permissions[permissionPortgroup] + err = comparePrivileges(ctx, validationCtx, network.Reference(), permissionGroup) + if err != nil { + return field.ErrorList{field.InternalError(fldPath, err)} + } return field.ErrorList{} } @@ -261,10 +281,13 @@ func computeClusterExists(validationCtx *validationContext, computeCluster strin ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second) defer cancel() - _, err := validationCtx.Finder.ClusterComputeResource(ctx, computeCluster) + computeClusterMo, err := validationCtx.Finder.ClusterComputeResource(ctx, computeCluster) if err != nil { return field.ErrorList{field.Invalid(fldPath, computeCluster, err.Error())} } + permissionGroup := permissions[permissionCluster] + err = comparePrivileges(ctx, validationCtx, computeClusterMo.Reference(), permissionGroup) + if err != nil { return field.ErrorList{field.InternalError(fldPath, err)} } @@ -274,6 +297,8 @@ func computeClusterExists(validationCtx *validationContext, computeCluster strin // resourcePoolExists returns an error if a resourcePool is specified in the vSphere platform but a resourcePool with that name is not found in the datacenter. func resourcePoolExists(validationCtx *validationContext, resourcePool string, fldPath *field.Path) field.ErrorList { + finder := validationCtx.Finder + // If no resourcePool is specified, skip this check as the root resourcePool will be used. if resourcePool == "" { return field.ErrorList{} @@ -282,9 +307,15 @@ func resourcePoolExists(validationCtx *validationContext, resourcePool string, f ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second) defer cancel() - if _, err := validationCtx.Finder.ResourcePool(ctx, resourcePool); err != nil { + resourcePoolMo, err := finder.ResourcePool(ctx, resourcePool) + if err != nil { return field.ErrorList{field.Invalid(fldPath, resourcePool, err.Error())} } + permissionGroup := permissions[permissionResourcePool] + err = comparePrivileges(ctx, validationCtx, resourcePoolMo.Reference(), permissionGroup) + if err != nil { + return field.ErrorList{field.InternalError(fldPath, err)} + } return field.ErrorList{} } @@ -296,10 +327,15 @@ func datacenterExists(validationCtx *validationContext, datacenterName string, f ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second) defer cancel() - _, err := finder.Datacenter(ctx, datacenterName) + dataCenter, err := finder.Datacenter(ctx, datacenterName) if err != nil { return field.ErrorList{field.Invalid(fldPath, datacenterName, err.Error())} } + permissionGroup := permissions[permissionDatacenter] + err = comparePrivileges(ctx, validationCtx, dataCenter.Reference(), permissionGroup) + if err != nil { + return field.ErrorList{field.InternalError(fldPath, err)} + } return field.ErrorList{} } @@ -336,7 +372,26 @@ func datastoreExists(validationCtx *validationContext, datacenterName string, da if datastoreMo == nil { return field.ErrorList{field.Invalid(fldPath, datastoreName, fmt.Sprintf("could not find datastore %s", datastoreName))} } + permissionGroup := permissions[permissionDatastore] + err = comparePrivileges(ctx, validationCtx, datastoreMo.Reference(), permissionGroup) + + if err != nil { + return field.ErrorList{field.InternalError(fldPath, err)} + } + return field.ErrorList{} +} +// validateVcenterPrivileges verifies the privileges associated with +func validateVcenterPrivileges(validationCtx *validationContext, fldPath *field.Path) field.ErrorList { + finder := validationCtx.Finder + ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second) + defer cancel() + rootFolder, err := finder.Folder(ctx, "/") + if err != nil { + return field.ErrorList{field.InternalError(fldPath, err)} + } + permissionGroup := permissions[permissionVcenter] + err = comparePrivileges(ctx, validationCtx, rootFolder.Reference(), permissionGroup) if err != nil { return field.ErrorList{field.InternalError(fldPath, err)} } diff --git a/pkg/asset/installconfig/vsphere/validation_test.go b/pkg/asset/installconfig/vsphere/validation_test.go index e20d7721098..e9b18b34471 100644 --- a/pkg/asset/installconfig/vsphere/validation_test.go +++ b/pkg/asset/installconfig/vsphere/validation_test.go @@ -9,7 +9,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/vmware/govmomi/object" - types2 "github.com/vmware/govmomi/vim25/types" + vim25types "github.com/vmware/govmomi/vim25/types" "k8s.io/apimachinery/pkg/util/validation/field" "github.com/openshift/installer/pkg/asset/installconfig/vsphere/mock" @@ -35,6 +35,7 @@ func validIPIInstallConfig(dcName string, fName string) *types.InstallConfig { Cluster: fmt.Sprintf("%s/%s_C0", fName, dcName), Datacenter: fmt.Sprintf("%s/%s", fName, dcName), DefaultDatastore: "LocalDS_0", + ResourcePool: "/DC0/host/DC0_C0/Resources/test-resourcepool", Network: fmt.Sprintf("%s_DVPG0", dcName), Password: "valid_password", Username: "valid_username", @@ -251,16 +252,22 @@ func TestValidate(t *testing.T) { t.Error(err) return } - _, err = resourcePools[0].Create(ctx, "test-resourcepool", types2.DefaultResourceConfigSpec()) + _, err = resourcePools[0].Create(ctx, "test-resourcepool", vim25types.DefaultResourceConfigSpec()) + if err != nil { + t.Error(err) + return + } + validPermissionsAuthManagerClient, err := buildAuthManagerClient(ctx, ctrl, finder, "test_username", nil) if err != nil { t.Error(err) return } validationCtx := &validationContext{ - User: "test_username", - Finder: finder, - Client: client, + User: "test_username", + AuthManager: validPermissionsAuthManagerClient, + Finder: finder, + Client: client, } for _, test := range tests {