Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions pkg/clusteraccess/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
68 changes: 68 additions & 0 deletions pkg/clusteraccess/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
))
})

})

})