VYPR
High severity8.4OSV Advisory· Published Apr 11, 2025· Updated Apr 15, 2026

CVE-2025-23389

CVE-2025-23389

Description

A Improper Access Control vulnerability in SUSE rancher allows a local user to impersonate other identities through SAML Authentication on first login. This issue affects rancher: from 2.8.0 before 2.8.13, from 2.9.0 before 2.9.7, from 2.10.0 before 2.10.3.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/rancher/rancherGo
>= 2.8.0, < 2.8.132.8.13
github.com/rancher/rancherGo
>= 2.9.0, < 2.9.72.9.7
github.com/rancher/rancherGo
>= 2.10.0, < 2.10.32.10.3

Affected products

1

Patches

6
4b885322eaf9

[v2.8] Expand SAML test coverage (#49031)

https://github.com/rancher/rancherRaul Cabello MartinFeb 7, 2025via ghsa
6 files changed · +244 12
  • pkg/auth/providers/saml/saml_actions.go+1 2 modified
    @@ -60,10 +60,9 @@ func (s *Provider) testAndEnable(actionName string, action *types.Action, reques
     
     	logrus.Debugf("SAML [testAndEnable]: Setting clientState for SAML service provider %v", s.name)
     	finalRedirectURL := samlLogin.FinalRedirectURL
    -	provider.clientState.SetState(request.Response, request.Request, "Rancher_UserID", provider.userMGR.GetUser(request))
     	provider.clientState.SetState(request.Response, request.Request, "Rancher_FinalRedirectURL", finalRedirectURL)
     	provider.clientState.SetState(request.Response, request.Request, "Rancher_Action", testAndEnableAction)
    -	idpRedirectURL, err := provider.HandleSamlLogin(request.Response, request.Request)
    +	idpRedirectURL, err := provider.HandleSamlLogin(request.Response, request.Request, provider.userMGR.GetUser(request))
     	if err != nil {
     		return err
     	}
    
  • pkg/auth/providers/saml/saml_client.go+38 8 modified
    @@ -18,6 +18,7 @@ import (
     	"time"
     
     	"github.com/crewjam/saml"
    +	"github.com/golang-jwt/jwt"
     	"github.com/gorilla/mux"
     	responsewriter "github.com/rancher/apiserver/pkg/middleware"
     	v32 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
    @@ -267,20 +268,26 @@ func (s *Provider) getSamlPrincipals(config *v32.SamlConfig, samlData map[string
     func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, assertion *saml.Assertion) {
     	var groupPrincipals []v3.Principal
     	var userPrincipal v3.Principal
    -
    -	if relayState := r.Form.Get("RelayState"); relayState != "" {
    -		// delete the cookie
    -		s.clientState.DeleteState(w, r, relayState)
    -	}
    -
    +	var userID string
     	redirectURL := s.clientState.GetState(r, "Rancher_FinalRedirectURL")
     	rancherAction := s.clientState.GetState(r, "Rancher_Action")
     	if rancherAction == loginAction {
     		redirectURL += "/login?"
     	} else if rancherAction == testAndEnableAction {
    +		var err error
    +		userID, err = s.getUserIdFromRelayStateCookie(r)
    +		if err != nil {
    +			log.Errorf("SAML: Error getting state from cookie: %v", err)
    +			http.Redirect(w, r, redirectURL+"errorCode=500", http.StatusFound)
    +			return
    +		}
     		// the first query param is config=saml_provider_name set by UI
     		redirectURL += "&"
     	}
    +	if relayState := r.Form.Get("RelayState"); relayState != "" {
    +		// delete the cookie
    +		s.clientState.DeleteState(w, r, relayState)
    +	}
     
     	samlData := make(map[string][]string)
     
    @@ -324,7 +331,6 @@ func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, a
     		return
     	}
     
    -	userID := s.clientState.GetState(r, "Rancher_UserID")
     	if userID != "" && rancherAction == testAndEnableAction {
     		user, err := s.userMGR.SetPrincipalOnCurrentUserByUserID(userID, userPrincipal)
     		if err != nil && user == nil {
    @@ -354,7 +360,6 @@ func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, a
     			http.Redirect(w, r, redirectURL+"errorCode=500", http.StatusFound)
     		}
     		// delete the cookies
    -		s.clientState.DeleteState(w, r, "Rancher_UserID")
     		s.clientState.DeleteState(w, r, "Rancher_Action")
     		redirectURL := s.clientState.GetState(r, "Rancher_FinalRedirectURL")
     		s.clientState.DeleteState(w, r, "Rancher_FinalRedirectURL")
    @@ -495,3 +500,28 @@ func (s *Provider) setRancherToken(w http.ResponseWriter, tokenMGR *tokens.Manag
     
     	return nil
     }
    +
    +func (s *Provider) getUserIdFromRelayStateCookie(r *http.Request) (string, error) {
    +	userID := ""
    +	// The state is stored in a cookie, which has the relay state as the key and a JWT token containing the userID as the value
    +	if relayState := r.Form.Get("RelayState"); relayState != "" {
    +		relayStateCookie := s.clientState.GetState(r, relayState)
    +		jwtParser := jwt.Parser{
    +			ValidMethods: []string{jwt.SigningMethodHS256.Name},
    +		}
    +		token, err := jwtParser.Parse(relayStateCookie, func(t *jwt.Token) (interface{}, error) {
    +			secretBlock := x509.MarshalPKCS1PrivateKey(s.serviceProvider.Key)
    +			return secretBlock, nil
    +		})
    +		if err != nil {
    +			return "", fmt.Errorf("error parsing relay state token: %w", err)
    +		}
    +		if !token.Valid {
    +			return "", fmt.Errorf("invalid token")
    +		}
    +		claims := token.Claims.(jwt.MapClaims)
    +		userID, _ = claims[rancherUserID].(string)
    +	}
    +
    +	return userID, nil
    +}
    
  • pkg/auth/providers/saml/saml_client_test.go+142 0 added
    @@ -0,0 +1,142 @@
    +package saml
    +
    +import (
    +	"crypto/rand"
    +	"crypto/rsa"
    +	"crypto/x509"
    +	"github.com/crewjam/saml"
    +	"github.com/golang-jwt/jwt"
    +	"github.com/stretchr/testify/assert"
    +	"net/http"
    +	"net/http/httptest"
    +	"testing"
    +)
    +
    +func TestGetUserIdFromRelayState(t *testing.T) {
    +	host := "http://www.rancher.com/"
    +	relayStateValue := "mockValue"
    +	mockUserID := "u-neuwrd"
    +	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    +	assert.NoError(t, err)
    +
    +	tests := map[string]struct {
    +		createRequest func() *http.Request
    +		wantUserID    string
    +		wantErr       string
    +	}{
    +		"valid userId": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +
    +				secretBlock := x509.MarshalPKCS1PrivateKey(privateKey)
    +				state := jwt.New(jwt.SigningMethodHS256)
    +				claims := state.Claims.(jwt.MapClaims)
    +				claims[rancherUserID] = mockUserID
    +
    +				signedState, err := state.SignedString(secretBlock)
    +				assert.NoError(t, err)
    +
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=" + signedState},
    +				}
    +
    +				return req
    +			},
    +			wantUserID: mockUserID,
    +		},
    +		"userId not present": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +
    +				secretBlock := x509.MarshalPKCS1PrivateKey(privateKey)
    +				state := jwt.New(jwt.SigningMethodHS256)
    +				signedState, err := state.SignedString(secretBlock)
    +				assert.NoError(t, err)
    +
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=" + signedState},
    +				}
    +
    +				return req
    +			},
    +		},
    +		"relay state not present": {
    +			createRequest: func() *http.Request {
    +				return httptest.NewRequest(http.MethodPost, host, nil)
    +			},
    +		},
    +		"invalid token": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=wrongToken"},
    +				}
    +
    +				return req
    +			},
    +			wantErr: "error parsing relay state token",
    +		},
    +		"state signed with a different key": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +
    +				anotherKey, err := rsa.GenerateKey(rand.Reader, 2048)
    +				assert.NoError(t, err)
    +				secretBlock := x509.MarshalPKCS1PrivateKey(anotherKey)
    +				state := jwt.New(jwt.SigningMethodHS256)
    +				claims := state.Claims.(jwt.MapClaims)
    +				claims[rancherUserID] = mockUserID
    +
    +				signedState, err := state.SignedString(secretBlock)
    +				assert.NoError(t, err)
    +
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=" + signedState},
    +				}
    +
    +				return req
    +			},
    +			wantErr: "signature is invalid",
    +		},
    +	}
    +
    +	for name, test := range tests {
    +		t.Run(name, func(t *testing.T) {
    +			t.Parallel()
    +			cookieStore := &ClientCookies{
    +				Name:   "token",
    +				Domain: host,
    +			}
    +			p := Provider{
    +				clientState: cookieStore,
    +				serviceProvider: &saml.ServiceProvider{
    +					Key: privateKey,
    +				},
    +			}
    +
    +			userID, err := p.getUserIdFromRelayStateCookie(test.createRequest())
    +			if test.wantErr != "" {
    +				assert.ErrorContains(t, err, test.wantErr)
    +			} else {
    +				assert.NoError(t, err)
    +			}
    +			assert.Equal(t, test.wantUserID, userID)
    +		})
    +	}
    +}
    
  • pkg/auth/providers/saml/saml_handlers.go+6 1 modified
    @@ -13,6 +13,8 @@ import (
     	log "github.com/sirupsen/logrus"
     )
     
    +const rancherUserID = "rancherUserID"
    +
     // ServeHTTP is the handler for /saml/metadata and /saml/acs endpoints
     func (s *Provider) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     	serviceProvider := s.serviceProvider
    @@ -68,7 +70,7 @@ func (s *Provider) getPossibleRequestIDs(r *http.Request) []string {
     }
     
     // HandleSamlLogin is the endpoint for /saml/login endpoint
    -func (s *Provider) HandleSamlLogin(w http.ResponseWriter, r *http.Request) (string, error) {
    +func (s *Provider) HandleSamlLogin(w http.ResponseWriter, r *http.Request, userID string) (string, error) {
     	serviceProvider := s.serviceProvider
     	if r.URL.Path == serviceProvider.AcsURL.Path {
     		return "", fmt.Errorf("don't wrap Middleware with RequireAccount")
    @@ -92,6 +94,9 @@ func (s *Provider) HandleSamlLogin(w http.ResponseWriter, r *http.Request) (stri
     	claims := state.Claims.(jwt.MapClaims)
     	claims["id"] = req.ID
     	claims["uri"] = r.URL.String()
    +	if userID != "" {
    +		claims[rancherUserID] = userID
    +	}
     	signedState, err := state.SignedString(secretBlock)
     	if err != nil {
     		http.Error(w, err.Error(), http.StatusInternalServerError)
    
  • pkg/auth/providers/saml/saml_handlers_test.go+55 0 added
    @@ -0,0 +1,55 @@
    +package saml
    +
    +import (
    +	"crypto/rand"
    +	"crypto/rsa"
    +	"crypto/x509"
    +	"github.com/crewjam/saml"
    +	"github.com/golang-jwt/jwt"
    +	"github.com/stretchr/testify/assert"
    +	"net/http"
    +	"net/http/httptest"
    +	"net/url"
    +	"testing"
    +)
    +
    +func TestHandleSamlLoginAddsRancherUserIdToCookie(t *testing.T) {
    +	host := "http://www.rancher.com/"
    +	mockUserId := "u-sidfes"
    +	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    +	assert.NoError(t, err)
    +
    +	serviceProvider := &saml.ServiceProvider{
    +		Key:         privateKey,
    +		IDPMetadata: &saml.EntityDescriptor{},
    +	}
    +	cookieStore := &ClientCookies{
    +		Name:            "token",
    +		Domain:          host,
    +		ServiceProvider: serviceProvider,
    +	}
    +	p := Provider{
    +		serviceProvider: serviceProvider,
    +		clientState:     cookieStore,
    +	}
    +	w := httptest.NewRecorder()
    +
    +	urlParams, err := p.HandleSamlLogin(w, httptest.NewRequest(http.MethodPost, host, nil), mockUserId)
    +
    +	assert.NoError(t, err)
    +	values, err := url.ParseQuery(urlParams)
    +	assert.NoError(t, err)
    +	relayState := values.Get("RelayState")
    +	// extract token from cookies. The key of the cookie is the relay state string, and the value is the token
    +	// e.g saml_DY3XlRnQsITgTsegiY-QuB38OsawU64uNZE4Q5iYCWIZOfz9YO6IYvUS=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImlkLTNkY2ViYTQ2MWE0Njg2YzRkYWEyNTZkYjI1YjZmMWFjNWE0YWY2Y2MiLCJyYW5jaGVyVXNlcklkIjoidS1zaWRmZXMiLCJ1cmkiOiJodHRwOi8vd3d3LnJhbmNoZXIuY29tLyJ9.oeUMC0d6FyNt2WlrgxsUjf4QQPIcjjpugULiQ87ep4M; Path=/; Max-Age=90; HttpOnly
    +	cookie, err := http.ParseSetCookie(w.Header().Get("Set-Cookie"))
    +	assert.NoError(t, err)
    +	assert.Equal(t, "saml_"+relayState, cookie.Name)
    +	token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) {
    +		secretBlock := x509.MarshalPKCS1PrivateKey(serviceProvider.Key)
    +		return secretBlock, nil
    +	})
    +	assert.NoError(t, err)
    +	claims, _ := token.Claims.(jwt.MapClaims)
    +	assert.Equal(t, mockUserId, claims[rancherUserID])
    +}
    
  • pkg/auth/providers/saml/saml_provider.go+2 1 modified
    @@ -127,7 +127,8 @@ func PerformSamlLogin(name string, apiContext *types.APIContext, input interface
     		provider.clientState.SetState(apiContext.Response, apiContext.Request, "Rancher_RequestID", login.RequestID)
     		provider.clientState.SetState(apiContext.Response, apiContext.Request, "Rancher_ResponseType", login.ResponseType)
     
    -		idpRedirectURL, err := provider.HandleSamlLogin(apiContext.Response, apiContext.Request)
    +		// userID is not needed for login. It's only needed for testAndEnable
    +		idpRedirectURL, err := provider.HandleSamlLogin(apiContext.Response, apiContext.Request, "")
     		if err != nil {
     			return err
     		}
    
cda77b743788

Merge pull request #49030 from raulcabello/saml-tests-2.9

https://github.com/rancher/ranchersamjustusFeb 6, 2025via ghsa
6 files changed · +244 12
  • pkg/auth/providers/saml/saml_actions.go+1 2 modified
    @@ -60,10 +60,9 @@ func (s *Provider) testAndEnable(actionName string, action *types.Action, reques
     
     	logrus.Debugf("SAML [testAndEnable]: Setting clientState for SAML service provider %v", s.name)
     	finalRedirectURL := samlLogin.FinalRedirectURL
    -	provider.clientState.SetState(request.Response, request.Request, "Rancher_UserID", provider.userMGR.GetUser(request))
     	provider.clientState.SetState(request.Response, request.Request, "Rancher_FinalRedirectURL", finalRedirectURL)
     	provider.clientState.SetState(request.Response, request.Request, "Rancher_Action", testAndEnableAction)
    -	idpRedirectURL, err := provider.HandleSamlLogin(request.Response, request.Request)
    +	idpRedirectURL, err := provider.HandleSamlLogin(request.Response, request.Request, provider.userMGR.GetUser(request))
     	if err != nil {
     		return err
     	}
    
  • pkg/auth/providers/saml/saml_client.go+38 8 modified
    @@ -18,6 +18,7 @@ import (
     	"time"
     
     	"github.com/crewjam/saml"
    +	"github.com/golang-jwt/jwt"
     	"github.com/gorilla/mux"
     	responsewriter "github.com/rancher/apiserver/pkg/middleware"
     	v32 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
    @@ -267,20 +268,26 @@ func (s *Provider) getSamlPrincipals(config *v32.SamlConfig, samlData map[string
     func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, assertion *saml.Assertion) {
     	var groupPrincipals []v3.Principal
     	var userPrincipal v3.Principal
    -
    -	if relayState := r.Form.Get("RelayState"); relayState != "" {
    -		// delete the cookie
    -		s.clientState.DeleteState(w, r, relayState)
    -	}
    -
    +	var userID string
     	redirectURL := s.clientState.GetState(r, "Rancher_FinalRedirectURL")
     	rancherAction := s.clientState.GetState(r, "Rancher_Action")
     	if rancherAction == loginAction {
     		redirectURL += "/login?"
     	} else if rancherAction == testAndEnableAction {
    +		var err error
    +		userID, err = s.getUserIdFromRelayStateCookie(r)
    +		if err != nil {
    +			log.Errorf("SAML: Error getting state from cookie: %v", err)
    +			http.Redirect(w, r, redirectURL+"errorCode=500", http.StatusFound)
    +			return
    +		}
     		// the first query param is config=saml_provider_name set by UI
     		redirectURL += "&"
     	}
    +	if relayState := r.Form.Get("RelayState"); relayState != "" {
    +		// delete the cookie
    +		s.clientState.DeleteState(w, r, relayState)
    +	}
     
     	samlData := make(map[string][]string)
     
    @@ -324,7 +331,6 @@ func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, a
     		return
     	}
     
    -	userID := s.clientState.GetState(r, "Rancher_UserID")
     	if userID != "" && rancherAction == testAndEnableAction {
     		user, err := s.userMGR.SetPrincipalOnCurrentUserByUserID(userID, userPrincipal)
     		if err != nil && user == nil {
    @@ -354,7 +360,6 @@ func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, a
     			http.Redirect(w, r, redirectURL+"errorCode=500", http.StatusFound)
     		}
     		// delete the cookies
    -		s.clientState.DeleteState(w, r, "Rancher_UserID")
     		s.clientState.DeleteState(w, r, "Rancher_Action")
     		redirectURL := s.clientState.GetState(r, "Rancher_FinalRedirectURL")
     		s.clientState.DeleteState(w, r, "Rancher_FinalRedirectURL")
    @@ -495,3 +500,28 @@ func (s *Provider) setRancherToken(w http.ResponseWriter, tokenMGR *tokens.Manag
     
     	return nil
     }
    +
    +func (s *Provider) getUserIdFromRelayStateCookie(r *http.Request) (string, error) {
    +	userID := ""
    +	// The state is stored in a cookie, which has the relay state as the key and a JWT token containing the userID as the value
    +	if relayState := r.Form.Get("RelayState"); relayState != "" {
    +		relayStateCookie := s.clientState.GetState(r, relayState)
    +		jwtParser := jwt.Parser{
    +			ValidMethods: []string{jwt.SigningMethodHS256.Name},
    +		}
    +		token, err := jwtParser.Parse(relayStateCookie, func(t *jwt.Token) (interface{}, error) {
    +			secretBlock := x509.MarshalPKCS1PrivateKey(s.serviceProvider.Key)
    +			return secretBlock, nil
    +		})
    +		if err != nil {
    +			return "", fmt.Errorf("error parsing relay state token: %w", err)
    +		}
    +		if !token.Valid {
    +			return "", fmt.Errorf("invalid token")
    +		}
    +		claims := token.Claims.(jwt.MapClaims)
    +		userID, _ = claims[rancherUserID].(string)
    +	}
    +
    +	return userID, nil
    +}
    
  • pkg/auth/providers/saml/saml_client_test.go+142 0 added
    @@ -0,0 +1,142 @@
    +package saml
    +
    +import (
    +	"crypto/rand"
    +	"crypto/rsa"
    +	"crypto/x509"
    +	"github.com/crewjam/saml"
    +	"github.com/golang-jwt/jwt"
    +	"github.com/stretchr/testify/assert"
    +	"net/http"
    +	"net/http/httptest"
    +	"testing"
    +)
    +
    +func TestGetUserIdFromRelayState(t *testing.T) {
    +	host := "http://www.rancher.com/"
    +	relayStateValue := "mockValue"
    +	mockUserID := "u-neuwrd"
    +	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    +	assert.NoError(t, err)
    +
    +	tests := map[string]struct {
    +		createRequest func() *http.Request
    +		wantUserID    string
    +		wantErr       string
    +	}{
    +		"valid userId": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +
    +				secretBlock := x509.MarshalPKCS1PrivateKey(privateKey)
    +				state := jwt.New(jwt.SigningMethodHS256)
    +				claims := state.Claims.(jwt.MapClaims)
    +				claims[rancherUserID] = mockUserID
    +
    +				signedState, err := state.SignedString(secretBlock)
    +				assert.NoError(t, err)
    +
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=" + signedState},
    +				}
    +
    +				return req
    +			},
    +			wantUserID: mockUserID,
    +		},
    +		"userId not present": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +
    +				secretBlock := x509.MarshalPKCS1PrivateKey(privateKey)
    +				state := jwt.New(jwt.SigningMethodHS256)
    +				signedState, err := state.SignedString(secretBlock)
    +				assert.NoError(t, err)
    +
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=" + signedState},
    +				}
    +
    +				return req
    +			},
    +		},
    +		"relay state not present": {
    +			createRequest: func() *http.Request {
    +				return httptest.NewRequest(http.MethodPost, host, nil)
    +			},
    +		},
    +		"invalid token": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=wrongToken"},
    +				}
    +
    +				return req
    +			},
    +			wantErr: "error parsing relay state token",
    +		},
    +		"state signed with a different key": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +
    +				anotherKey, err := rsa.GenerateKey(rand.Reader, 2048)
    +				assert.NoError(t, err)
    +				secretBlock := x509.MarshalPKCS1PrivateKey(anotherKey)
    +				state := jwt.New(jwt.SigningMethodHS256)
    +				claims := state.Claims.(jwt.MapClaims)
    +				claims[rancherUserID] = mockUserID
    +
    +				signedState, err := state.SignedString(secretBlock)
    +				assert.NoError(t, err)
    +
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=" + signedState},
    +				}
    +
    +				return req
    +			},
    +			wantErr: "signature is invalid",
    +		},
    +	}
    +
    +	for name, test := range tests {
    +		t.Run(name, func(t *testing.T) {
    +			t.Parallel()
    +			cookieStore := &ClientCookies{
    +				Name:   "token",
    +				Domain: host,
    +			}
    +			p := Provider{
    +				clientState: cookieStore,
    +				serviceProvider: &saml.ServiceProvider{
    +					Key: privateKey,
    +				},
    +			}
    +
    +			userID, err := p.getUserIdFromRelayStateCookie(test.createRequest())
    +			if test.wantErr != "" {
    +				assert.ErrorContains(t, err, test.wantErr)
    +			} else {
    +				assert.NoError(t, err)
    +			}
    +			assert.Equal(t, test.wantUserID, userID)
    +		})
    +	}
    +}
    
  • pkg/auth/providers/saml/saml_handlers.go+6 1 modified
    @@ -13,6 +13,8 @@ import (
     	log "github.com/sirupsen/logrus"
     )
     
    +const rancherUserID = "rancherUserID"
    +
     // ServeHTTP is the handler for /saml/metadata and /saml/acs endpoints
     func (s *Provider) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     	serviceProvider := s.serviceProvider
    @@ -68,7 +70,7 @@ func (s *Provider) getPossibleRequestIDs(r *http.Request) []string {
     }
     
     // HandleSamlLogin is the endpoint for /saml/login endpoint
    -func (s *Provider) HandleSamlLogin(w http.ResponseWriter, r *http.Request) (string, error) {
    +func (s *Provider) HandleSamlLogin(w http.ResponseWriter, r *http.Request, userID string) (string, error) {
     	serviceProvider := s.serviceProvider
     	if r.URL.Path == serviceProvider.AcsURL.Path {
     		return "", fmt.Errorf("don't wrap Middleware with RequireAccount")
    @@ -92,6 +94,9 @@ func (s *Provider) HandleSamlLogin(w http.ResponseWriter, r *http.Request) (stri
     	claims := state.Claims.(jwt.MapClaims)
     	claims["id"] = req.ID
     	claims["uri"] = r.URL.String()
    +	if userID != "" {
    +		claims[rancherUserID] = userID
    +	}
     	signedState, err := state.SignedString(secretBlock)
     	if err != nil {
     		http.Error(w, err.Error(), http.StatusInternalServerError)
    
  • pkg/auth/providers/saml/saml_handlers_test.go+55 0 added
    @@ -0,0 +1,55 @@
    +package saml
    +
    +import (
    +	"crypto/rand"
    +	"crypto/rsa"
    +	"crypto/x509"
    +	"github.com/crewjam/saml"
    +	"github.com/golang-jwt/jwt"
    +	"github.com/stretchr/testify/assert"
    +	"net/http"
    +	"net/http/httptest"
    +	"net/url"
    +	"testing"
    +)
    +
    +func TestHandleSamlLoginAddsRancherUserIdToCookie(t *testing.T) {
    +	host := "http://www.rancher.com/"
    +	mockUserId := "u-sidfes"
    +	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    +	assert.NoError(t, err)
    +
    +	serviceProvider := &saml.ServiceProvider{
    +		Key:         privateKey,
    +		IDPMetadata: &saml.EntityDescriptor{},
    +	}
    +	cookieStore := &ClientCookies{
    +		Name:            "token",
    +		Domain:          host,
    +		ServiceProvider: serviceProvider,
    +	}
    +	p := Provider{
    +		serviceProvider: serviceProvider,
    +		clientState:     cookieStore,
    +	}
    +	w := httptest.NewRecorder()
    +
    +	urlParams, err := p.HandleSamlLogin(w, httptest.NewRequest(http.MethodPost, host, nil), mockUserId)
    +
    +	assert.NoError(t, err)
    +	values, err := url.ParseQuery(urlParams)
    +	assert.NoError(t, err)
    +	relayState := values.Get("RelayState")
    +	// extract token from cookies. The key of the cookie is the relay state string, and the value is the token
    +	// e.g saml_DY3XlRnQsITgTsegiY-QuB38OsawU64uNZE4Q5iYCWIZOfz9YO6IYvUS=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImlkLTNkY2ViYTQ2MWE0Njg2YzRkYWEyNTZkYjI1YjZmMWFjNWE0YWY2Y2MiLCJyYW5jaGVyVXNlcklkIjoidS1zaWRmZXMiLCJ1cmkiOiJodHRwOi8vd3d3LnJhbmNoZXIuY29tLyJ9.oeUMC0d6FyNt2WlrgxsUjf4QQPIcjjpugULiQ87ep4M; Path=/; Max-Age=90; HttpOnly
    +	cookie, err := http.ParseSetCookie(w.Header().Get("Set-Cookie"))
    +	assert.NoError(t, err)
    +	assert.Equal(t, "saml_"+relayState, cookie.Name)
    +	token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) {
    +		secretBlock := x509.MarshalPKCS1PrivateKey(serviceProvider.Key)
    +		return secretBlock, nil
    +	})
    +	assert.NoError(t, err)
    +	claims, _ := token.Claims.(jwt.MapClaims)
    +	assert.Equal(t, mockUserId, claims[rancherUserID])
    +}
    
  • pkg/auth/providers/saml/saml_provider.go+2 1 modified
    @@ -127,7 +127,8 @@ func PerformSamlLogin(name string, apiContext *types.APIContext, input interface
     		provider.clientState.SetState(apiContext.Response, apiContext.Request, "Rancher_RequestID", login.RequestID)
     		provider.clientState.SetState(apiContext.Response, apiContext.Request, "Rancher_ResponseType", login.ResponseType)
     
    -		idpRedirectURL, err := provider.HandleSamlLogin(apiContext.Response, apiContext.Request)
    +		// userID is not needed for login. It's only needed for testAndEnable
    +		idpRedirectURL, err := provider.HandleSamlLogin(apiContext.Response, apiContext.Request, "")
     		if err != nil {
     			return err
     		}
    
f36b896a9944

Expand SAML test coverage (#48964)

https://github.com/rancher/rancherRaul Cabello MartinFeb 3, 2025via ghsa
6 files changed · +245 14
  • pkg/auth/providers/saml/saml_actions.go+1 2 modified
    @@ -64,11 +64,10 @@ func (s *Provider) testAndEnable(actionName string, action *types.Action, reques
     	logrus.Debugf("SAML [testAndEnable]: Final redirect will be (%v)", finalRedirectURL)
     
     	provider.clientState.SetPath(provider.serviceProvider.AcsURL.Path)
    -	provider.clientState.SetState(request.Response, request.Request, "Rancher_UserID", provider.userMGR.GetUser(request))
     	provider.clientState.SetState(request.Response, request.Request, "Rancher_FinalRedirectURL", finalRedirectURL)
     	provider.clientState.SetState(request.Response, request.Request, "Rancher_Action", testAndEnableAction)
     
    -	idpRedirectURL, err := provider.HandleSamlLogin(request.Response, request.Request)
    +	idpRedirectURL, err := provider.HandleSamlLogin(request.Response, request.Request, provider.userMGR.GetUser(request))
     	if err != nil {
     		return err
     	}
    
  • pkg/auth/providers/saml/saml_client.go+38 9 modified
    @@ -18,6 +18,7 @@ import (
     	"time"
     
     	"github.com/crewjam/saml"
    +	"github.com/golang-jwt/jwt"
     	"github.com/gorilla/mux"
     	responsewriter "github.com/rancher/apiserver/pkg/middleware"
     	v32 "github.com/rancher/rancher/pkg/apis/management.cattle.io/v3"
    @@ -307,7 +308,6 @@ func (s *Provider) FinalizeSamlLogout(w http.ResponseWriter, r *http.Request) {
     
     	redirectURL := s.clientState.GetState(r, "Rancher_FinalRedirectURL")
     
    -	s.clientState.DeleteState(w, r, "Rancher_UserID")
     	s.clientState.DeleteState(w, r, "Rancher_Action")
     	s.clientState.DeleteState(w, r, "Rancher_FinalRedirectURL")
     
    @@ -358,12 +358,7 @@ func (s *Provider) FinalizeSamlLogout(w http.ResponseWriter, r *http.Request) {
     func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, assertion *saml.Assertion) {
     	var groupPrincipals []v3.Principal
     	var userPrincipal v3.Principal
    -
    -	if relayState := r.Form.Get("RelayState"); relayState != "" {
    -		// delete the cookie
    -		s.clientState.DeleteState(w, r, relayState)
    -	}
    -
    +	var userID string
     	redirectURL := s.clientState.GetState(r, "Rancher_FinalRedirectURL")
     	rancherAction := s.clientState.GetState(r, "Rancher_Action")
     
    @@ -373,9 +368,20 @@ func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, a
     		}
     		redirectURL += loginPath
     	} else if rancherAction == testAndEnableAction {
    +		var err error
    +		userID, err = s.getUserIdFromRelayStateCookie(r)
    +		if err != nil {
    +			log.Errorf("SAML: Error getting state from cookie: %v", err)
    +			http.Redirect(w, r, redirectURL+"errorCode=500", http.StatusFound)
    +			return
    +		}
     		// the first query param is config=saml_provider_name set by UI
     		redirectURL += "&"
     	}
    +	if relayState := r.Form.Get("RelayState"); relayState != "" {
    +		// delete the cookie
    +		s.clientState.DeleteState(w, r, relayState)
    +	}
     
     	samlData := make(map[string][]string)
     
    @@ -419,7 +425,6 @@ func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, a
     		return
     	}
     
    -	userID := s.clientState.GetState(r, "Rancher_UserID")
     	if userID != "" && rancherAction == testAndEnableAction {
     		user, err := s.userMGR.SetPrincipalOnCurrentUserByUserID(userID, userPrincipal)
     		if err != nil && user == nil {
    @@ -449,7 +454,6 @@ func (s *Provider) HandleSamlAssertion(w http.ResponseWriter, r *http.Request, a
     			http.Redirect(w, r, redirectURL+"errorCode=500", http.StatusFound)
     		}
     		// delete the cookies
    -		s.clientState.DeleteState(w, r, "Rancher_UserID")
     		s.clientState.DeleteState(w, r, "Rancher_Action")
     		redirectURL := s.clientState.GetState(r, "Rancher_FinalRedirectURL")
     		s.clientState.DeleteState(w, r, "Rancher_FinalRedirectURL")
    @@ -588,3 +592,28 @@ func (s *Provider) setRancherToken(w http.ResponseWriter, tokenMGR *tokens.Manag
     
     	return nil
     }
    +
    +func (s *Provider) getUserIdFromRelayStateCookie(r *http.Request) (string, error) {
    +	userID := ""
    +	// The state is stored in a cookie, which has the relay state as the key and a JWT token containing the userID as the value
    +	if relayState := r.Form.Get("RelayState"); relayState != "" {
    +		relayStateCookie := s.clientState.GetState(r, relayState)
    +		jwtParser := jwt.Parser{
    +			ValidMethods: []string{jwt.SigningMethodHS256.Name},
    +		}
    +		token, err := jwtParser.Parse(relayStateCookie, func(t *jwt.Token) (interface{}, error) {
    +			secretBlock := x509.MarshalPKCS1PrivateKey(s.serviceProvider.Key)
    +			return secretBlock, nil
    +		})
    +		if err != nil {
    +			return "", fmt.Errorf("error parsing relay state token: %w", err)
    +		}
    +		if !token.Valid {
    +			return "", fmt.Errorf("invalid token")
    +		}
    +		claims := token.Claims.(jwt.MapClaims)
    +		userID, _ = claims[rancherUserID].(string)
    +	}
    +
    +	return userID, nil
    +}
    
  • pkg/auth/providers/saml/saml_client_test.go+142 0 added
    @@ -0,0 +1,142 @@
    +package saml
    +
    +import (
    +	"crypto/rand"
    +	"crypto/rsa"
    +	"crypto/x509"
    +	"github.com/crewjam/saml"
    +	"github.com/golang-jwt/jwt"
    +	"github.com/stretchr/testify/assert"
    +	"net/http"
    +	"net/http/httptest"
    +	"testing"
    +)
    +
    +func TestGetUserIdFromRelayState(t *testing.T) {
    +	host := "http://www.rancher.com/"
    +	relayStateValue := "mockValue"
    +	mockUserID := "u-neuwrd"
    +	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    +	assert.NoError(t, err)
    +
    +	tests := map[string]struct {
    +		createRequest func() *http.Request
    +		wantUserID    string
    +		wantErr       string
    +	}{
    +		"valid userId": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +
    +				secretBlock := x509.MarshalPKCS1PrivateKey(privateKey)
    +				state := jwt.New(jwt.SigningMethodHS256)
    +				claims := state.Claims.(jwt.MapClaims)
    +				claims[rancherUserID] = mockUserID
    +
    +				signedState, err := state.SignedString(secretBlock)
    +				assert.NoError(t, err)
    +
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=" + signedState},
    +				}
    +
    +				return req
    +			},
    +			wantUserID: mockUserID,
    +		},
    +		"userId not present": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +
    +				secretBlock := x509.MarshalPKCS1PrivateKey(privateKey)
    +				state := jwt.New(jwt.SigningMethodHS256)
    +				signedState, err := state.SignedString(secretBlock)
    +				assert.NoError(t, err)
    +
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=" + signedState},
    +				}
    +
    +				return req
    +			},
    +		},
    +		"relay state not present": {
    +			createRequest: func() *http.Request {
    +				return httptest.NewRequest(http.MethodPost, host, nil)
    +			},
    +		},
    +		"invalid token": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=wrongToken"},
    +				}
    +
    +				return req
    +			},
    +			wantErr: "error parsing relay state token",
    +		},
    +		"state signed with a different key": {
    +			createRequest: func() *http.Request {
    +				req := httptest.NewRequest(http.MethodPost, host, nil)
    +				req.Form = map[string][]string{
    +					"RelayState": {relayStateValue},
    +				}
    +				assert.NoError(t, req.ParseForm())
    +
    +				anotherKey, err := rsa.GenerateKey(rand.Reader, 2048)
    +				assert.NoError(t, err)
    +				secretBlock := x509.MarshalPKCS1PrivateKey(anotherKey)
    +				state := jwt.New(jwt.SigningMethodHS256)
    +				claims := state.Claims.(jwt.MapClaims)
    +				claims[rancherUserID] = mockUserID
    +
    +				signedState, err := state.SignedString(secretBlock)
    +				assert.NoError(t, err)
    +
    +				req.Header = map[string][]string{
    +					"Cookie": {"saml_Rancher_FinalRedirectURL=redirectURL;saml_" + relayStateValue + "=" + signedState},
    +				}
    +
    +				return req
    +			},
    +			wantErr: "signature is invalid",
    +		},
    +	}
    +
    +	for name, test := range tests {
    +		t.Run(name, func(t *testing.T) {
    +			t.Parallel()
    +			cookieStore := &ClientCookies{
    +				Name:   "token",
    +				Domain: host,
    +			}
    +			p := Provider{
    +				clientState: cookieStore,
    +				serviceProvider: &saml.ServiceProvider{
    +					Key: privateKey,
    +				},
    +			}
    +
    +			userID, err := p.getUserIdFromRelayStateCookie(test.createRequest())
    +			if test.wantErr != "" {
    +				assert.ErrorContains(t, err, test.wantErr)
    +			} else {
    +				assert.NoError(t, err)
    +			}
    +			assert.Equal(t, test.wantUserID, userID)
    +		})
    +	}
    +}
    
  • pkg/auth/providers/saml/saml_handlers.go+6 1 modified
    @@ -13,6 +13,8 @@ import (
     	log "github.com/sirupsen/logrus"
     )
     
    +const rancherUserID = "rancherUserID"
    +
     // ServeHTTP is the handler for /saml/metadata and /saml/acs endpoints
     func (s *Provider) ServeHTTP(w http.ResponseWriter, r *http.Request) {
     	serviceProvider := s.serviceProvider
    @@ -100,7 +102,7 @@ func (s *Provider) getPossibleRequestIDs(r *http.Request) []string {
     }
     
     // HandleSamlLogin is the endpoint for /saml/login endpoint
    -func (s *Provider) HandleSamlLogin(w http.ResponseWriter, r *http.Request) (string, error) {
    +func (s *Provider) HandleSamlLogin(w http.ResponseWriter, r *http.Request, userID string) (string, error) {
     	serviceProvider := s.serviceProvider
     	if r.URL.Path == serviceProvider.AcsURL.Path {
     		return "", fmt.Errorf("don't wrap Middleware with RequireAccount")
    @@ -124,6 +126,9 @@ func (s *Provider) HandleSamlLogin(w http.ResponseWriter, r *http.Request) (stri
     	claims := state.Claims.(jwt.MapClaims)
     	claims["id"] = req.ID
     	claims["uri"] = r.URL.String()
    +	if userID != "" {
    +		claims[rancherUserID] = userID
    +	}
     
     	signedState, err := state.SignedString(secretBlock)
     	if err != nil {
    
  • pkg/auth/providers/saml/saml_handlers_test.go+56 0 added
    @@ -0,0 +1,56 @@
    +package saml
    +
    +import (
    +	"crypto/rand"
    +	"crypto/rsa"
    +	"crypto/x509"
    +	"github.com/crewjam/saml"
    +	"github.com/golang-jwt/jwt"
    +	"github.com/stretchr/testify/assert"
    +	"net/http"
    +	"net/http/httptest"
    +	"net/url"
    +	"testing"
    +)
    +
    +func TestHandleSamlLoginAddsRancherUserIdToCookie(t *testing.T) {
    +	host := "http://www.rancher.com/"
    +	mockUserId := "u-sidfes"
    +	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
    +	assert.NoError(t, err)
    +
    +	serviceProvider := &saml.ServiceProvider{
    +		Key:         privateKey,
    +		IDPMetadata: &saml.EntityDescriptor{},
    +	}
    +	cookieStore := &ClientCookies{
    +		Name:            "token",
    +		Domain:          host,
    +		Path:            "/",
    +		ServiceProvider: serviceProvider,
    +	}
    +	p := Provider{
    +		serviceProvider: serviceProvider,
    +		clientState:     cookieStore,
    +	}
    +	w := httptest.NewRecorder()
    +
    +	urlParams, err := p.HandleSamlLogin(w, httptest.NewRequest(http.MethodPost, host, nil), mockUserId)
    +
    +	assert.NoError(t, err)
    +	values, err := url.ParseQuery(urlParams)
    +	assert.NoError(t, err)
    +	relayState := values.Get("RelayState")
    +	// extract token from cookies. The key of the cookie is the relay state string, and the value is the token
    +	// e.g saml_DY3XlRnQsITgTsegiY-QuB38OsawU64uNZE4Q5iYCWIZOfz9YO6IYvUS=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImlkLTNkY2ViYTQ2MWE0Njg2YzRkYWEyNTZkYjI1YjZmMWFjNWE0YWY2Y2MiLCJyYW5jaGVyVXNlcklkIjoidS1zaWRmZXMiLCJ1cmkiOiJodHRwOi8vd3d3LnJhbmNoZXIuY29tLyJ9.oeUMC0d6FyNt2WlrgxsUjf4QQPIcjjpugULiQ87ep4M; Path=/; Max-Age=90; HttpOnly
    +	cookie, err := http.ParseSetCookie(w.Header().Get("Set-Cookie"))
    +	assert.NoError(t, err)
    +	assert.Equal(t, "saml_"+relayState, cookie.Name)
    +	token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) {
    +		secretBlock := x509.MarshalPKCS1PrivateKey(serviceProvider.Key)
    +		return secretBlock, nil
    +	})
    +	assert.NoError(t, err)
    +	claims, _ := token.Claims.(jwt.MapClaims)
    +	assert.Equal(t, mockUserId, claims[rancherUserID])
    +}
    
  • pkg/auth/providers/saml/saml_provider.go+2 2 modified
    @@ -167,7 +167,6 @@ func (s *Provider) LogoutAll(apiContext *types.APIContext, token *v3.Token) erro
     	w := apiContext.Response
     	provider.clientState.SetPath(provider.serviceProvider.SloURL.Path)
     	provider.clientState.SetState(w, r, "Rancher_FinalRedirectURL", finalRedirectURL)
    -	provider.clientState.SetState(w, r, "Rancher_UserID", userName)
     	provider.clientState.SetState(w, r, "Rancher_Action", "logout-all")
     
     	idpRedirectURL, err := provider.HandleSamlLogout(userAtProvider, w, r)
    @@ -213,7 +212,8 @@ func PerformSamlLogin(name string, apiContext *types.APIContext, input interface
     		provider.clientState.SetState(apiContext.Response, apiContext.Request, "Rancher_RequestID", login.RequestID)
     		provider.clientState.SetState(apiContext.Response, apiContext.Request, "Rancher_ResponseType", login.ResponseType)
     
    -		idpRedirectURL, err := provider.HandleSamlLogin(apiContext.Response, apiContext.Request)
    +		// userID is not needed for login. It's only needed for testAndEnable
    +		idpRedirectURL, err := provider.HandleSamlLogin(apiContext.Response, apiContext.Request, "")
     		if err != nil {
     			return err
     		}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

14

News mentions

0

No linked articles in our index yet.