-
Notifications
You must be signed in to change notification settings - Fork 288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Validate VSphere User Privs #2907
Conversation
Skipping CI for Draft Pull Request. |
pkg/providers/vsphere/validator.go
Outdated
} | ||
|
||
func NewValidator(govc ProviderGovcClient, netClient networkutils.NetClient) *Validator { | ||
func NewValidator(govc ProviderGovcClient, netClient networkutils.NetClient, f NewVSphereClientCallable) *Validator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like I might be abusing golang here. I want a way to do the following:
- inject a vsphere client mock when testing
- when not testing, I want to automatically specify a sane default vsphere client
Is there a better way to achieve this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have a look at this, might be some inspo: #2887
pkg/providers/vsphere/validator.go
Outdated
"privsContent": vsphereAdminPrivsFile, | ||
"path": controlPlaneMachineConfig.Spec.Folder, | ||
}, | ||
// Not implementing a check on the template because I'm not sure if we always require/provide/allow template permissions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand what this section means:
https://anywhere.eks.amazonaws.com/docs/reference/vsphere/vsphere-preparation/#deploy-an-ova-template
Does it mean that a user doesn't necessarily have to have access to the templates folder?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This validation can be conditional. If the template field in vsphere config object has been configured, we can skip the validation.
If we do apply the validation, we need to do so on:
- control plane template directory
- worker node template directory
- external etcd directory
pkg/providers/vsphere/vsphere.go
Outdated
Append(sharedExtraArgs) | ||
controllerManagerExtraArgs := clusterapi.SecureTlsCipherSuitesExtraArgs(). | ||
Append(clusterapi.NodeCIDRMaskExtraArgs(&clusterSpec.Cluster.Spec.ClusterNetwork)) | ||
type VSphereUserConfig struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I bundled these things together because we were reading from env vars all through the code. I was hoping to consolidate the config into some kind of object we could pass around instead. Eventually, I'd love to move these configs into the CliConfig object.
As is, I can move this into the config/
package.
ctrl := gomock.NewController(t) | ||
vSphereClientCallable := func(ctx context.Context, host string, username string, password string, insecure bool, datacenter string) (VSphereClient, error) { | ||
vsc := mocks.NewMockVSphereClient(ctrl) | ||
vsc.EXPECT().Username().Return("foobar").AnyTimes() | ||
|
||
var privs []string | ||
err := json.Unmarshal([]byte(vsphereAdminPrivsFile), &privs) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
vsc.EXPECT().GetPrivsOnEntity(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(privs, nil).AnyTimes() | ||
return vsc, nil | ||
} | ||
validator := NewValidator(govc, netClient, vSphereClientCallable) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this code is messy. I needed a way to inject a dummy vsphere client into the validator for my tests. I am extremely open to alternative suggestions.
ae19887
to
db3e9c7
Compare
Codecov Report
@@ Coverage Diff @@
## main #2907 +/- ##
==========================================
+ Coverage 62.25% 62.72% +0.46%
==========================================
Files 334 339 +5
Lines 26865 27235 +370
==========================================
+ Hits 16724 17082 +358
+ Misses 8857 8847 -10
- Partials 1284 1306 +22
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
a2377ef
to
08465ab
Compare
08465ab
to
0512381
Compare
pkg/providers/vsphere/vsphere.go
Outdated
logger.MarkPass("EKSA_VSPHERE_USER vSphere privileges validated") | ||
} | ||
|
||
if len(vuc.EksaVsphereCPUsername) > 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verify that CP/CSI values exist and usernames are different from EKSA_VSPHERE_USERNAME
.
698ea1d
to
1c32705
Compare
pkg/providers/vsphere/validator.go
Outdated
"privsContent": config.VSphereCnsVmPrivsFile, | ||
"path": controlPlaneMachineConfig.Spec.Folder, | ||
}, | ||
// CNS-HOST-CONFIG-STORAGE I don't know what this applies to yet |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ask Vignesh or Abhinav about where this should be applied.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on a conversation with Abhinav I think this role only needs to be applied to the datastore.
10221e1
to
fa20e5c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I understand the validations overall and seems like the right approach. Let's make sure that we are able to test it against our vcenter as well.
) | ||
|
||
const ( | ||
EksavSphereUsernameKey = "EKSA_VSPHERE_USERNAME" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't these constants defined elsewhere already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like they already existed in two places:
eks-anywhere/pkg/executables/govc.go
Line 36 in b1df4a1
vSphereUsernameKey = "EKSA_VSPHERE_USERNAME" |
EksavSphereUsernameKey = "EKSA_VSPHERE_USERNAME" |
It's also defined in many different test files. I feel like a good first step is making executables/govc
import and use the const from config to button things up, and we can get the test usages later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a case where we need to keep the validation of env vars in govc somehow and pass in parameters to the check setup method to not have multiple instances of these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. I've filed concerns in a ticket here.
} | ||
|
||
//go:embed static/globalPrivs.json | ||
var VSphereGlobalPrivsFile string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also read the entire folder as a filesystem and read them at the time that we are doing the specific validation. This is okay but makes it one more place to add if we need to add a new group of privs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually did that originally, but it meant I didn't get linting errors if a particular file wasn't present. I'm kind of paranoid about forgetting to bundle static assets in builds because python makes it suuuper easy to shoot yourself in the foot this way when creating packages.
Agreed it's not great to need to add each new file here.
var VSphereReadOnlyPrivs string | ||
|
||
func NewVsphereUserConfig() *VSphereUserConfig { | ||
eksaVsphereUsername := os.Getenv(EksavSphereUsernameKey) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to do validations of whether they are empty or not, or will the govmomi api handle it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
already in place 👍
#2907 (comment)
pkg/dependencies/factory.go
Outdated
@@ -1046,8 +1046,12 @@ func (f *Factory) WithVSphereValidator() *Factory { | |||
if f.dependencies.VSphereValidator != nil { | |||
return nil | |||
} | |||
|
|||
f.dependencies.VSphereValidator = vsphere.NewValidator(f.dependencies.Govc, &networkutils.DefaultNetClient{}) | |||
vb := vsphere.NewValidatorBuilder( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take a look at what we adopted now, we decided not to use a validator builder but instead pull the clients as part of the validator methods: #2887
if err := os.Setenv(vSpherePasswordKey, vSpherePassword); err != nil { | ||
return fmt.Errorf("unable to set %s: %v", EksavSpherePasswordKey, err) | ||
return fmt.Errorf("unable to set %s: %v", config.EksavSpherePasswordKey, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this where the above env vars get validated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup 👍
} | ||
|
||
func (vsc *VMOMIClient) GetPrivsOnEntity(ctx context.Context, path string, objType string, username string) ([]string, error) { | ||
var objRef types.ManagedObjectReference |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does this object ref refer to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be a ref to an object in vsphere. I've tried renaming it, let me know if you think it's clearer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the official name that vcenter uses? I might like resource
more otherwise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion here and am happy to go with whatever you think is best.
In general, I really don't like "object" for variable names because it's so vague. At the same time, golang's naming conventions go against a lot of what I'm used to, so I've been tossing my personal preferences out the window lately. 😅
I unfortunately do see "object" in a lot of the docs, but again am happy to go with whatever you think is best:
https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.vcenterhost.doc/GUID-031BDB12-D3B2-4E2D-80E6-604F304B4D0C.html
I am guessing that these persistent references to "objects" in the govmomi code derive from the "object" language in the vsphere docs.
pkg/providers/vsphere/validator.go
Outdated
return nil | ||
} | ||
|
||
func diffPrivs(requiredPrivs []string, hasPrivs []string) []string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Diff makes it sound like we are running a diff, but this is almost a subset of a diff imo
func diffPrivs(requiredPrivs []string, hasPrivs []string) []string { | |
func checkRequiredPrivs(requiredPrivs []string, hasPrivs []string) []string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
committed locally 👍
f9efe0d
to
e5c144f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left some more comments, let me know what you think
pkg/dependencies/factory.go
Outdated
v := vsphere.NewValidator( | ||
f.dependencies.Govc, | ||
&networkutils.DefaultNetClient{}, | ||
&vsphere.VMOMIClientBuilder{}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even tho this doesn't contain anything, I would prefer creating a constructor for it. That way, this struct can be unexported with the methods being the only ones exported
pkg/providers/vsphere/vsphere.go
Outdated
if err := p.validator.validateCSIUserPrivs(ctx, vSphereClusterSpec, vuc); err != nil { | ||
return err | ||
} else { | ||
logger.MarkPass("EKSA_VSPHERE_CSI_USER vSphere privileges validated") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the CSI and CP user privs are just a subset of the main user? No other new ones here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation mentions a few permissions that are not included in the set assigned to EKSA_VSPHERE_USERNAME
per our docs. However, I was not able to find anything broken in the driver when deploying without them. Presumably the majority of our users have been deploying without them and we haven't heard any complaints?
Aside from those one or two perms, the CSI and CP user privs are just subsets of EKSA_VSPHERE_USER's perms.
I think in this situation I'd rather the validator be more permissive than restrictive until we can find an example of the software breaking without the permissions. What do you think?
@@ -0,0 +1,152 @@ | |||
package vsphere |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's maybe move this to pkg/govmomi/client.go
?
FetchUserPrivilegeOnEntities(ctx context.Context, entities []types.ManagedObjectReference, userName string) ([]types.UserPrivilegeResult, error) | ||
} | ||
|
||
type VSphereClient interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't look like we need this interface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generate a mock object off of it for tests in vsphere_test.go
.
vsc := mocks.NewMockVSphereClient(ctrl) |
After refactoring to pkg/govmomi/client.go
it will look a bit different, but I think I still want that mock object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am wondering if we are interfacing too much here but I can't think of an alternate solution except for maybe passing in values like datacenter on each validate command so we can potentially remove the need of a builder. We can brainstorm maybe as I want to avoid overcomplicating and it getting stuck in code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm. Yeah let's brain storm in office hours today?
|
||
type VMOMIClientBuilder struct{} | ||
|
||
func (*VMOMIClientBuilder) Build(ctx context.Context, host string, username string, password string, insecure bool, datacenter string) (VSphereClient, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to unit test this function, even if we are mocking some of the calls. The govmomi.NewClient
call should be able to create a client with fake creds right? If not, that might be something we need to create a builder interface for and inject into the constructor of this client builder.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I've been going back and forth on this. The test will just end up verifying that a bunch of mocks are called correctly. It seemed like a lot of extra code to test ~15loc, but you're right that we should have something around it.
Also has the benefit of pushing us towards structuring the builder better with a DI approach.
f := fields{ | ||
AuthorizationManager: am, | ||
Finder: finder, | ||
Path: tt.path, | ||
} | ||
tt.prepare(&f) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
f := fields{ | |
AuthorizationManager: am, | |
Finder: finder, | |
Path: tt.path, | |
} | |
tt.prepare(&f) | |
f := &fields{ | |
AuthorizationManager: am, | |
Finder: finder, | |
Path: tt.path, | |
} | |
tt.prepare(f) |
t.Fatal("Missing error") | ||
} else if !tt.wantErr && !reflect.DeepEqual(privs, wantPrivs) { | ||
t.Fatalf("privs = %v, want %v", privs, wantPrivs) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can probably make use of gomega library here with the assertions. An example that I worked on recently: https://github.com/aws/eks-anywhere/blob/main/pkg/providers/snow/reconciler/clientbuilder_test.go#L117-L121
pkg/providers/vsphere/validator.go
Outdated
}, | ||
} | ||
|
||
var newRas []RequiredAccess |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does newRas mean? Maybe we need to modify the naming here between ras and newRas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These names are so bad, haha. I'll make them better.
pkg/providers/vsphere/validator.go
Outdated
} | ||
|
||
func checkRequiredPrivs(requiredPrivs []string, hasPrivs []string) []string { | ||
hp := map[string]int{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that we are using this as a hashset basically. I would suggest using struct{}
or interface{}
to show that there is no meaning to the value of the map, and it also doesn't take up any memory.
hp := map[string]int{} | |
hp := map[string]interface{} |
pkg/providers/vsphere/vsphere.go
Outdated
@@ -149,22 +142,34 @@ type ClusterResourceSetManager interface { | |||
ForceUpdate(ctx context.Context, name, namespace string, managementCluster, workloadCluster *types.Cluster) error | |||
} | |||
|
|||
type ValidatorBuilder interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to avoid a ValidatorBuilder
now that #2887 is merged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh i meant to strip this out, yes 👍
pkg/providers/vsphere/vsphere.go
Outdated
if err := p.validator.validateUserPrivs(ctx, vSphereClusterSpec, vuc); err != nil { | ||
return err | ||
} else { | ||
logger.MarkPass("EKSA_VSPHERE_USER vSphere privileges validated") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these should probably reference "USERNAME" if that's how we documented and read them, and then maybe we can say the following instead. Or better if we can just output the value of the env var below instead
logger.MarkPass("EKSA_VSPHERE_USER vSphere privileges validated") | |
logger.MarkPass("EKSA_VSPHERE_USERNAME user vSphere privileges validated") |
pkg/providers/vsphere/vsphere.go
Outdated
if err := p.validator.validateUserPrivs(ctx, vSphereClusterSpec, vuc); err != nil { | ||
return err | ||
} else { | ||
logger.MarkPass("EKSA_VSPHERE_USERNAME vSphere privileges validated") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Let's print the values of this env var and add "user" after that name
ce4181a
to
637fe26
Compare
637fe26
to
7a9065b
Compare
/approve |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: jonathanmeier5 The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
This reverts commit 3a86f86.
Issue #, if available:
#2744
Description of changes:
Add a preflight check to validate vSphere user's permissions.
Testing (if applicable):
I have tested creating clusters using the permissions I defined following our docs. I'm concerned that my test environment might not reflect real world use cases in some way that I am not aware of.
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.