Files
kubevela/pkg/auth/kubeconfig.go
Ai Ranthem a5606b7808
Some checks failed
CodeQL / Analyze (go) (push) Failing after 6m23s
Definition-Lint / definition-doc (push) Failing after 3m8s
E2E MultiCluster Test / detect-noop (push) Successful in 6s
E2E Test / detect-noop (push) Successful in 3s
Go / detect-noop (push) Successful in 2s
license / Check for unapproved licenses (push) Failing after 18s
Registry / publish-core-images (push) Failing after 1m4s
Unit-Test / detect-noop (push) Successful in 20s
Sync SDK / sync_sdk (push) Failing after 3m9s
Go / staticcheck (push) Successful in 6m17s
Go / check-diff (push) Failing after 19m4s
Go / check-core-image-build (push) Failing after 5m44s
Go / check-cli-image-build (push) Failing after 3m31s
Unit-Test / unit-tests (push) Failing after 13m54s
Go / lint (push) Failing after 1h53m27s
Scorecards supply-chain security / Scorecards analysis (push) Failing after 1m27s
E2E MultiCluster Test / e2e-multi-cluster-tests (v1.29) (push) Has been cancelled
E2E Test / e2e-tests (v1.29) (push) Has been cancelled
Go / check-windows (push) Has been cancelled
Chore: (deps): Update k8s to 1.29 (#6654)
* chore: update k8s to 1.29

Signed-off-by: phantomnat <w.nattadej@gmail.com>

* fix: unit test

Signed-off-by: phantomnat <w.nattadej@gmail.com>

* fix: lint

Signed-off-by: phantomnat <w.nattadej@gmail.com>

* fix: lint

Signed-off-by: phantomnat <w.nattadej@gmail.com>

* fix: e2e

Signed-off-by: phantomnat <w.nattadej@gmail.com>

* fix: lint and e2e test

Signed-off-by: phantomnat <w.nattadej@gmail.com>

* test(e2e): increase timeout

Signed-off-by: phantomnat <w.nattadej@gmail.com>

* fix e2e and scripts

Signed-off-by: AiRanthem <zhongtianyun.zty@alibaba-inc.com>

* make reviewable

Signed-off-by: AiRanthem <zhongtianyun.zty@alibaba-inc.com>

* rollback a unnecessary ut change

Signed-off-by: AiRanthem <zhongtianyun.zty@alibaba-inc.com>

* update go.mod to import merged workflow

Signed-off-by: AiRanthem <zhongtianyun.zty@alibaba-inc.com>

---------

Signed-off-by: phantomnat <w.nattadej@gmail.com>
Signed-off-by: AiRanthem <zhongtianyun.zty@alibaba-inc.com>
Co-authored-by: phantomnat <w.nattadej@gmail.com>
2025-01-03 07:54:42 +08:00

441 lines
16 KiB
Go

/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package auth
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"os"
"time"
"github.com/pkg/errors"
authenticationv1 "k8s.io/api/authentication/v1"
certificatesv1 "k8s.io/api/certificates/v1"
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/authentication/serviceaccount"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
"k8s.io/utils/ptr"
"github.com/oam-dev/kubevela/pkg/utils"
)
// DefaultExpireTime is default expire time for both X.509 and SA token apply
const DefaultExpireTime = time.Hour * 24 * 365
// KubeConfigGenerateOptions options for create KubeConfig
type KubeConfigGenerateOptions struct {
X509 *KubeConfigGenerateX509Options
ServiceAccount *KubeConfigGenerateServiceAccountOptions
}
// KubeConfigGenerateX509Options options for create X509 based KubeConfig
type KubeConfigGenerateX509Options struct {
User string
Groups []string
ExpireTime time.Duration
PrivateKeyBits int
}
// KubeConfigGenerateServiceAccountOptions options for create ServiceAccount based KubeConfig
type KubeConfigGenerateServiceAccountOptions struct {
ServiceAccountName string
ServiceAccountNamespace string
ExpireTime time.Duration
}
// KubeConfigWithUserGenerateOption option for setting user in KubeConfig
type KubeConfigWithUserGenerateOption string
// ApplyToOptions .
func (opt KubeConfigWithUserGenerateOption) ApplyToOptions(options *KubeConfigGenerateOptions) {
options.X509.User = string(opt)
}
// KubeConfigWithGroupGenerateOption option for setting group in KubeConfig
type KubeConfigWithGroupGenerateOption string
// ApplyToOptions .
func (opt KubeConfigWithGroupGenerateOption) ApplyToOptions(options *KubeConfigGenerateOptions) {
for _, group := range options.X509.Groups {
if group == string(opt) {
return
}
}
options.X509.Groups = append(options.X509.Groups, string(opt))
}
// KubeConfigWithServiceAccountGenerateOption option for setting service account in KubeConfig
type KubeConfigWithServiceAccountGenerateOption types.NamespacedName
// ApplyToOptions .
func (opt KubeConfigWithServiceAccountGenerateOption) ApplyToOptions(options *KubeConfigGenerateOptions) {
options.X509 = nil
options.ServiceAccount = &KubeConfigGenerateServiceAccountOptions{
ServiceAccountName: opt.Name,
ServiceAccountNamespace: opt.Namespace,
ExpireTime: DefaultExpireTime,
}
}
// KubeConfigWithIdentityGenerateOption option for setting identity in KubeConfig
type KubeConfigWithIdentityGenerateOption Identity
// ApplyToOptions .
func (opt KubeConfigWithIdentityGenerateOption) ApplyToOptions(options *KubeConfigGenerateOptions) {
if opt.User != "" {
KubeConfigWithUserGenerateOption(opt.User).ApplyToOptions(options)
}
for _, group := range opt.Groups {
KubeConfigWithGroupGenerateOption(group).ApplyToOptions(options)
}
if opt.ServiceAccount != "" {
(KubeConfigWithServiceAccountGenerateOption{
Name: opt.ServiceAccount,
Namespace: opt.ServiceAccountNamespace,
}).ApplyToOptions(options)
}
}
// KubeConfigGenerateOption option for create KubeConfig
type KubeConfigGenerateOption interface {
ApplyToOptions(options *KubeConfigGenerateOptions)
}
func newKubeConfigGenerateOptions(options ...KubeConfigGenerateOption) *KubeConfigGenerateOptions {
opts := &KubeConfigGenerateOptions{
X509: &KubeConfigGenerateX509Options{
User: user.Anonymous,
Groups: []string{KubeVelaClientGroup},
ExpireTime: DefaultExpireTime,
PrivateKeyBits: 2048,
},
ServiceAccount: nil,
}
for _, op := range options {
op.ApplyToOptions(opts)
}
return opts
}
const (
// KubeVelaClientGroup the default group to be added to the generated X509 KubeConfig
KubeVelaClientGroup = "kubevela:client"
// CSRNamePrefix the prefix of the CSR name
CSRNamePrefix = "kubevela-csr"
)
// GenerateKubeConfig generate KubeConfig for users with given options.
func GenerateKubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, options ...KubeConfigGenerateOption) (*clientcmdapi.Config, error) {
opts := newKubeConfigGenerateOptions(options...)
if opts.X509 != nil {
return generateX509KubeConfig(ctx, cli, cfg, writer, opts.X509)
} else if opts.ServiceAccount != nil {
return generateServiceAccountKubeConfig(ctx, cli, cfg, writer, opts.ServiceAccount)
}
return nil, errors.New("either x509 or serviceaccount must be set for creating KubeConfig")
}
func genKubeConfig(cfg *clientcmdapi.Config, authInfo *clientcmdapi.AuthInfo, caData []byte) (*clientcmdapi.Config, error) {
if len(cfg.Clusters) == 0 {
return nil, fmt.Errorf("there is no clusters in the cluster config")
}
exportCfg := cfg.DeepCopy()
var exportContext *clientcmdapi.Context
if len(cfg.Contexts) > 0 {
exportContext = cfg.Contexts[cfg.CurrentContext].DeepCopy()
exportCfg.Contexts = map[string]*clientcmdapi.Context{cfg.CurrentContext: exportContext}
} else {
exportCfg.Contexts = map[string]*clientcmdapi.Context{}
for name := range cfg.Clusters {
exportContext = &clientcmdapi.Context{
Cluster: name,
AuthInfo: authInfo.Username,
}
exportCfg.Contexts["local"] = exportContext
}
exportCfg.CurrentContext = "local"
}
exportCluster := cfg.Clusters[exportContext.Cluster].DeepCopy()
if caData != nil {
exportCluster.CertificateAuthorityData = caData
}
exportCfg.Clusters = map[string]*clientcmdapi.Cluster{exportContext.Cluster: exportCluster}
exportCfg.AuthInfos = map[string]*clientcmdapi.AuthInfo{exportContext.AuthInfo: authInfo}
return exportCfg, nil
}
func makeCertAndKey(writer io.Writer, opts *KubeConfigGenerateX509Options) ([]byte, []byte, error) {
// generate private key
privateKey, err := rsa.GenerateKey(rand.Reader, opts.PrivateKeyBits)
if err != nil {
return nil, nil, err
}
keyBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
_, _ = fmt.Fprintf(writer, "Private key generated.\n")
template := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: opts.User,
Organization: opts.Groups,
},
SignatureAlgorithm: x509.SHA256WithRSA,
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey)
if err != nil {
return nil, nil, err
}
csrPemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
_, _ = fmt.Fprintf(writer, "Certificate request generated.\n")
return csrPemBytes, keyBytes, nil
}
func makeCSRName(user string) string {
return fmt.Sprintf("%s-%s", CSRNamePrefix, user)
}
func generateX509KubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, options *KubeConfigGenerateX509Options) (*clientcmdapi.Config, error) {
info, _ := cli.Discovery().ServerVersion()
if info == nil || version.MustParseGeneric(info.String()).AtLeast(version.MustParseSemantic("v1.19.0")) {
return generateX509KubeConfigV1(ctx, cli, cfg, writer, options)
}
return generateX509KubeConfigV1Beta(ctx, cli, cfg, writer, options)
}
func generateX509KubeConfigV1(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, opts *KubeConfigGenerateX509Options) (*clientcmdapi.Config, error) {
csrPemBytes, keyBytes, err := makeCertAndKey(writer, opts)
if err != nil {
return nil, err
}
csr := &certificatesv1.CertificateSigningRequest{}
csr.Name = makeCSRName(opts.User)
csr.Spec.SignerName = certificatesv1.KubeAPIServerClientSignerName
csr.Spec.Usages = []certificatesv1.KeyUsage{certificatesv1.UsageClientAuth}
csr.Spec.Request = csrPemBytes
csr.Spec.ExpirationSeconds = ptr.To(int32(opts.ExpireTime.Seconds()))
if _, err := cli.CertificatesV1().CertificateSigningRequests().Create(ctx, csr, metav1.CreateOptions{}); err != nil {
return nil, err
}
_, _ = fmt.Fprintf(writer, "Certificate signing request %s generated.\n", csr.Name)
defer func() {
_ = cli.CertificatesV1().CertificateSigningRequests().Delete(ctx, csr.Name, metav1.DeleteOptions{})
}()
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{
Type: certificatesv1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "Self-generated and auto-approved by KubeVela",
Message: "This CSR was approved by KubeVela",
LastUpdateTime: metav1.Now(),
})
if csr, err = cli.CertificatesV1().CertificateSigningRequests().UpdateApproval(ctx, csr.Name, csr, metav1.UpdateOptions{}); err != nil {
return nil, err
}
_, _ = fmt.Fprintf(writer, "Certificate signing request %s approved.\n", csr.Name)
if err := wait.PollUntilContextTimeout(ctx, time.Second, time.Minute, false, func(ctx context.Context) (done bool, err error) {
if csr, err = cli.CertificatesV1().CertificateSigningRequests().Get(ctx, csr.Name, metav1.GetOptions{}); err != nil {
return false, err
}
if csr.Status.Certificate == nil {
return false, nil
}
return true, nil
}); err != nil {
return nil, err
}
_, _ = fmt.Fprintf(writer, "Signed certificate retrieved.\n")
return genKubeConfig(cfg, &clientcmdapi.AuthInfo{
ClientKeyData: keyBytes,
ClientCertificateData: csr.Status.Certificate,
}, nil)
}
func generateX509KubeConfigV1Beta(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, opts *KubeConfigGenerateX509Options) (*clientcmdapi.Config, error) {
csrPemBytes, keyBytes, err := makeCertAndKey(writer, opts)
if err != nil {
return nil, err
}
csr := &certificatesv1beta1.CertificateSigningRequest{}
csr.Name = makeCSRName(opts.User)
var name = certificatesv1beta1.KubeAPIServerClientSignerName
csr.Spec.SignerName = &name
csr.Spec.Usages = []certificatesv1beta1.KeyUsage{certificatesv1beta1.UsageClientAuth}
csr.Spec.Request = csrPemBytes
csr.Spec.ExpirationSeconds = ptr.To(int32(opts.ExpireTime.Seconds()))
// create
if _, err = cli.CertificatesV1beta1().CertificateSigningRequests().Create(ctx, csr, metav1.CreateOptions{}); err != nil {
return nil, err
}
_, _ = fmt.Fprintf(writer, "Certificate signing request %s generated.\n", csr.Name)
defer func() {
_ = cli.CertificatesV1beta1().CertificateSigningRequests().Delete(ctx, csr.Name, metav1.DeleteOptions{})
}()
// approval
csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{
Type: certificatesv1beta1.CertificateApproved,
Status: corev1.ConditionTrue,
Reason: "Self-generated and auto-approved by KubeVela",
Message: "This CSR was approved by KubeVela",
LastUpdateTime: metav1.Now(),
})
if csr, err = cli.CertificatesV1beta1().CertificateSigningRequests().UpdateApproval(ctx, csr, metav1.UpdateOptions{}); err != nil {
return nil, err
}
_, _ = fmt.Fprintf(writer, "Certificate signing request %s approved.\n", csr.Name)
// waiting and get the status
if err = wait.PollUntilContextTimeout(ctx, time.Second, time.Minute, false, func(ctx context.Context) (done bool, err error) {
if csr, err = cli.CertificatesV1beta1().CertificateSigningRequests().Get(ctx, csr.Name, metav1.GetOptions{}); err != nil {
return false, err
}
if csr.Status.Certificate == nil {
return false, nil
}
return true, nil
}); err != nil {
return nil, err
}
_, _ = fmt.Fprintf(writer, "Signed certificate retrieved.\n")
return genKubeConfig(cfg, &clientcmdapi.AuthInfo{
ClientKeyData: keyBytes,
ClientCertificateData: csr.Status.Certificate,
}, nil)
}
func generateServiceAccountKubeConfig(ctx context.Context, cli kubernetes.Interface, cfg *clientcmdapi.Config, writer io.Writer, opts *KubeConfigGenerateServiceAccountOptions) (*clientcmdapi.Config, error) {
var (
token string
CA []byte
)
sa, err := cli.CoreV1().ServiceAccounts(opts.ServiceAccountNamespace).Get(ctx, opts.ServiceAccountName, metav1.GetOptions{})
if err != nil {
return nil, err
}
_, _ = fmt.Fprintf(writer, "ServiceAccount %s/%s found.\n", opts.ServiceAccountNamespace, opts.ServiceAccountName)
if len(sa.Secrets) == 0 {
_, _ = fmt.Fprintf(writer, "ServiceAccount %s/%s has no secret. Requesting token", opts.ServiceAccountNamespace, opts.ServiceAccountName)
request := authenticationv1.TokenRequest{
Spec: authenticationv1.TokenRequestSpec{
Audiences: []string{},
ExpirationSeconds: ptr.To(int64(opts.ExpireTime.Seconds())),
},
}
tokenRequest, err := cli.CoreV1().ServiceAccounts(opts.ServiceAccountNamespace).CreateToken(ctx, opts.ServiceAccountName, &request, metav1.CreateOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to request token")
}
token = tokenRequest.Status.Token
CAConfigMap, err := cli.CoreV1().ConfigMaps(sa.Namespace).Get(ctx, "kube-root-ca.crt", metav1.GetOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to get root CA secret")
}
CA = []byte(CAConfigMap.Data["ca.crt"])
} else {
secretKey := sa.Secrets[0]
if secretKey.Namespace == "" {
secretKey.Namespace = sa.Namespace
}
secret, err := cli.CoreV1().Secrets(secretKey.Namespace).Get(ctx, secretKey.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
_, _ = fmt.Fprintf(writer, "ServiceAccount secret %s/%s found.\n", secretKey.Namespace, secret.Name)
if len(secret.Data["token"]) == 0 {
return nil, errors.Errorf("no token found in secret %s/%s", secret.Namespace, secret.Name)
}
_, _ = fmt.Fprintf(writer, "ServiceAccount token found.\n")
token = string(secret.Data["token"])
CA = secret.Data["ca.crt"]
}
return genKubeConfig(cfg, &clientcmdapi.AuthInfo{
Token: token,
}, CA)
}
// ReadIdentityFromKubeConfig extract identity from kubeconfig
func ReadIdentityFromKubeConfig(kubeconfigPath string) (*Identity, error) {
cfg, err := clientcmd.LoadFromFile(kubeconfigPath)
if err != nil {
return nil, err
}
ctx, exists := cfg.Contexts[cfg.CurrentContext]
if !exists {
return nil, fmt.Errorf("cannot find current-context %s", cfg.CurrentContext)
}
authInfo, exists := cfg.AuthInfos[ctx.AuthInfo]
if !exists {
return nil, fmt.Errorf("cannot find auth-info %s", ctx.AuthInfo)
}
identity := &Identity{}
token := authInfo.Token
if token == "" && authInfo.TokenFile != "" {
bs, err := os.ReadFile(authInfo.TokenFile)
if err != nil {
return nil, fmt.Errorf("failed to read token file %s: %w", authInfo.TokenFile, err)
}
token = string(bs)
}
if token != "" {
sub, err := utils.GetTokenSubject(token)
if err != nil {
return nil, fmt.Errorf("failed to recognize serviceaccount: %w", err)
}
identity.ServiceAccountNamespace, identity.ServiceAccount, err = serviceaccount.SplitUsername(sub)
if err != nil {
return nil, fmt.Errorf("cannot parse serviceaccount from %s: %w", sub, err)
}
return identity, nil
}
certData := authInfo.ClientCertificateData
if len(certData) == 0 && authInfo.ClientCertificate != "" {
certData, err = os.ReadFile(authInfo.ClientCertificate)
if err != nil {
return nil, fmt.Errorf("failed to read cert file %s: %w", authInfo.ClientCertificate, err)
}
}
if len(certData) > 0 {
name, err := utils.GetCertificateSubject(certData)
if err != nil {
return nil, fmt.Errorf("failed to get subject from certificate data: %w", err)
}
identity.User = name.CommonName
identity.Groups = name.Organization
return identity, nil
}
return nil, fmt.Errorf("cannot find client certificate or serviceaccount token in kubeconfig")
}