Skip to content
Open
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
40 changes: 34 additions & 6 deletions internal/api/v1/cluster/member.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
)

// Update Cluster Member
Expand Down Expand Up @@ -214,6 +215,28 @@ func (h *Handler) ListClusterMembers() iris.Handler {
// @Success 200 {object} Member
// @Security ApiKeyAuth
// @Router /clusters/{cluster}/members [post]
func canContinueWithoutMemberCertificate(err error) bool {
return err != nil && strings.Contains(err.Error(), "csr approve time out")
}
func shouldUseImpersonationOnly(c *v1Cluster.Cluster) bool {
if c == nil || strings.ToLower(c.Spec.Authentication.Mode) != "configfile" {
return false
}
kubeConfig, err := clientcmd.Load(c.Spec.Authentication.ConfigFileContent)
if err != nil {
return false
}
ctx := kubeConfig.Contexts[kubeConfig.CurrentContext]
if ctx == nil {
return false
}
authInfo := kubeConfig.AuthInfos[ctx.AuthInfo]
if authInfo == nil {
return false
}
return (authInfo.Token != "" || authInfo.Exec != nil) && len(authInfo.ClientCertificateData) == 0 && len(authInfo.ClientKeyData) == 0
}

func (h *Handler) CreateClusterMember() iris.Handler {
return func(ctx *context.Context) {
name := ctx.Params().GetString("name")
Expand Down Expand Up @@ -257,12 +280,17 @@ func (h *Handler) CreateClusterMember() iris.Handler {
}

k := kubernetes.NewKubernetes(c)
cert, err := k.CreateCommonUser(req.Name)
if err != nil {
_ = tx.Rollback()
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("create common user failed: %s", err.Error()))
return
var cert []byte
if !shouldUseImpersonationOnly(c) {
cert, err = k.CreateCommonUser(req.Name)
if err != nil {
if !canContinueWithoutMemberCertificate(err) {
_ = tx.Rollback()
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("create common user failed: %s", err.Error()))
return
}
}
}
binding.Certificate = cert
if err := h.clusterBindingService.CreateClusterBinding(&binding, common.DBOptions{DB: tx}); err != nil {
Expand Down
100 changes: 100 additions & 0 deletions internal/api/v1/cluster/member_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package cluster

import (
"errors"
"testing"

v1Cluster "github.com/1Panel-dev/KubePi/internal/model/v1/cluster"
)

func TestCanContinueWithoutMemberCertificateForCSRTimeout(t *testing.T) {
if !canContinueWithoutMemberCertificate(errors.New("csr approve time out ,30 sec")) {
t.Fatalf("expected CSR timeout to allow impersonation fallback")
}
}

func TestCanContinueWithoutMemberCertificateRejectsOtherErrors(t *testing.T) {
if canContinueWithoutMemberCertificate(errors.New("forbidden")) {
t.Fatalf("expected non-CSR errors to remain fatal")
}
}

func TestShouldUseImpersonationOnlyForTokenKubeconfig(t *testing.T) {
c := &v1Cluster.Cluster{}
c.Spec.Authentication.Mode = "configFile"
c.Spec.Authentication.ConfigFileContent = []byte(`apiVersion: v1
kind: Config
clusters:
- name: c
cluster:
server: https://example.invalid
contexts:
- name: c
context:
cluster: c
user: u
current-context: c
users:
- name: u
user:
token: token-value
`)

if !shouldUseImpersonationOnly(c) {
t.Fatalf("expected token kubeconfig to skip CSR and use impersonation")
}
}
func TestShouldUseImpersonationOnlyForExecKubeconfig(t *testing.T) {
c := &v1Cluster.Cluster{}
c.Spec.Authentication.Mode = "configFile"
c.Spec.Authentication.ConfigFileContent = []byte(`apiVersion: v1
kind: Config
clusters:
- name: c
cluster:
server: https://example.invalid
contexts:
- name: c
context:
cluster: c
user: u
current-context: c
users:
- name: u
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: aws
`)

if !shouldUseImpersonationOnly(c) {
t.Fatalf("expected exec kubeconfig to skip CSR and use impersonation")
}
}

func TestShouldUseImpersonationOnlyRejectsCertificateKubeconfig(t *testing.T) {
c := &v1Cluster.Cluster{}
c.Spec.Authentication.Mode = "configFile"
c.Spec.Authentication.ConfigFileContent = []byte(`apiVersion: v1
kind: Config
clusters:
- name: c
cluster:
server: https://example.invalid
contexts:
- name: c
context:
cluster: c
user: u
current-context: c
users:
- name: u
user:
client-certificate-data: cert
client-key-data: key
`)

if shouldUseImpersonationOnly(c) {
t.Fatalf("expected certificate kubeconfig to keep CSR certificate flow")
}
}
11 changes: 2 additions & 9 deletions internal/api/v1/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package proxy

import (
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
Expand All @@ -16,6 +15,7 @@ import (
"github.com/1Panel-dev/KubePi/internal/api/v1/session"
v1Cluster "github.com/1Panel-dev/KubePi/internal/model/v1/cluster"
"github.com/1Panel-dev/KubePi/internal/service/v1/cluster"
"github.com/1Panel-dev/KubePi/internal/service/v1/clusteraccess"
"github.com/1Panel-dev/KubePi/internal/service/v1/clusterbinding"
"github.com/1Panel-dev/KubePi/internal/service/v1/common"
pkgV1 "github.com/1Panel-dev/KubePi/pkg/api/v1"
Expand Down Expand Up @@ -449,14 +449,7 @@ func (h *Handler) generateTLSTransport(c *v1Cluster.Cluster, profile session.Use
if err != nil {
return nil, err
}
kubeConf.Username = ""
kubeConf.Password = ""
kubeConf.BearerToken = ""
kubeConf.BearerTokenFile = ""
kubeConf.AuthProvider = nil
kubeConf.ExecProvider = nil
kubeConf.TLSClientConfig.CertData = binding.Certificate
kubeConf.TLSClientConfig.KeyData = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: c.PrivateKey})
clusteraccess.ApplyUserAccessConfig(kubeConf, c, binding)
return rest.TransportFor(kubeConf)
}

Expand Down
9 changes: 6 additions & 3 deletions internal/api/v1/webkubectl/webkubectl.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package webkubectl

import (
"encoding/pem"
"fmt"
"time"

"github.com/1Panel-dev/KubePi/internal/api/v1/session"
"github.com/1Panel-dev/KubePi/internal/service/v1/cluster"
"github.com/1Panel-dev/KubePi/internal/service/v1/clusteraccess"
"github.com/1Panel-dev/KubePi/internal/service/v1/clusterbinding"
"github.com/1Panel-dev/KubePi/internal/service/v1/common"
"github.com/1Panel-dev/KubePi/pkg/kubernetes"
Expand Down Expand Up @@ -82,6 +82,10 @@ func toCmdConfig(sess *Session) *clientcmdapi.Config {
Token: sess.config.BearerToken,
Username: sess.config.Username,
Password: sess.config.Password,
Impersonate: sess.config.Impersonate.UserName,
ImpersonateUID: sess.config.Impersonate.UID,
ImpersonateGroups: sess.config.Impersonate.Groups,
ImpersonateUserExtra: sess.config.Impersonate.Extra,
}
contextName := fmt.Sprintf("%s@%s", sess.Cluster, sess.User)
cc.Contexts[contextName] = &clientcmdapi.Context{
Expand Down Expand Up @@ -123,8 +127,7 @@ func (h *Handler) CreateSession() iris.Handler {
ctx.Values().Set("message", err.Error())
return
}
cfg.CertData = rb.Certificate
cfg.KeyData = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: c.PrivateKey})
clusteraccess.ApplyUserAccessConfig(cfg, c, rb)
}
sess.config = cfg
sess.User = profile.Name
Expand Down
34 changes: 34 additions & 0 deletions internal/api/v1/webkubectl/webkubectl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package webkubectl

import (
"testing"

"k8s.io/client-go/rest"
)

func TestToCmdConfigPreservesImpersonation(t *testing.T) {
sess := &Session{
Cluster: "eks",
User: "alice",
config: &rest.Config{
Host: "https://example.invalid",
BearerToken: "admin-token",
Impersonate: rest.ImpersonationConfig{UserName: "alice", Groups: []string{"developers"}},
},
}

cfg := toCmdConfig(sess)
auth := cfg.AuthInfos["alice"]
if auth == nil {
t.Fatalf("expected auth info for alice")
}
if auth.Token != "admin-token" {
t.Fatalf("expected bearer token to be preserved")
}
if auth.Impersonate != "alice" {
t.Fatalf("expected impersonation username alice, got %q", auth.Impersonate)
}
if len(auth.ImpersonateGroups) != 1 || auth.ImpersonateGroups[0] != "developers" {
t.Fatalf("expected impersonation groups to be preserved")
}
}
10 changes: 9 additions & 1 deletion internal/service/v1/clusteraccess/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ func ConfigForUser(clusterName string, user User) (*rest.Config, *v1Cluster.Clus
if err != nil {
return nil, nil, err
}
ApplyUserAccessConfig(cfg, c, binding)
return cfg, c, nil
}
func ApplyUserAccessConfig(cfg *rest.Config, c *v1Cluster.Cluster, binding *v1Cluster.Binding) {
if len(binding.Certificate) == 0 {
cfg.Impersonate = rest.ImpersonationConfig{UserName: binding.UserRef}
return
}

cfg.Username = ""
cfg.Password = ""
cfg.BearerToken = ""
Expand All @@ -44,7 +53,6 @@ func ConfigForUser(clusterName string, user User) (*rest.Config, *v1Cluster.Clus
cfg.Impersonate = rest.ImpersonationConfig{}
cfg.CertData = binding.Certificate
cfg.KeyData = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: c.PrivateKey})
return cfg, c, nil
}

func ClientForUser(clusterName string, user User) (*kubernetesClient.Clientset, *rest.Config, *v1Cluster.Cluster, error) {
Expand Down
47 changes: 47 additions & 0 deletions internal/service/v1/clusteraccess/access_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package clusteraccess

import (
"testing"

v1Cluster "github.com/1Panel-dev/KubePi/internal/model/v1/cluster"
"k8s.io/client-go/rest"
)

func TestApplyUserAccessUsesImpersonationWhenMemberCertificateMissing(t *testing.T) {
cfg := &rest.Config{BearerToken: "admin-token"}
cluster := &v1Cluster.Cluster{PrivateKey: []byte("private-key")}
binding := &v1Cluster.Binding{UserRef: "alice"}

ApplyUserAccessConfig(cfg, cluster, binding)

if cfg.BearerToken != "admin-token" {
t.Fatalf("expected imported cluster bearer token to remain for impersonation, got %q", cfg.BearerToken)
}
if cfg.Impersonate.UserName != "alice" {
t.Fatalf("expected impersonation username alice, got %q", cfg.Impersonate.UserName)
}
if len(cfg.CertData) != 0 || len(cfg.KeyData) != 0 {
t.Fatalf("expected no client certificate when binding has none")
}
}

func TestApplyUserAccessUsesMemberCertificateWhenAvailable(t *testing.T) {
cfg := &rest.Config{BearerToken: "admin-token"}
cluster := &v1Cluster.Cluster{PrivateKey: []byte("private-key")}
binding := &v1Cluster.Binding{UserRef: "alice", Certificate: []byte("cert")}

ApplyUserAccessConfig(cfg, cluster, binding)

if cfg.BearerToken != "" {
t.Fatalf("expected bearer token cleared for certificate auth, got %q", cfg.BearerToken)
}
if cfg.Impersonate.UserName != "" {
t.Fatalf("expected no impersonation when member certificate exists, got %q", cfg.Impersonate.UserName)
}
if string(cfg.CertData) != "cert" {
t.Fatalf("expected member certificate to be used")
}
if len(cfg.KeyData) == 0 {
t.Fatalf("expected cluster private key to be used with member certificate")
}
}