Merge pull request #451 from dasarinaidu/backport-openldap-v2.10

Backport PR for v210 to enable OpenLdap test automation
This commit is contained in:
dasarinaidu 2025-10-10 09:25:55 -07:00 committed by GitHub
commit 6105e162ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 248 additions and 13 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"`
}

View File

@ -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"}`)
}

View File

@ -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
}

View File

@ -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],