diff --git a/authentication/openshift.go b/authentication/openshift.go index a1d7efa73..d4f48550a 100644 --- a/authentication/openshift.go +++ b/authentication/openshift.go @@ -63,6 +63,7 @@ type openshiftAuthenticatorConfig struct { ServiceAccount string `json:"serviceAccount"` RedirectURL string `json:"redirectURL"` CookieSecret string `json:"cookieSecret"` + SSREnabled bool `json:"ssrEnabled"` ServiceAccountCA []byte } @@ -203,6 +204,10 @@ func newOpenshiftAuthenticator(c map[string]interface{}, tenant string, return nil, errors.Wrap(err, "unable to initialize authenticator") } + if config.SSREnabled { + authenticator = openshift.NewSelfSubjectReview(config.KubeConfigPath, logger) + } + var cipher *openshift.Cipher if config.CookieSecret != "" { cipher, err = openshift.NewCipher([]byte(config.CookieSecret)) diff --git a/authentication/openshift/selfsubjectreview.go b/authentication/openshift/selfsubjectreview.go new file mode 100644 index 000000000..0cb6dd4f8 --- /dev/null +++ b/authentication/openshift/selfsubjectreview.go @@ -0,0 +1,114 @@ +package openshift + +import ( + "context" + "fmt" + "net/http" + "os" + "os/user" + "path" + "strings" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + + authenticationv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/authentication/authenticator" + k8suser "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +// SelfSubjectReview is a struct that implements the Token and Request interfaces. +type SelfSubjectReview struct { + logger log.Logger + // RemoteKubeConfigFile is the file to use to connect to a "normal" kube API server which hosts the + // TokenAccessReview.authentication.k8s.io endpoint for checking tokens. + RemoteKubeConfigFile string +} + +// NewSelfSubjectReview creates a new instance of SelfSubjectReview. +func NewSelfSubjectReview(kubeCfgFile string, logger log.Logger) authenticator.Request { + return &SelfSubjectReview{ + logger: logger, + RemoteKubeConfigFile: kubeCfgFile, + } +} + +// AuthenticateRequest implements the authenticator.Request interface. +func (s *SelfSubjectReview) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { + authHeader := req.Header.Get("Authorization") + level.Debug(s.logger).Log("msg", "Extracting token from Authorization header", "header", authHeader) + if authHeader == "" { + return nil, false, nil + } + + const bearerPrefix = "Bearer " + if !strings.HasPrefix(authHeader, bearerPrefix) { + return nil, false, fmt.Errorf("invalid authorization header format") + } + + token := strings.TrimPrefix(authHeader, bearerPrefix) + cfg, err := getConfig(s.RemoteKubeConfigFile) + if err != nil { + return nil, false, err + } + + cfg = rest.AnonymousClientConfig(cfg) + cfg.BearerToken = token + clientset, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, false, err + } + + ssr, err := clientset.AuthenticationV1().SelfSubjectReviews().Create(context.Background(), &authenticationv1.SelfSubjectReview{}, metav1.CreateOptions{}) + if err != nil { + return nil, false, err + } + + level.Debug(s.logger).Log("msg", "SelfSubjectReview", + "username", ssr.Status.UserInfo.Username, + "uid", ssr.Status.UserInfo.UID, + "groups", fmt.Sprintf("%v", ssr.Status.UserInfo.Groups), + "extra", fmt.Sprintf("%v", ssr.Status.UserInfo.Extra)) + + extra := make(map[string][]string) + for k, v := range ssr.Status.UserInfo.Extra { + extra[k] = v + } + return &authenticator.Response{ + User: &k8suser.DefaultInfo{ + Name: ssr.Status.UserInfo.Username, + UID: ssr.Status.UserInfo.UID, + Groups: ssr.Status.UserInfo.Groups, + Extra: extra, + }, + }, true, nil +} + +func getConfig(kubeconfig string) (*rest.Config, error) { + if len(kubeconfig) > 0 { + loader := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig} + return loadConfig(loader) + } + kubeconfigPath := os.Getenv(clientcmd.RecommendedConfigPathEnvVar) + if len(kubeconfigPath) == 0 { + return rest.InClusterConfig() //nolint:wrapcheck + } + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if _, ok := os.LookupEnv("HOME"); !ok { + u, err := user.Current() + if err != nil { + return nil, fmt.Errorf("could not get current user: %w", err) + } + p := path.Join(u.HomeDir, clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName) + loadingRules.Precedence = append(loadingRules.Precedence, p) + } + return loadConfig(loadingRules) +} + +func loadConfig(loader clientcmd.ClientConfigLoader) (*rest.Config, error) { + return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, nil).ClientConfig() //nolint:wrapcheck +} diff --git a/go.mod b/go.mod index 7e8190d7c..5f5c6381e 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 google.golang.org/grpc v1.68.1 google.golang.org/protobuf v1.35.2 + k8s.io/api v0.30.5 k8s.io/apimachinery v0.30.5 k8s.io/apiserver v0.30.5 k8s.io/client-go v0.30.5 @@ -188,7 +189,6 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.30.5 // indirect k8s.io/component-base v0.30.5 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect diff --git a/main.go b/main.go index af8794947..f0c9266a2 100644 --- a/main.go +++ b/main.go @@ -230,6 +230,7 @@ type tenant struct { ServiceAccount string `json:"serviceAccount"` RedirectURL string `json:"redirectURL"` CookieSecret string `json:"cookieSecret"` + SSREnabled bool `json:"ssrEnabled"` config map[string]interface{} } `json:"openshift"` Authenticator *struct {