diff --git a/pkg/clusteraccess/access.go b/pkg/clusteraccess/access.go index 2d62f07..f24065e 100644 --- a/pkg/clusteraccess/access.go +++ b/pkg/clusteraccess/access.go @@ -344,6 +344,170 @@ func ComputeTokenRenewalTimeWithRatio(creationTime, expirationTime time.Time, ra return renewalAt } +// CreateOIDCKubeconfig creates a kubeconfig that uses the oidc-login plugin for authentication. +// The 'user' arg is used as key for the auth configuration and can be chosen freely. +// Note that this kubeconfig is meant for human users, controllers can usually not execute 'kubectl oidc-login get-token'. +func CreateOIDCKubeconfig(user, host string, caData []byte, issuer, clientID string, extraOptions ...CreateOIDCKubeconfigOption) ([]byte, error) { + opts := &CreateOIDCKubeconfigOptions{ + User: user, + Host: host, + CAData: caData, + Issuer: issuer, + ClientID: clientID, + ContextName: "cluster", + ClusterName: "cluster", + } + + for _, apply := range extraOptions { + apply(opts) + } + + return createOIDCKubeconfig(opts) +} + +func createOIDCKubeconfig(opts *CreateOIDCKubeconfigOptions) ([]byte, error) { + grantType := opts.GrantType + if grantType == "" { + grantType = GrantTypeAuto + } + exec := &clientcmdapi.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1beta1", + Command: "kubectl", + Args: []string{ + "oidc-login", + "get-token", + "--grant-type=" + string(grantType), + "--oidc-issuer-url=" + opts.Issuer, + "--oidc-client-id=" + opts.ClientID, + }, + } + if opts.ClientSecret != "" { + exec.Args = append(exec.Args, "--oidc-client-secret="+opts.ClientSecret) + } + for _, extraScope := range opts.ExtraScopes { + exec.Args = append(exec.Args, "--oidc-extra-scope="+extraScope) + } + if opts.UsePKCE { + exec.Args = append(exec.Args, "--oidc-use-pkce") + } + if opts.ForceRefresh { + exec.Args = append(exec.Args, "--force-refresh") + } + + kcfg := clientcmdapi.Config{ + APIVersion: "v1", + Kind: "Config", + Clusters: map[string]*clientcmdapi.Cluster{ + opts.ClusterName: { + Server: opts.Host, + CertificateAuthorityData: opts.CAData, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + opts.ContextName: { + Cluster: opts.ClusterName, + AuthInfo: opts.User, + }, + }, + CurrentContext: opts.ContextName, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + opts.User: { + Exec: exec, + }, + }, + } + + kcfgBytes, err := clientcmd.Write(kcfg) + if err != nil { + return nil, fmt.Errorf("error converting converting generated kubeconfig into yaml: %w", err) + } + return kcfgBytes, nil +} + +type CreateOIDCKubeconfigOptions struct { + ContextName string + ClusterName string + User string + Host string + CAData []byte + Issuer string + ClientID string + ClientSecret string + ExtraScopes []string + UsePKCE bool + ForceRefresh bool + GrantType OIDCGrantType +} + +type OIDCGrantType string + +const ( + GrantTypeAuto OIDCGrantType = "auto" + GrantTypeAuthCode OIDCGrantType = "authcode" + GrantTypeAuthCodeKeyboard OIDCGrantType = "authcode-keyboard" + GrantTypePassword OIDCGrantType = "password" + GrantTypeDeviceCode OIDCGrantType = "device-code" +) + +type CreateOIDCKubeconfigOption func(*CreateOIDCKubeconfigOptions) + +// WithExtraScope is an option for CreateOIDCKubeconfig that adds an extra scope to the oidc-login subcommand. +// This option can be used multiple times to add multiple scopes. +func WithExtraScope(scope string) CreateOIDCKubeconfigOption { + return func(opts *CreateOIDCKubeconfigOptions) { + opts.ExtraScopes = append(opts.ExtraScopes, scope) + } +} + +// UsePKCE is an option for CreateOIDCKubeconfig that enforces the use of PKCE. +func UsePKCE() CreateOIDCKubeconfigOption { + return func(opts *CreateOIDCKubeconfigOptions) { + opts.UsePKCE = true + } +} + +// ForceRefresh is an option for CreateOIDCKubeconfig that forces the refresh of the token, independent of its expiration time. +func ForceRefresh() CreateOIDCKubeconfigOption { + return func(opts *CreateOIDCKubeconfigOptions) { + opts.ForceRefresh = true + } +} + +// WithGrantType is an option for CreateOIDCKubeconfig that sets the grant type. +// Valid values are "auto", "authcode", "authcode-keyboard", "password", and "device-code". +func WithGrantType(grantType OIDCGrantType) CreateOIDCKubeconfigOption { + return func(opts *CreateOIDCKubeconfigOptions) { + opts.GrantType = grantType + } +} + +// WithClientSecret is an option for CreateOIDCKubeconfig that sets the client secret. +func WithClientSecret(clientSecret string) CreateOIDCKubeconfigOption { + return func(opts *CreateOIDCKubeconfigOptions) { + opts.ClientSecret = clientSecret + } +} + +// WithContextName allows to override the default context name "cluster" in the kubeconfig. +func WithContextName(contextName string) CreateOIDCKubeconfigOption { + return func(opts *CreateOIDCKubeconfigOptions) { + opts.ContextName = contextName + if opts.ContextName == "" { + opts.ContextName = "cluster" + } + } +} + +// WithClusterName allows to override the default cluster name "cluster" in the kubeconfig. +func WithClusterName(clusterName string) CreateOIDCKubeconfigOption { + return func(opts *CreateOIDCKubeconfigOptions) { + opts.ClusterName = clusterName + if opts.ClusterName == "" { + opts.ClusterName = "cluster" + } + } +} + // oidcTrustConfig represents the configuration for an OIDC trust relationship. // It includes the host of the Kubernetes API server, CA data for TLS verification, // and the audience for the OIDC tokens. diff --git a/pkg/clusteraccess/access_test.go b/pkg/clusteraccess/access_test.go index 286b5a8..bf515ce 100644 --- a/pkg/clusteraccess/access_test.go +++ b/pkg/clusteraccess/access_test.go @@ -462,4 +462,72 @@ var _ = Describe("ClusterAccess", func() { }) }) + Context("CreateOIDCKubeconfig", func() { + + It("should create a kubeconfig with oidc-login plugin (no options)", func() { + kcfgBytes, err := clusteraccess.CreateOIDCKubeconfig("testuser", "https://api.example.com", []byte("test-ca"), "https://example.com/oidc", "test-client-id") + Expect(err).ToNot(HaveOccurred()) + Expect(kcfgBytes).ToNot(BeEmpty()) + + kcfg, err := clientcmd.Load(kcfgBytes) + Expect(err).ToNot(HaveOccurred()) + id := "cluster" + Expect(kcfg.CurrentContext).To(Equal(id)) + Expect(kcfg.Contexts[id].Cluster).To(Equal(id)) + Expect(kcfg.Contexts[id].AuthInfo).To(Equal("testuser")) + Expect(kcfg.Clusters[id].Server).To(Equal("https://api.example.com")) + Expect(kcfg.Clusters[id].CertificateAuthorityData).To(Equal([]byte("test-ca"))) + auth := kcfg.AuthInfos["testuser"] + Expect(auth).ToNot(BeNil()) + Expect(auth.Exec).ToNot(BeNil()) + Expect(auth.Exec.Command).To(Equal("kubectl")) + Expect(auth.Exec.Args[:2]).To(Equal([]string{"oidc-login", "get-token"})) + Expect(auth.Exec.Args[2:]).To(ConsistOf( + "--oidc-issuer-url=https://example.com/oidc", + "--oidc-client-id=test-client-id", + "--grant-type=auto", + )) + }) + + It("should create a kubeconfig with oidc-login plugin (all options)", func() { + contextId := "my-context" + clusterId := "my-cluster" + kcfgBytes, err := clusteraccess.CreateOIDCKubeconfig("testuser", "https://api.example.com", []byte("test-ca"), "https://example.com/oidc", "test-client-id", + clusteraccess.WithExtraScope("foo"), + clusteraccess.WithExtraScope("bar"), + clusteraccess.UsePKCE(), + clusteraccess.ForceRefresh(), + clusteraccess.WithClientSecret("test-client-secret"), + clusteraccess.WithGrantType(clusteraccess.GrantTypePassword), + clusteraccess.WithContextName(contextId), + clusteraccess.WithClusterName(clusterId)) + Expect(err).ToNot(HaveOccurred()) + Expect(kcfgBytes).ToNot(BeEmpty()) + + kcfg, err := clientcmd.Load(kcfgBytes) + Expect(err).ToNot(HaveOccurred()) + Expect(kcfg.CurrentContext).To(Equal(contextId)) + Expect(kcfg.Contexts[contextId].Cluster).To(Equal(clusterId)) + Expect(kcfg.Contexts[contextId].AuthInfo).To(Equal("testuser")) + Expect(kcfg.Clusters[clusterId].Server).To(Equal("https://api.example.com")) + Expect(kcfg.Clusters[clusterId].CertificateAuthorityData).To(Equal([]byte("test-ca"))) + auth := kcfg.AuthInfos["testuser"] + Expect(auth).ToNot(BeNil()) + Expect(auth.Exec).ToNot(BeNil()) + Expect(auth.Exec.Command).To(Equal("kubectl")) + Expect(auth.Exec.Args[:2]).To(Equal([]string{"oidc-login", "get-token"})) + Expect(auth.Exec.Args[2:]).To(ConsistOf( + "--oidc-issuer-url=https://example.com/oidc", + "--oidc-client-id=test-client-id", + "--oidc-client-secret=test-client-secret", + "--grant-type=password", + "--oidc-extra-scope=foo", + "--oidc-extra-scope=bar", + "--oidc-use-pkce", + "--force-refresh", + )) + }) + + }) + })