Skip to content

Commit b999d98

Browse files
Merge pull request #2075 from tchap/login-u-oidc
OCPBUGS-58393: oc login: Preserve ExecProvider on oc login -u
2 parents bb2c20e + de85e04 commit b999d98

File tree

2 files changed

+132
-4
lines changed

2 files changed

+132
-4
lines changed

pkg/cli/login/loginoptions.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717

1818
"github.com/pkg/browser"
1919

20+
userv1 "github.com/openshift/api/user/v1"
2021
projectv1typedclient "github.com/openshift/client-go/project/clientset/versioned/typed/project/v1"
2122
"github.com/openshift/library-go/pkg/oauth/tokenrequest"
2223
"github.com/openshift/library-go/pkg/oauth/tokenrequest/challengehandlers"
@@ -94,6 +95,10 @@ type LoginOptions struct {
9495
RequestTimeout time.Duration
9596

9697
genericiooptions.IOStreams
98+
99+
// whoAmIFunc is used to mock project.WhoAmI. It's only being used in tests.
100+
// See LoginOptions.whoAmI for details.
101+
whoAmIFunc func(clientConfig *restclient.Config) (*userv1.User, error)
97102
}
98103

99104
type passwordPrompter func(r io.Reader, w io.Writer, format string, a ...interface{}) string
@@ -234,7 +239,7 @@ func (o *LoginOptions) gatherAuthInfo() error {
234239
// if a token were explicitly provided, try to use it
235240
if o.tokenProvided() {
236241
clientConfig.BearerToken = o.Token
237-
me, err := project.WhoAmI(clientConfig)
242+
me, err := o.whoAmI(clientConfig)
238243
if err != nil {
239244
if kerrors.IsUnauthorized(err) {
240245
return fmt.Errorf("The token provided is invalid or expired.\n\n")
@@ -259,13 +264,18 @@ func (o *LoginOptions) gatherAuthInfo() error {
259264
if matchingClusters.Has(context.Cluster) {
260265
clientcmdConfig := kclientcmd.NewDefaultClientConfig(kubeconfig, &kclientcmd.ConfigOverrides{CurrentContext: key})
261266
if kubeconfigClientConfig, err := clientcmdConfig.ClientConfig(); err == nil {
262-
if me, err := project.WhoAmI(kubeconfigClientConfig); err == nil && (o.Username == me.Name) {
267+
if me, err := o.whoAmI(kubeconfigClientConfig); err == nil && (o.Username == me.Name) {
263268
clientConfig.BearerToken = kubeconfigClientConfig.BearerToken
264269
clientConfig.CertFile = kubeconfigClientConfig.CertFile
265270
clientConfig.CertData = kubeconfigClientConfig.CertData
266271
clientConfig.KeyFile = kubeconfigClientConfig.KeyFile
267272
clientConfig.KeyData = kubeconfigClientConfig.KeyData
268273

274+
// Preserve ExecProvider configuration.
275+
if kubeconfigClientConfig.ExecProvider != nil {
276+
clientConfig.ExecProvider = kubeconfigClientConfig.ExecProvider.DeepCopy()
277+
}
278+
269279
o.Config = clientConfig
270280

271281
fmt.Fprintf(o.Out, "Logged into %q as %q using existing credentials.\n\n", o.Config.Host, o.Username)
@@ -284,7 +294,7 @@ func (o *LoginOptions) gatherAuthInfo() error {
284294
}
285295

286296
clientConfig.ExecProvider = execProvider
287-
me, err := project.WhoAmI(clientConfig)
297+
me, err := o.whoAmI(clientConfig)
288298
if err != nil {
289299
return err
290300
}
@@ -319,7 +329,7 @@ func (o *LoginOptions) gatherAuthInfo() error {
319329

320330
clientConfig.BearerToken = token
321331

322-
me, err := project.WhoAmI(clientConfig)
332+
me, err := o.whoAmI(clientConfig)
323333
if err != nil {
324334
return err
325335
}
@@ -565,6 +575,13 @@ func (o *LoginOptions) SaveConfig() (bool, error) {
565575
return created, nil
566576
}
567577

578+
func (o *LoginOptions) whoAmI(clientConfig *restclient.Config) (*userv1.User, error) {
579+
if o.whoAmIFunc != nil {
580+
return o.whoAmIFunc(clientConfig)
581+
}
582+
return project.WhoAmI(clientConfig)
583+
}
584+
568585
func (o *LoginOptions) usernameProvided() bool {
569586
return len(o.Username) > 0
570587
}

pkg/cli/login/loginoptions_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ import (
1111
"testing"
1212

1313
"github.com/MakeNowJust/heredoc"
14+
"github.com/google/go-cmp/cmp"
1415

1516
kapierrs "k8s.io/apimachinery/pkg/api/errors"
17+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1618
"k8s.io/cli-runtime/pkg/genericiooptions"
1719
restclient "k8s.io/client-go/rest"
1820
kclientcmdapi "k8s.io/client-go/tools/clientcmd/api"
1921

22+
userv1 "github.com/openshift/api/user/v1"
2023
"github.com/openshift/library-go/pkg/oauth/oauthdiscovery"
2124
cliconfig "github.com/openshift/oc/pkg/helpers/kubeconfig"
2225
)
@@ -374,6 +377,114 @@ func TestDialToHTTPSServer(t *testing.T) {
374377
}
375378
}
376379

380+
func TestPreserveExecProviderOnUsernameLogin(t *testing.T) {
381+
// Test that when using -u flag with existing OIDC credentials,
382+
// the ExecProvider configuration is preserved
383+
384+
execProvider := &kclientcmdapi.ExecConfig{
385+
APIVersion: "client.authentication.k8s.io/v1",
386+
Command: "oc",
387+
Args: []string{
388+
"get-token",
389+
"--issuer-url=https://oauth.example.com",
390+
"--client-id=test-client",
391+
"--callback-address=127.0.0.1:8080",
392+
},
393+
InstallHint: "Please be sure that oc is defined in $PATH to be executed as credentials exec plugin",
394+
InteractiveMode: kclientcmdapi.IfAvailableExecInteractiveMode,
395+
}
396+
397+
testCases := map[string]struct {
398+
username string
399+
existingExecProvider *kclientcmdapi.ExecConfig
400+
expectedExecProvider *kclientcmdapi.ExecConfig
401+
}{
402+
"preserve OIDC exec provider": {
403+
username: "oidc-user-test:[email protected]",
404+
existingExecProvider: execProvider,
405+
expectedExecProvider: execProvider,
406+
},
407+
"no exec provider preserved when none exists": {
408+
username: "regular-user",
409+
existingExecProvider: nil,
410+
expectedExecProvider: nil,
411+
},
412+
}
413+
414+
for name, test := range testCases {
415+
t.Run(name, func(t *testing.T) {
416+
// Mock config constants.
417+
serverURL := "https://api.test.devcluster.openshift.com:6443"
418+
clusterNick := "api-test-devcluster-openshift-com:6443"
419+
userNick := test.username + "/" + clusterNick
420+
contextNick := "default/" + clusterNick + "/" + test.username
421+
422+
// Create starting kubeconfig with existing OIDC credentials.
423+
startingConfig := &kclientcmdapi.Config{
424+
Clusters: map[string]*kclientcmdapi.Cluster{
425+
clusterNick: {
426+
Server: serverURL,
427+
},
428+
},
429+
AuthInfos: map[string]*kclientcmdapi.AuthInfo{
430+
userNick: {
431+
Exec: test.existingExecProvider,
432+
},
433+
},
434+
Contexts: map[string]*kclientcmdapi.Context{
435+
contextNick: {
436+
Cluster: clusterNick,
437+
AuthInfo: userNick,
438+
Namespace: "default",
439+
},
440+
},
441+
CurrentContext: contextNick,
442+
}
443+
444+
// Setup LoginOptions with username and existing config
445+
options := &LoginOptions{
446+
Username: test.username,
447+
Server: serverURL,
448+
StartingKubeConfig: startingConfig,
449+
Config: &restclient.Config{
450+
Host: serverURL,
451+
ExecProvider: test.existingExecProvider,
452+
},
453+
IOStreams: genericiooptions.NewTestIOStreamsDiscard(),
454+
}
455+
456+
// Mock the WhoAmI function to always match the user.
457+
whoAmICalled := false
458+
options.whoAmIFunc = func(clientConfig *restclient.Config) (*userv1.User, error) {
459+
whoAmICalled = true
460+
return &userv1.User{
461+
ObjectMeta: metav1.ObjectMeta{
462+
Name: test.username,
463+
},
464+
}, nil
465+
}
466+
467+
// Calling gatherAuthInfo should preserve ExecProvider.
468+
if err := options.gatherAuthInfo(); err != nil {
469+
t.Fatalf("gatherAuthInfo failed: %v", err)
470+
}
471+
472+
// Verify ExecProvider is preserved correctly.
473+
if options.Config == nil {
474+
t.Fatal("LoginOptions.Config is nil")
475+
}
476+
if !cmp.Equal(options.Config.ExecProvider, test.expectedExecProvider) {
477+
t.Errorf("expected provider mismatch: \n%s\n", cmp.Diff(test.expectedExecProvider, options.Config.ExecProvider))
478+
}
479+
480+
// Just check additionally that whoAmI has been called.
481+
if !whoAmICalled {
482+
t.Errorf("WhoAmI function has not been called")
483+
}
484+
})
485+
}
486+
}
487+
377488
func newTLSServer(certString, keyString string) (*httptest.Server, error) {
378489
invoked := make(chan struct{}, 1)
379490
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

0 commit comments

Comments
 (0)