diff --git a/clients/rancher/auth/auth.go b/clients/rancher/auth/auth.go new file mode 100644 index 0000000..38e4dfe --- /dev/null +++ b/clients/rancher/auth/auth.go @@ -0,0 +1,23 @@ +package auth + +import ( + "github.com/rancher/shepherd/clients/rancher/auth/openldap" + management "github.com/rancher/shepherd/clients/rancher/generated/management/v3" + "github.com/rancher/shepherd/pkg/session" +) + +type Client struct { + OLDAP *openldap.OLDAPClient +} + +// NewAuth constructs the Auth Provider Struct +func NewClient(mgmt *management.Client, session *session.Session) (*Client, error) { + oLDAP, err := openldap.NewOLDAP(mgmt, session) + if err != nil { + return nil, err + } + + return &Client{ + OLDAP: oLDAP, + }, nil +} diff --git a/clients/rancher/auth/authprovider.go b/clients/rancher/auth/authprovider.go new file mode 100644 index 0000000..efd10f1 --- /dev/null +++ b/clients/rancher/auth/authprovider.go @@ -0,0 +1,13 @@ +package auth + +type Provider string + +const ( + LocalAuth Provider = "local" + OpenLDAPAuth Provider = "openLdap" +) + +// String stringer for the AuthProvider +func (a Provider) String() string { + return string(a) +} diff --git a/clients/rancher/auth/openldap/config.go b/clients/rancher/auth/openldap/config.go new file mode 100644 index 0000000..c5e14bc --- /dev/null +++ b/clients/rancher/auth/openldap/config.go @@ -0,0 +1,44 @@ +package openldap + +const ( + ConfigurationFileKey = "openLDAP" +) + +// Config represents the OpenLDAP authentication configuration structure +// used for configuring LDAP connection parameters, user search settings, +// and group membership configuration. +type Config struct { + Hostname string `json:"hostname" yaml:"hostname"` + IP string `json:"IP" yaml:"IP"` + ServiceAccount *ServiceAccount `json:"serviceAccount" yaml:"serviceAccount"` + Groups *Groups `json:"groups" yaml:"groups"` + Users *Users `json:"users" yaml:"users"` + AccessMode string `json:"accessMode" yaml:"accessMode" default:"unrestricted"` +} + +type ServiceAccount struct { + DistinguishedName string `json:"distinguishedName" yaml:"distinguishedName"` + Password string `json:"password" yaml:"password"` +} + +// Users represents LDAP Groups, used in test scenarios for validating Groups search. +type Groups struct { + ObjectClass string `json:"objectClass" yaml:"objectClass"` + MemberMappingAttribute string `json:"memberMappingAttribute" yaml:"memberMappingAttribute"` + NestedGroupMembershipEnabled bool `json:"nestedGroupMembershipEnabled,omitempty" yaml:"nestedGroupMembershipEnabled,omitempty"` + SearchDirectGroupMemberships bool `json:"searchDirectGroupMemberships,omitempty" yaml:"searchDirectGroupMemberships,omitempty"` + SearchBase string `json:"searchBase" yaml:"searchBase"` +} + +// Users represents LDAP users, used in test scenarios for validating users search. + +type Users struct { + Admin *User `json:"admin" yaml:"admin"` + SearchBase string `json:"searchBase" yaml:"searchBase"` +} + +// User represents an LDAP user with authentication credentials, used in test scenarios for validating user authentication. +type User struct { + Password string `json:"password,omitempty" yaml:"password,omitempty"` + Username string `json:"username,omitempty" yaml:"username,omitempty"` +} diff --git a/clients/rancher/auth/openldap/openldap.go b/clients/rancher/auth/openldap/openldap.go new file mode 100644 index 0000000..7d24b82 --- /dev/null +++ b/clients/rancher/auth/openldap/openldap.go @@ -0,0 +1,132 @@ +package openldap + +import ( + "fmt" + + apisv3 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3" + + management "github.com/rancher/shepherd/clients/rancher/generated/management/v3" + "github.com/rancher/shepherd/pkg/config" + "github.com/rancher/shepherd/pkg/session" +) + +type OLDAPOperations interface { + Enable() error + Disable() error + Update(existing, updates *management.AuthConfig) (*management.AuthConfig, error) +} + +const ( + resourceType = "openldap" + schemaType = "openLdapConfigs" +) + +type OLDAPClient struct { + client *management.Client + session *session.Session + + Config *Config +} + +// NewOLDAP constructs OLDAP struct after it reads Open LDAP from the configuration file +func NewOLDAP(client *management.Client, session *session.Session) (*OLDAPClient, error) { + ldapConfig := new(Config) + config.LoadConfig(ConfigurationFileKey, ldapConfig) + + return &OLDAPClient{ + client: client, + session: session, + Config: ldapConfig, + }, nil +} + +// Enable is a method of OLDAP, makes a request to the action with the given +// configuration values +func (o *OLDAPClient) Enable() error { + var jsonResp map[string]interface{} + + url := o.newActionURL("testAndApply") + enableActionInput, err := o.newEnableInputFromConfig() + if err != nil { + return err + } + + err = o.client.Ops.DoModify("POST", url, enableActionInput, &jsonResp) + if err != nil { + return err + } + + o.session.RegisterCleanupFunc(func() error { + return o.Disable() + }) + + return nil +} + +// Update is a method of OLDAP, makes an update with the given configuration values +func (o *OLDAPClient) Update( + existing, updates *management.AuthConfig, +) (*management.AuthConfig, error) { + return o.client.AuthConfig.Update(existing, updates) +} + +// Disable is a method of OLDAP, makes a request to disable Open LDAP +func (o *OLDAPClient) Disable() error { + var jsonResp map[string]any + + url := o.newActionURL("disable") + disableActionInput := o.newDisableInput() + + return o.client.Ops.DoModify("POST", url, &disableActionInput, &jsonResp) +} + +func (o *OLDAPClient) newActionURL(action string) string { + return fmt.Sprintf( + "%v/%v/%v?action=%v", + o.client.Opts.URL, + schemaType, + resourceType, + action, + ) +} + +func (o *OLDAPClient) newEnableInputFromConfig() (*apisv3.LdapTestAndApplyInput, error) { + var resource apisv3.LdapTestAndApplyInput + + var server string + if o.Config.Hostname == "" && o.Config.IP == "" { + return nil, fmt.Errorf("open LDAP Hostname and IP are empty, please provide one of them") + } + server = o.Config.Hostname + if server == "" { + server = o.Config.IP + } + + resource.Enabled = true + resource.AccessMode = o.Config.AccessMode + + resource.UserSearchBase = o.Config.Users.SearchBase + resource.GroupSearchBase = o.Config.Groups.SearchBase + + if o.Config.Users.Admin.Username == "" || o.Config.Users.Admin.Password == "" { + return nil, fmt.Errorf("admin username or password are empty, please provide them") + } + + resource.Username = o.Config.Users.Admin.Username + resource.Password = o.Config.Users.Admin.Password + + resource.Servers = []string{server} + + resource.ServiceAccountDistinguishedName = o.Config.ServiceAccount.DistinguishedName + resource.ServiceAccountPassword = o.Config.ServiceAccount.Password + + resource.GroupMemberUserAttribute = o.Config.Groups.MemberMappingAttribute + resource.NestedGroupMembershipEnabled = o.Config.Groups.NestedGroupMembershipEnabled + resource.GroupObjectClass = o.Config.Groups.ObjectClass + + return &resource, nil +} + +func (o *OLDAPClient) newDisableInput() []byte { + return []byte(`{"action": "disable"}`) +} diff --git a/clients/rancher/client.go b/clients/rancher/client.go index 0000ac5..7cf660c 100644 --- a/clients/rancher/client.go +++ b/clients/rancher/client.go @@ -9,6 +9,8 @@ import ( "net/http" "strings" + "github.com/rancher/shepherd/clients/rancher/auth" + "github.com/pkg/errors" "github.com/rancher/norman/httperror" frameworkDynamic "github.com/rancher/shepherd/clients/dynamic" @@ -49,6 +51,8 @@ type Client struct { // CLI is the client used to interact with the Rancher CLI CLI *ranchercli.Client // Session is the session object used by the client to track all the resources being created by the client. + Auth *auth.Client + // Session is the session object used by the client to track all the resources being created by the client. Session *session.Session // Flags is the environment flags used by the client to test selectively against a rancher instance. Flags *environmentflag.EnvironmentFlags @@ -136,6 +140,12 @@ func newClient(c *Client, bearerToken string, config *Config, session *session.S } c.WranglerContext = wranglerContext + auth, err := auth.NewClient(c.Management, session) + if err != nil { + return nil, err + } + + c.Auth = auth splitBearerKey := strings.Split(bearerToken, ":") token, err := c.Management.Token.ByID(splitBearerKey[0]) @@ -220,7 +230,7 @@ func (c *Client) doAction(endpoint, action string, body []byte, output interface // AsUser accepts a user object, and then creates a token for said `user`. Then it instantiates and returns a Client using the token created. // This function uses the login action, and user must have a correct username and password combination. func (c *Client) AsUser(user *management.User) (*Client, error) { - returnedToken, err := c.login(user) + returnedToken, err := c.login(user, auth.LocalAuth) if err != nil { return nil, err } @@ -228,15 +238,15 @@ func (c *Client) AsUser(user *management.User) (*Client, error) { return NewClient(returnedToken.Token, c.Session) } -// AsUserForConfig accepts a Config and a user object, and then creates a token for said `user`. Then it instantiates and returns a Client using the token created. +// AsAuthUser accepts a user object, and then creates a token for said `user`. Then it instantiates and returns a Client using the token created. // This function uses the login action, and user must have a correct username and password combination. -func (c *Client) AsUserForConfig(rancherConfig *Config, user *management.User) (*Client, error) { - returnedToken, err := c.login(user) +func (c *Client) AsAuthUser(user *management.User, authProvider auth.Provider) (*Client, error) { + returnedToken, err := c.login(user, authProvider) if err != nil { return nil, err } - return NewClientForConfig(returnedToken.Token, rancherConfig, c.Session) + return NewClientForConfig(returnedToken.Token, c.RancherConfig, c.Session) } // ReLogin reinstantiates a Client to update its API schema. This function would be used for a non admin user that needs to be @@ -369,7 +379,7 @@ func (c *Client) GetManagementWatchInterface(schemaType string, opts metav1.List } // login uses the local authentication provider to authenticate a user and return the subsequent token. -func (c *Client) login(user *management.User) (*management.Token, error) { +func (c *Client) login(user *management.User, provider auth.Provider) (*management.Token, error) { token := &management.Token{} bodyContent, err := json.Marshal(struct { Username string `json:"username"` @@ -381,7 +391,8 @@ func (c *Client) login(user *management.User) (*management.Token, error) { if err != nil { return nil, err } - err = c.doAction("/v3-public/localProviders/local", "login", bodyContent, token) + endpoint := fmt.Sprintf("/v3-public/%vProviders/%v", provider.String(), strings.ToLower(provider.String())) + err = c.doAction(endpoint, "login", bodyContent, token) if err != nil { return nil, err } diff --git a/extensions/users/users.go b/extensions/users/users.go index ebd4e8d..9efe444 100644 --- a/extensions/users/users.go +++ b/extensions/users/users.go @@ -49,6 +49,22 @@ func UserConfig() (user *management.User) { return } +// RefreshGroupMembership is helper function that sends a POST request to user action refresh auth provider access +func RefreshGroupMembership(client *rancher.Client) error { + endpoint := fmt.Sprintf("https://%v/v3/%v?action=%v", client.RancherConfig.Host, "users", "refreshauthprovideraccess") + + var jsonResp map[string]any + + bodyContent := []byte(`{}`) + + err := client.Management.Ops.DoModify("POST", endpoint, &bodyContent, &jsonResp) + if err != nil { + return err + } + + return nil +} + // CreateUserWithRole is helper function that creates a user with a role or multiple roles func CreateUserWithRole(rancherClient *rancher.Client, user *management.User, roles ...string) (*management.User, error) { createdUser, err := rancherClient.Management.User.Create(user) @@ -76,9 +92,7 @@ func CreateUserWithRole(rancherClient *rancher.Client, user *management.User, ro // AddProjectMember is a helper function that adds a project role to `user`. It uses the watch.WatchWait to ensure BackingNamespaceCreated is true. // If a list of ResourceAttributes is given, then the function blocks until all // attributes are allowed by SelfSubjectAccessReviews OR the function times out. -func AddProjectMember(rancherClient *rancher.Client, project *management.Project, - user *management.User, projectRole string, attrs []*authzv1.ResourceAttributes, -) error { +func AddProjectMember(rancherClient *rancher.Client, project *management.Project, user *management.User, projectRole string, attrs []*authzv1.ResourceAttributes) error { role := &management.ProjectRoleTemplateBinding{ ProjectID: project.ID, UserPrincipalID: user.PrincipalIDs[0], @@ -186,9 +200,7 @@ func RemoveProjectMember(rancherClient *rancher.Client, user *management.User) e // AddClusterRoleToUser is a helper function that adds a cluster role to `user`. // If a list of ResourceAttributes is given, then the function blocks until all // attributes are allowed by SelfSubjectAccessReviews OR the function times out. -func AddClusterRoleToUser(rancherClient *rancher.Client, cluster *management.Cluster, - user *management.User, clusterRole string, attrs []*authzv1.ResourceAttributes, -) error { +func AddClusterRoleToUser(rancherClient *rancher.Client, cluster *management.Cluster, user *management.User, clusterRole string, attrs []*authzv1.ResourceAttributes) error { role := &management.ClusterRoleTemplateBinding{ ClusterID: cluster.Resource.ID, UserPrincipalID: user.PrincipalIDs[0],