mirror of
https://github.com/int128/kubelogin.git
synced 2026-02-14 16:39:51 +00:00
Add --grant-type option and username prompt for ROPC (#178)
This commit is contained in:
37
README.md
37
README.md
@@ -85,13 +85,14 @@ Flags:
|
||||
--oidc-client-id string Client ID of the provider (mandatory)
|
||||
--oidc-client-secret string Client secret of the provider
|
||||
--oidc-extra-scope strings Scopes to request to the provider
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
|
||||
--grant-type string The authorization grant type to use. One of (auto|authcode|password) (default "auto")
|
||||
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--username string If set, perform the resource owner password credentials grant
|
||||
--password string If set, use the password instead of asking it
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--token-cache-dir string Path to a directory for caching tokens (default "~/.kube/cache/oidc-login")
|
||||
-h, --help help for get-token
|
||||
|
||||
Global Flags:
|
||||
@@ -155,21 +156,39 @@ You can change the ports by the option:
|
||||
|
||||
#### Resource owner password credentials grant flow
|
||||
|
||||
As well as you can use the resource owner password credentials grant flow.
|
||||
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
|
||||
Most OIDC providers do not support this flow.
|
||||
Kubelogin performs the resource owner password credentials grant flow
|
||||
when `--grant-type=password` or `--username` is set.
|
||||
|
||||
You can pass the username and password:
|
||||
Note that most OIDC providers do not support this flow.
|
||||
Keycloak supports this flow but you need to explicitly enable the "Direct Access Grants" feature in the client settings.
|
||||
|
||||
You can set the username and password.
|
||||
|
||||
```yaml
|
||||
- --username USERNAME
|
||||
- --password PASSWORD
|
||||
```
|
||||
|
||||
If the password is not set, kubelogin will show the prompt.
|
||||
If the password is not set, kubelogin will show the prompt for the password.
|
||||
|
||||
```yaml
|
||||
- --username USERNAME
|
||||
```
|
||||
|
||||
```
|
||||
% kubelogin --username USER
|
||||
% kubectl get pods
|
||||
Password:
|
||||
```
|
||||
|
||||
If the username is not set, kubelogin will show the prompt for the username and password.
|
||||
|
||||
```yaml
|
||||
- --grant-type=password
|
||||
```
|
||||
|
||||
```
|
||||
% kubectl get pods
|
||||
Username: foo
|
||||
Password:
|
||||
```
|
||||
|
||||
|
||||
@@ -102,12 +102,13 @@ Flags:
|
||||
--kubeconfig string Path to the kubeconfig file
|
||||
--context string The name of the kubeconfig context to use
|
||||
--user string The name of the kubeconfig user to use. Prior to --context
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--grant-type string The authorization grant type to use. One of (auto|authcode|password) (default "auto")
|
||||
--listen-port ints Port to bind to the local server. If multiple ports are given, it will try the ports in order (default [8000,18000])
|
||||
--skip-open-browser If true, it does not open the browser on authentication
|
||||
--username string If set, perform the resource owner password credentials grant
|
||||
--password string If set, use the password instead of asking it
|
||||
--certificate-authority string Path to a cert file for the certificate authority
|
||||
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
|
||||
--add_dir_header If true, adds the file directory to the header
|
||||
--alsologtostderr log to standard error as well as files
|
||||
--log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0)
|
||||
|
||||
@@ -18,100 +18,96 @@ func TestCmd_Run(t *testing.T) {
|
||||
const version = "HEAD"
|
||||
|
||||
t.Run("root", func(t *testing.T) {
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
mockStandalone := mock_standalone.NewMockInterface(ctrl)
|
||||
mockStandalone.EXPECT().
|
||||
Do(ctx, standalone.Input{
|
||||
tests := map[string]struct {
|
||||
args []string
|
||||
in standalone.Input
|
||||
}{
|
||||
"Defaults": {
|
||||
args: []string{executable},
|
||||
in: standalone.Input{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
|
||||
},
|
||||
})
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Standalone: mockStandalone,
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AuthCodeOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
mockStandalone := mock_standalone.NewMockInterface(ctrl)
|
||||
mockStandalone.EXPECT().
|
||||
Do(ctx, standalone.Input{
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
SkipOpenBrowser: true,
|
||||
},
|
||||
})
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Standalone: mockStandalone,
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
"FullOptions": {
|
||||
args: []string{executable,
|
||||
"--kubeconfig", "/path/to/kubeconfig",
|
||||
"--context", "hello.k8s.local",
|
||||
"--user", "google",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
"--grant-type", "authcode",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--skip-open-browser",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FullOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
mockStandalone := mock_standalone.NewMockInterface(ctrl)
|
||||
mockStandalone.EXPECT().
|
||||
Do(ctx, standalone.Input{
|
||||
in: standalone.Input{
|
||||
KubeconfigFilename: "/path/to/kubeconfig",
|
||||
KubeconfigContext: "hello.k8s.local",
|
||||
KubeconfigUser: "google",
|
||||
CACertFilename: "/path/to/cacert",
|
||||
SkipTLSVerify: true,
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
SkipOpenBrowser: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"GrantType=password": {
|
||||
args: []string{executable,
|
||||
"--grant-type", "password",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
in: standalone.Input{
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
})
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Standalone: mockStandalone,
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"--kubeconfig", "/path/to/kubeconfig",
|
||||
"--context", "hello.k8s.local",
|
||||
"--user", "google",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
},
|
||||
"GrantType=auto": {
|
||||
args: []string{executable,
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
in: standalone.Input{
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, c := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
mockStandalone := mock_standalone.NewMockInterface(ctrl)
|
||||
mockStandalone.EXPECT().
|
||||
Do(ctx, c.in)
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Standalone: mockStandalone,
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, c.args, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("TooManyArgs", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
@@ -131,85 +127,44 @@ func TestCmd_Run(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("get-token", func(t *testing.T) {
|
||||
t.Run("Defaults", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
getToken := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, credentialplugin.Input{
|
||||
tests := map[string]struct {
|
||||
args []string
|
||||
in credentialplugin.Input
|
||||
}{
|
||||
"Defaults": {
|
||||
args: []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:8000", "127.0.0.1:18000"},
|
||||
},
|
||||
})
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
GetToken: &GetToken{
|
||||
GetToken: getToken,
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
"FullOptions": {
|
||||
args: []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
"--grant-type", "authcode",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AuthCodeOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
getToken := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
SkipOpenBrowser: true,
|
||||
},
|
||||
})
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
GetToken: &GetToken{
|
||||
GetToken: getToken,
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--skip-open-browser",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("FullOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
getToken := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, credentialplugin.Input{
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
@@ -217,41 +172,78 @@ func TestCmd_Run(t *testing.T) {
|
||||
ExtraScopes: []string{"email", "profile"},
|
||||
CACertFilename: "/path/to/cacert",
|
||||
SkipTLSVerify: true,
|
||||
AuthCodeOption: &authentication.AuthCodeOption{
|
||||
BindAddress: []string{"127.0.0.1:10080", "127.0.0.1:20080"},
|
||||
SkipOpenBrowser: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"GrantType=password": {
|
||||
args: []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--grant-type", "password",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
})
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
},
|
||||
},
|
||||
"GrantType=auto": {
|
||||
args: []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
},
|
||||
in: credentialplugin.Input{
|
||||
TokenCacheDir: defaultTokenCacheDir,
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ROPCOption: &authentication.ROPCOption{
|
||||
Username: "USER",
|
||||
Password: "PASS",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, c := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx := context.TODO()
|
||||
getToken := mock_credentialplugin.NewMockInterface(ctrl)
|
||||
getToken.EXPECT().
|
||||
Do(ctx, c.in)
|
||||
cmd := Cmd{
|
||||
Root: &Root{
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
GetToken: &GetToken{
|
||||
GetToken: getToken,
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
GetToken: &GetToken{
|
||||
GetToken: getToken,
|
||||
Logger: mock_logger.New(t),
|
||||
},
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
exitCode := cmd.Run(ctx, []string{executable,
|
||||
"get-token",
|
||||
"--oidc-issuer-url", "https://issuer.example.com",
|
||||
"--oidc-client-id", "YOUR_CLIENT_ID",
|
||||
"--oidc-client-secret", "YOUR_CLIENT_SECRET",
|
||||
"--oidc-extra-scope", "email",
|
||||
"--oidc-extra-scope", "profile",
|
||||
"--certificate-authority", "/path/to/cacert",
|
||||
"--insecure-skip-tls-verify",
|
||||
"-v1",
|
||||
"--listen-port", "10080",
|
||||
"--listen-port", "20080",
|
||||
"--skip-open-browser",
|
||||
"--username", "USER",
|
||||
"--password", "PASS",
|
||||
}, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
exitCode := cmd.Run(ctx, c.args, version)
|
||||
if exitCode != 0 {
|
||||
t.Errorf("exitCode wants 0 but %d", exitCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("MissingMandatoryOptions", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
@@ -57,7 +57,10 @@ func (cmd *GetToken) New(ctx context.Context) *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
authCodeOption, ropcOption := o.authenticationOptions.toUseCaseOptions()
|
||||
authCodeOption, ropcOption, err := o.authenticationOptions.toUseCaseOptions()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("error: %w", err)
|
||||
}
|
||||
in := credentialplugin.Input{
|
||||
IssuerURL: o.IssuerURL,
|
||||
ClientID: o.ClientID,
|
||||
|
||||
@@ -43,6 +43,7 @@ func (o *rootOptions) register(f *pflag.FlagSet) {
|
||||
}
|
||||
|
||||
type authenticationOptions struct {
|
||||
GrantType string
|
||||
ListenPort []int
|
||||
SkipOpenBrowser bool
|
||||
Username string
|
||||
@@ -50,26 +51,27 @@ type authenticationOptions struct {
|
||||
}
|
||||
|
||||
func (o *authenticationOptions) register(f *pflag.FlagSet) {
|
||||
f.StringVar(&o.GrantType, "grant-type", "auto", "The authorization grant type to use. One of (auto|authcode|password)")
|
||||
f.IntSliceVar(&o.ListenPort, "listen-port", defaultListenPort, "Port to bind to the local server. If multiple ports are given, it will try the ports in order")
|
||||
f.BoolVar(&o.SkipOpenBrowser, "skip-open-browser", false, "If true, it does not open the browser on authentication")
|
||||
f.StringVar(&o.Username, "username", "", "If set, perform the resource owner password credentials grant")
|
||||
f.StringVar(&o.Password, "password", "", "If set, use the password instead of asking it")
|
||||
}
|
||||
|
||||
func (o *authenticationOptions) toUseCaseOptions() (authCodeOption *authentication.AuthCodeOption, ropcOption *authentication.ROPCOption) {
|
||||
switch {
|
||||
case o.Username != "":
|
||||
ropcOption = &authentication.ROPCOption{
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
}
|
||||
default:
|
||||
authCodeOption = &authentication.AuthCodeOption{
|
||||
func (o *authenticationOptions) toUseCaseOptions() (*authentication.AuthCodeOption, *authentication.ROPCOption, error) {
|
||||
if o.GrantType == "authcode" || (o.GrantType == "auto" && o.Username == "") {
|
||||
return &authentication.AuthCodeOption{
|
||||
BindAddress: translateListenPortToBindAddress(o.ListenPort),
|
||||
SkipOpenBrowser: o.SkipOpenBrowser,
|
||||
}
|
||||
}, nil, nil
|
||||
}
|
||||
return
|
||||
if o.GrantType == "password" || (o.GrantType == "auto" && o.Username != "") {
|
||||
return nil, &authentication.ROPCOption{
|
||||
Username: o.Username,
|
||||
Password: o.Password,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil, xerrors.Errorf("grant-type must be one of (auto|authcode|password)")
|
||||
}
|
||||
|
||||
type Root struct {
|
||||
@@ -85,7 +87,10 @@ func (cmd *Root) New(ctx context.Context, executable string) *cobra.Command {
|
||||
Long: longDescription,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(*cobra.Command, []string) error {
|
||||
authCodeOption, ropcOption := o.authenticationOptions.toUseCaseOptions()
|
||||
authCodeOption, ropcOption, err := o.authenticationOptions.toUseCaseOptions()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("invalid option: %w", err)
|
||||
}
|
||||
in := standalone.Input{
|
||||
KubeconfigFilename: o.Kubeconfig,
|
||||
KubeconfigContext: kubeconfig.ContextName(o.Context),
|
||||
|
||||
@@ -42,7 +42,10 @@ func (cmd *Setup) New(ctx context.Context) *cobra.Command {
|
||||
Short: "Show the setup instruction",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(c *cobra.Command, _ []string) error {
|
||||
authCodeOption, ropcOption := o.authenticationOptions.toUseCaseOptions()
|
||||
authCodeOption, ropcOption, err := o.authenticationOptions.toUseCaseOptions()
|
||||
if err != nil {
|
||||
return xerrors.Errorf("error: %w", err)
|
||||
}
|
||||
in := setup.Stage2Input{
|
||||
IssuerURL: o.IssuerURL,
|
||||
ClientID: o.ClientID,
|
||||
|
||||
20
pkg/adaptors/env/env.go
vendored
20
pkg/adaptors/env/env.go
vendored
@@ -1,8 +1,11 @@
|
||||
// Package env provides environment dependent facilities.
|
||||
package env
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/google/wire"
|
||||
@@ -27,6 +30,7 @@ var Set = wire.NewSet(
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
ReadString(prompt string) (string, error)
|
||||
ReadPassword(prompt string) (string, error)
|
||||
OpenBrowser(url string) error
|
||||
}
|
||||
@@ -34,6 +38,20 @@ type Interface interface {
|
||||
// Env provides environment specific facilities.
|
||||
type Env struct{}
|
||||
|
||||
// ReadString reads a string from the stdin.
|
||||
func (*Env) ReadString(prompt string) (string, error) {
|
||||
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
|
||||
return "", xerrors.Errorf("could not write the prompt: %w", err)
|
||||
}
|
||||
r := bufio.NewReader(os.Stdin)
|
||||
s, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("could not read from stdin: %w", err)
|
||||
}
|
||||
s = strings.TrimRight(s, "\r\n")
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ReadPassword reads a password from the stdin without echo back.
|
||||
func (*Env) ReadPassword(prompt string) (string, error) {
|
||||
if _, err := fmt.Fprint(os.Stderr, prompt); err != nil {
|
||||
@@ -41,7 +59,7 @@ func (*Env) ReadPassword(prompt string) (string, error) {
|
||||
}
|
||||
b, err := terminal.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return "", xerrors.Errorf("could not read: %w", err)
|
||||
return "", xerrors.Errorf("could not read from stdin: %w", err)
|
||||
}
|
||||
if _, err := fmt.Fprintln(os.Stderr); err != nil {
|
||||
return "", xerrors.Errorf("could not write a new line: %w", err)
|
||||
|
||||
19
pkg/adaptors/env/mock_env/mock_env.go
vendored
19
pkg/adaptors/env/mock_env/mock_env.go
vendored
@@ -34,6 +34,7 @@ func (m *MockInterface) EXPECT() *MockInterfaceMockRecorder {
|
||||
|
||||
// OpenBrowser mocks base method
|
||||
func (m *MockInterface) OpenBrowser(arg0 string) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "OpenBrowser", arg0)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
@@ -41,11 +42,13 @@ func (m *MockInterface) OpenBrowser(arg0 string) error {
|
||||
|
||||
// OpenBrowser indicates an expected call of OpenBrowser
|
||||
func (mr *MockInterfaceMockRecorder) OpenBrowser(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenBrowser", reflect.TypeOf((*MockInterface)(nil).OpenBrowser), arg0)
|
||||
}
|
||||
|
||||
// ReadPassword mocks base method
|
||||
func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadPassword", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
@@ -54,5 +57,21 @@ func (m *MockInterface) ReadPassword(arg0 string) (string, error) {
|
||||
|
||||
// ReadPassword indicates an expected call of ReadPassword
|
||||
func (mr *MockInterfaceMockRecorder) ReadPassword(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPassword", reflect.TypeOf((*MockInterface)(nil).ReadPassword), arg0)
|
||||
}
|
||||
|
||||
// ReadString mocks base method
|
||||
func (m *MockInterface) ReadString(arg0 string) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReadString", arg0)
|
||||
ret0, _ := ret[0].(string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReadString indicates an expected call of ReadString
|
||||
func (mr *MockInterfaceMockRecorder) ReadString(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadString", reflect.TypeOf((*MockInterface)(nil).ReadString), arg0)
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ type Output struct {
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
const usernamePrompt = "Username: "
|
||||
const passwordPrompt = "Password: "
|
||||
|
||||
// Authentication provides the internal use-case of authentication.
|
||||
@@ -200,6 +201,13 @@ func (u *Authentication) doAuthCodeFlow(ctx context.Context, in *AuthCodeOption,
|
||||
|
||||
func (u *Authentication) doPasswordCredentialsFlow(ctx context.Context, in *ROPCOption, client oidcclient.Interface) (*Output, error) {
|
||||
u.Logger.V(1).Infof("performing the resource owner password credentials flow")
|
||||
if in.Username == "" {
|
||||
var err error
|
||||
in.Username, err = u.Env.ReadString(usernamePrompt)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("could not get the username: %w", err)
|
||||
}
|
||||
}
|
||||
if in.Password == "" {
|
||||
var err error
|
||||
in.Password, err = u.Env.ReadPassword(passwordPrompt)
|
||||
|
||||
@@ -141,6 +141,59 @@ func TestAuthentication_Do(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentialsFlow/AskUsernameAndPassword", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), timeout)
|
||||
defer cancel()
|
||||
in := Input{
|
||||
ROPCOption: &ROPCOption{},
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
}
|
||||
mockOIDCClient := mock_oidcclient.NewMockInterface(ctrl)
|
||||
mockOIDCClient.EXPECT().
|
||||
AuthenticateByPassword(gomock.Any(), "USER", "PASS").
|
||||
Return(&oidcclient.TokenSet{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}, nil)
|
||||
mockOIDCClientFactory := mock_oidcclient.NewMockFactoryInterface(ctrl)
|
||||
mockOIDCClientFactory.EXPECT().
|
||||
New(ctx, oidcclient.Config{
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
ClientID: "YOUR_CLIENT_ID",
|
||||
ClientSecret: "YOUR_CLIENT_SECRET",
|
||||
}).
|
||||
Return(mockOIDCClient, nil)
|
||||
mockEnv := mock_env.NewMockInterface(ctrl)
|
||||
mockEnv.EXPECT().ReadString(usernamePrompt).Return("USER", nil)
|
||||
mockEnv.EXPECT().ReadPassword(passwordPrompt).Return("PASS", nil)
|
||||
u := Authentication{
|
||||
OIDCClientFactory: mockOIDCClientFactory,
|
||||
Env: mockEnv,
|
||||
Logger: mock_logger.New(t),
|
||||
}
|
||||
out, err := u.Do(ctx, in)
|
||||
if err != nil {
|
||||
t.Errorf("Do returned error: %+v", err)
|
||||
}
|
||||
want := &Output{
|
||||
IDToken: "YOUR_ID_TOKEN",
|
||||
RefreshToken: "YOUR_REFRESH_TOKEN",
|
||||
IDTokenSubject: "YOUR_SUBJECT",
|
||||
IDTokenExpiry: futureTime,
|
||||
IDTokenClaims: dummyTokenClaims,
|
||||
}
|
||||
if diff := deep.Equal(want, out); diff != nil {
|
||||
t.Error(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ResourceOwnerPasswordCredentialsFlow/UsePassword", func(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
defer ctrl.Finish()
|
||||
|
||||
Reference in New Issue
Block a user