pkg/internal/converter/convert.go (409 lines of code) (raw):
package converter
import (
"fmt"
"strings"
"github.com/Azure/kubelogin/pkg/internal/token"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
klog "k8s.io/klog/v2"
)
const (
azureAuthProvider = "azure"
cfgClientID = "client-id"
cfgApiserverID = "apiserver-id"
cfgTenantID = "tenant-id"
cfgEnvironment = "environment"
cfgConfigMode = "config-mode"
argClientID = "--client-id"
argServerID = "--server-id"
argTenantID = "--tenant-id"
argEnvironment = "--environment"
argClientSecret = "--client-secret"
argClientCert = "--client-certificate"
argClientCertPassword = "--client-certificate-password"
argIsLegacy = "--legacy"
argUsername = "--username"
argPassword = "--password"
argLoginMethod = "--login"
argIdentityResourceID = "--identity-resource-id"
argAuthorityHost = "--authority-host"
argFederatedTokenFile = "--federated-token-file"
argTokenCacheDir = "--token-cache-dir"
argAuthRecordCacheDir = "--cache-dir"
argIsPoPTokenEnabled = "--pop-enabled"
argPoPTokenClaims = "--pop-claims"
argDisableEnvironmentOverride = "--disable-environment-override"
argRedirectURL = "--redirect-url"
flagAzureConfigDir = "azure-config-dir"
flagClientID = "client-id"
flagContext = "context"
flagServerID = "server-id"
flagTenantID = "tenant-id"
flagEnvironment = "environment"
flagClientSecret = "client-secret"
flagClientCert = "client-certificate"
flagClientCertPassword = "client-certificate-password"
flagIsLegacy = "legacy"
flagUsername = "username"
flagPassword = "password"
flagLoginMethod = "login"
flagIdentityResourceID = "identity-resource-id"
flagAuthorityHost = "authority-host"
flagFederatedTokenFile = "federated-token-file"
flagTokenCacheDir = "token-cache-dir"
flagAuthRecordCacheDir = "cache-dir"
flagIsPoPTokenEnabled = "pop-enabled"
flagPoPTokenClaims = "pop-claims"
flagDisableEnvironmentOverride = "disable-environment-override"
flagRedirectURL = "redirect-url"
execName = "kubelogin"
getTokenCommand = "get-token"
execAPIVersion = "client.authentication.k8s.io/v1beta1"
execInstallHint = `
kubelogin is not installed which is required to connect to AAD enabled cluster.
To learn more, please go to https://azure.github.io/kubelogin/
`
azureConfigDir = "AZURE_CONFIG_DIR"
)
func getArgValues(o Options, authInfo *api.AuthInfo) (
argServerIDVal,
argClientIDVal,
argEnvironmentVal,
argTenantIDVal,
argAuthRecordCacheDirVal,
argPoPTokenClaimsVal,
argRedirectURLVal string,
argIsLegacyConfigModeVal,
argIsPoPTokenEnabledVal bool,
) {
if authInfo == nil {
return
}
isLegacyAuthProvider := isLegacyAzureAuth(authInfo)
if o.isSet(flagEnvironment) {
argEnvironmentVal = o.TokenOptions.Environment
} else if isLegacyAuthProvider {
if x, ok := authInfo.AuthProvider.Config[cfgEnvironment]; ok {
argEnvironmentVal = x
}
} else {
argEnvironmentVal = getExecArg(authInfo, argEnvironment)
}
if o.isSet(flagTenantID) {
argTenantIDVal = o.TokenOptions.TenantID
} else if isLegacyAuthProvider {
if x, ok := authInfo.AuthProvider.Config[cfgTenantID]; ok {
argTenantIDVal = x
}
} else {
argTenantIDVal = getExecArg(authInfo, argTenantID)
}
if o.isSet(flagClientID) {
argClientIDVal = o.TokenOptions.ClientID
} else if isLegacyAuthProvider {
if x, ok := authInfo.AuthProvider.Config[cfgClientID]; ok {
argClientIDVal = x
}
} else {
argClientIDVal = getExecArg(authInfo, argClientID)
}
if o.isSet(flagServerID) {
argServerIDVal = o.TokenOptions.ServerID
} else if isLegacyAuthProvider {
if x, ok := authInfo.AuthProvider.Config[cfgApiserverID]; ok {
argServerIDVal = x
}
} else {
argServerIDVal = getExecArg(authInfo, argServerID)
}
if o.isSet(flagIsLegacy) && o.TokenOptions.IsLegacy {
argIsLegacyConfigModeVal = true
} else if isLegacyAuthProvider {
if x := authInfo.AuthProvider.Config[cfgConfigMode]; x == "" || x == "0" {
argIsLegacyConfigModeVal = true
}
} else {
if found := getExecBoolArg(authInfo, argIsLegacy); found {
argIsLegacyConfigModeVal = true
}
}
if o.isSet(flagAuthRecordCacheDir) || o.isSet(flagTokenCacheDir) {
argAuthRecordCacheDirVal = o.TokenOptions.AuthRecordCacheDir
} else {
if val := getExecArg(authInfo, argAuthRecordCacheDir); val != "" {
argAuthRecordCacheDirVal = val
} else {
argAuthRecordCacheDirVal = getExecArg(authInfo, argTokenCacheDir)
}
}
if o.isSet(flagIsPoPTokenEnabled) {
argIsPoPTokenEnabledVal = o.TokenOptions.IsPoPTokenEnabled
} else {
if found := getExecBoolArg(authInfo, argIsPoPTokenEnabled); found {
argIsPoPTokenEnabledVal = true
}
}
if o.isSet(flagPoPTokenClaims) {
argPoPTokenClaimsVal = o.TokenOptions.PoPTokenClaims
} else {
argPoPTokenClaimsVal = getExecArg(authInfo, argPoPTokenClaims)
}
if o.isSet(flagRedirectURL) {
argRedirectURLVal = o.TokenOptions.RedirectURL
} else {
argRedirectURLVal = getExecArg(authInfo, argRedirectURL)
}
return
}
func isLegacyAzureAuth(authInfoPtr *api.AuthInfo) (ok bool) {
if authInfoPtr == nil {
return
}
if authInfoPtr.AuthProvider == nil {
return
}
return authInfoPtr.AuthProvider.Name == azureAuthProvider
}
func isExecUsingkubelogin(authInfoPtr *api.AuthInfo) (ok bool) {
if authInfoPtr == nil {
return
}
if authInfoPtr.Exec == nil {
return
}
lowerc := strings.ToLower(authInfoPtr.Exec.Command)
return strings.Contains(lowerc, "kubelogin")
}
func Convert(o Options, pathOptions *clientcmd.PathOptions) error {
clientConfig := o.configFlags.ToRawKubeConfigLoader()
var kubeconfigs []string
klog.V(5).Info(o.ToString())
if clientConfig.ConfigAccess() != nil {
if clientConfig.ConfigAccess().GetExplicitFile() != "" {
kubeconfigs = append(kubeconfigs, clientConfig.ConfigAccess().GetExplicitFile())
} else {
kubeconfigs = append(kubeconfigs, clientConfig.ConfigAccess().GetLoadingPrecedence()...)
}
}
klog.V(5).Infof("Loading kubeconfig from %s", strings.Join(kubeconfigs, ":"))
config, err := clientConfig.RawConfig()
if err != nil {
return fmt.Errorf("unable to load kubeconfig: %s", err)
}
targetAuthInfo := ""
if o.context != "" {
if config.Contexts[o.context] == nil {
return fmt.Errorf("no context exists with the name: %q", o.context)
}
targetAuthInfo = config.Contexts[o.context].AuthInfo
}
for name, authInfo := range config.AuthInfos {
if targetAuthInfo != "" && name != targetAuthInfo {
continue
}
klog.V(5).Infof("context: %q", name)
// is it legacy aad auth or is it exec using kubelogin?
if !isExecUsingkubelogin(authInfo) && !isLegacyAzureAuth(authInfo) {
continue
}
klog.V(5).Info("converting...")
argServerIDVal,
argClientIDVal,
argEnvironmentVal,
argTenantIDVal,
argAuthRecordCacheDirVal,
argPoPTokenClaimsVal,
argRedirectURLVal,
isLegacyConfigMode,
isPoPTokenEnabled := getArgValues(o, authInfo)
exec := &api.ExecConfig{
Command: execName,
Args: []string{
getTokenCommand,
},
APIVersion: execAPIVersion,
InstallHint: execInstallHint,
}
// Preserve any existing install hint
if authInfo.Exec != nil && authInfo.Exec.InstallHint != "" {
exec.InstallHint = authInfo.Exec.InstallHint
}
exec.Args = append(exec.Args, argLoginMethod, o.TokenOptions.LoginMethod)
// all login methods require --server-id specified
if argServerIDVal == "" {
return fmt.Errorf("%s is required", argServerID)
}
exec.Args = append(exec.Args, argServerID, argServerIDVal)
if argAuthRecordCacheDirVal != "" {
exec.Args = append(exec.Args, argAuthRecordCacheDir, argAuthRecordCacheDirVal)
}
switch o.TokenOptions.LoginMethod {
case token.AzureDeveloperCLILogin:
if o.isSet(flagTenantID) {
exec.Args = append(exec.Args, argTenantID, o.TokenOptions.TenantID)
}
case token.AzureCLILogin:
if o.azureConfigDir != "" {
exec.Env = append(exec.Env, api.ExecEnvVar{Name: azureConfigDir, Value: o.azureConfigDir})
}
// when convert to azurecli login, tenantID from the input kubeconfig will be disregarded and
// will have to come from explicit flag `--tenant-id`.
// this is because azure cli logged in using MSI does not allow specifying tenant ID
// see https://github.com/Azure/kubelogin/issues/123#issuecomment-1209652342
if o.isSet(flagTenantID) {
exec.Args = append(exec.Args, argTenantID, o.TokenOptions.TenantID)
}
case token.DeviceCodeLogin:
if argClientIDVal == "" {
return fmt.Errorf("%s is required", argClientID)
}
exec.Args = append(exec.Args, argClientID, argClientIDVal)
if argTenantIDVal == "" {
return fmt.Errorf("%s is required", argTenantID)
}
exec.Args = append(exec.Args, argTenantID, argTenantIDVal)
if argEnvironmentVal != "" {
// environment is optional
exec.Args = append(exec.Args, argEnvironment, argEnvironmentVal)
}
if isLegacyConfigMode {
exec.Args = append(exec.Args, argIsLegacy)
}
case token.InteractiveLogin:
if argClientIDVal == "" {
return fmt.Errorf("%s is required", argClientID)
}
exec.Args = append(exec.Args, argClientID, argClientIDVal)
if argTenantIDVal == "" {
return fmt.Errorf("%s is required", argTenantID)
}
exec.Args = append(exec.Args, argTenantID, argTenantIDVal)
if argEnvironmentVal != "" {
// environment is optional
exec.Args = append(exec.Args, argEnvironment, argEnvironmentVal)
}
// PoP token flags are optional but must be provided together
exec.Args, err = validatePoPClaims(exec.Args, isPoPTokenEnabled, argPoPTokenClaims, argPoPTokenClaimsVal)
if err != nil {
return err
}
if argRedirectURLVal != "" {
exec.Args = append(exec.Args, argRedirectURL, argRedirectURLVal)
}
case token.ServicePrincipalLogin:
if argClientIDVal == "" {
return fmt.Errorf("%s is required", argClientID)
}
exec.Args = append(exec.Args, argClientID, argClientIDVal)
if argTenantIDVal == "" {
return fmt.Errorf("%s is required", argTenantID)
}
exec.Args = append(exec.Args, argTenantID, argTenantIDVal)
if argEnvironmentVal != "" {
// environment is optional
exec.Args = append(exec.Args, argEnvironment, argEnvironmentVal)
}
if o.isSet(flagClientSecret) {
exec.Args = append(exec.Args, argClientSecret, o.TokenOptions.ClientSecret)
}
if o.isSet(flagClientCert) {
exec.Args = append(exec.Args, argClientCert, o.TokenOptions.ClientCert)
}
if o.isSet(flagClientCertPassword) {
exec.Args = append(exec.Args, argClientCertPassword, o.TokenOptions.ClientCertPassword)
}
if isLegacyConfigMode {
exec.Args = append(exec.Args, argIsLegacy)
}
// PoP token flags are optional but must be provided together
exec.Args, err = validatePoPClaims(exec.Args, isPoPTokenEnabled, argPoPTokenClaims, argPoPTokenClaimsVal)
if err != nil {
return err
}
if o.isSet(flagDisableEnvironmentOverride) {
exec.Args = append(exec.Args, argDisableEnvironmentOverride)
}
case token.MSILogin:
if o.isSet(flagClientID) {
exec.Args = append(exec.Args, argClientID, o.TokenOptions.ClientID)
} else if o.isSet(flagIdentityResourceID) {
exec.Args = append(exec.Args, argIdentityResourceID, o.TokenOptions.IdentityResourceID)
}
case token.ROPCLogin:
if argClientIDVal == "" {
return fmt.Errorf("%s is required", argClientID)
}
exec.Args = append(exec.Args, argClientID, argClientIDVal)
if argTenantIDVal == "" {
return fmt.Errorf("%s is required", argTenantID)
}
exec.Args = append(exec.Args, argTenantID, argTenantIDVal)
if argEnvironmentVal != "" {
// environment is optional
exec.Args = append(exec.Args, argEnvironment, argEnvironmentVal)
}
if o.isSet(flagUsername) {
exec.Args = append(exec.Args, argUsername, o.TokenOptions.Username)
}
if o.isSet(flagPassword) {
exec.Args = append(exec.Args, argPassword, o.TokenOptions.Password)
}
if isLegacyConfigMode {
exec.Args = append(exec.Args, argIsLegacy)
}
case token.WorkloadIdentityLogin:
if o.isSet(flagClientID) {
exec.Args = append(exec.Args, argClientID, o.TokenOptions.ClientID)
}
if o.isSet(flagTenantID) {
exec.Args = append(exec.Args, argTenantID, o.TokenOptions.TenantID)
}
if o.isSet(flagAuthorityHost) {
exec.Args = append(exec.Args, argAuthorityHost, o.TokenOptions.AuthorityHost)
}
if o.isSet(flagFederatedTokenFile) {
exec.Args = append(exec.Args, argFederatedTokenFile, o.TokenOptions.FederatedTokenFile)
}
}
authInfo.Exec = exec
authInfo.AuthProvider = nil
}
err = clientcmd.ModifyConfig(pathOptions, config, true)
return err
}
// get the item in Exec.Args[] right after someArg
func getExecArg(authInfoPtr *api.AuthInfo, someArg string) (resultStr string) {
if someArg == "" {
return
}
if authInfoPtr == nil || authInfoPtr.Exec == nil || authInfoPtr.Exec.Args == nil {
return
}
if len(authInfoPtr.Exec.Args) < 1 {
return
}
for i := range authInfoPtr.Exec.Args {
if authInfoPtr.Exec.Args[i] == someArg {
if len(authInfoPtr.Exec.Args) > i+1 {
return authInfoPtr.Exec.Args[i+1]
}
}
}
return
}
func getExecBoolArg(authInfoPtr *api.AuthInfo, someArg string) bool {
if someArg == "" {
return false
}
if authInfoPtr == nil || authInfoPtr.Exec == nil || authInfoPtr.Exec.Args == nil {
return false
}
if len(authInfoPtr.Exec.Args) < 1 {
return false
}
for i := range authInfoPtr.Exec.Args {
if authInfoPtr.Exec.Args[i] == someArg {
return true
}
}
return false
}
// If enabling PoP token support, users must provide both "--pop-enabled" and "--pop-claims" flags together.
// If either is provided without the other, validation should throw an error, otherwise the get-token command
// will fail under the hood.
func validatePoPClaims(args []string, isPopTokenEnabled bool, popTokenClaimsFlag, popTokenClaimsVal string) ([]string, error) {
if isPopTokenEnabled && popTokenClaimsVal == "" {
// pop-enabled and pop-claims must be provided together
return args, fmt.Errorf("%s is required when specifying %s", argPoPTokenClaims, argIsPoPTokenEnabled)
}
if popTokenClaimsVal != "" && !isPopTokenEnabled {
// pop-enabled and pop-claims must be provided together
return args, fmt.Errorf("%s is required when specifying %s", argIsPoPTokenEnabled, argPoPTokenClaims)
}
if isPopTokenEnabled && popTokenClaimsVal != "" {
args = append(args, argIsPoPTokenEnabled)
args = append(args, popTokenClaimsFlag, popTokenClaimsVal)
}
return args, nil
}