Add auth API to get self subject attributes
Signed-off-by: m.nabokikh <maksim.nabokikh@flant.com> Kubernetes-commit: 00dfba473b08528f0a21f7bd3b921f5dd35b013a
This commit is contained in:
parent
a38f2f2f78
commit
2988d073de
|
@ -20,6 +20,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/kubectl/pkg/cmd/auth"
|
||||
"k8s.io/kubectl/pkg/cmd/events"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
|
@ -38,6 +39,15 @@ func NewCmdAlpha(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.
|
|||
// from here to the CommandGroups defined by NewKubeletCommand() in cmd.go.
|
||||
cmd.AddCommand(events.NewCmdEvents(f, streams))
|
||||
|
||||
authCmds := &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Inspect authorization",
|
||||
Long: `Inspect authorization`,
|
||||
Run: cmdutil.DefaultSubCommandRun(streams.ErrOut),
|
||||
}
|
||||
cmd.AddCommand(authCmds)
|
||||
authCmds.AddCommand(auth.NewCmdWhoAmI(f, streams))
|
||||
|
||||
// NewKubeletCommand() will hide the alpha command if it has no subcommands. Overriding
|
||||
// the help function ensures a reasonable message if someone types the hidden command anyway.
|
||||
if !cmd.HasAvailableSubCommands() {
|
||||
|
|
|
@ -0,0 +1,203 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes 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"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
authenticationv1alpha1 "k8s.io/api/authentication/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
authenticationv1alpha1client "k8s.io/client-go/kubernetes/typed/authentication/v1alpha1"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// WhoAmIFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which
|
||||
// reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes
|
||||
// the logic itself easy to unit test.
|
||||
type WhoAmIFlags struct {
|
||||
RESTClientGetter genericclioptions.RESTClientGetter
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewWhoAmIFlags returns a default WhoAmIFlags.
|
||||
func NewWhoAmIFlags(restClientGetter genericclioptions.RESTClientGetter, streams genericclioptions.IOStreams) *WhoAmIFlags {
|
||||
return &WhoAmIFlags{
|
||||
RESTClientGetter: restClientGetter,
|
||||
PrintFlags: genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme),
|
||||
IOStreams: streams,
|
||||
}
|
||||
}
|
||||
|
||||
// AddFlags registers flags for a cli.
|
||||
func (flags *WhoAmIFlags) AddFlags(cmd *cobra.Command) {
|
||||
flags.PrintFlags.AddFlags(cmd)
|
||||
}
|
||||
|
||||
// ToOptions converts from CLI inputs to runtime inputs.
|
||||
func (flags *WhoAmIFlags) ToOptions(ctx context.Context, args []string) (*WhoAmIOptions, error) {
|
||||
w := &WhoAmIOptions{
|
||||
ctx: ctx,
|
||||
IOStreams: flags.IOStreams,
|
||||
}
|
||||
|
||||
clientConfig, err := flags.RESTClientGetter.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w.authClient, err = authenticationv1alpha1client.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !flags.PrintFlags.OutputFlagSpecified() {
|
||||
w.resourcePrinterFunc = printTableSelfSubjectAccessReview
|
||||
} else {
|
||||
printer, err := flags.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
w.resourcePrinterFunc = printer.PrintObj
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// WhoAmIOptions is the start of the data required to perform the operation. As new fields are added,
|
||||
// add them here instead of referencing the cmd.Flags()
|
||||
type WhoAmIOptions struct {
|
||||
authClient authenticationv1alpha1client.AuthenticationV1alpha1Interface
|
||||
ctx context.Context
|
||||
|
||||
resourcePrinterFunc printers.ResourcePrinterFunc
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
var (
|
||||
whoAmILong = templates.LongDesc(`
|
||||
Experimental: Check who you are and your attributes (groups, extra).
|
||||
|
||||
This command is helpful to get yourself aware of the current user attributes,
|
||||
especially when dynamic authentication, e.g., token webhook, auth proxy, or OIDC provider,
|
||||
is enabled in the Kubernetes cluster.
|
||||
`)
|
||||
|
||||
whoAmIExample = templates.Examples(`
|
||||
# Get your subject attributes.
|
||||
kubectl alpha auth whoami
|
||||
|
||||
# Get your subject attributes in JSON format.
|
||||
kubectl alpha auth whoami -o json
|
||||
`)
|
||||
)
|
||||
|
||||
// NewCmdWhoAmI returns an initialized Command for 'auth whoami' sub command. Experimental.
|
||||
func NewCmdWhoAmI(restClientGetter genericclioptions.RESTClientGetter, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
flags := NewWhoAmIFlags(restClientGetter, streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "whoami",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Experimental: Check self subject attributes",
|
||||
Long: whoAmILong,
|
||||
Example: whoAmIExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o, err := flags.ToOptions(cmd.Context(), args)
|
||||
cmdutil.CheckErr(err)
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
|
||||
flags.AddFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Run prints all user attributes.
|
||||
func (o WhoAmIOptions) Run() error {
|
||||
sar := &authenticationv1alpha1.SelfSubjectReview{}
|
||||
response, err := o.authClient.SelfSubjectReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return fmt.Errorf("the selfsubjectreviews API is not enabled in the cluster.\n" +
|
||||
"enable APISelfSubjectReview feature gate and authentication.k8s.io/v1alpha1 API.")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return o.resourcePrinterFunc(response, o.Out)
|
||||
}
|
||||
|
||||
func printTableSelfSubjectAccessReview(obj runtime.Object, out io.Writer) error {
|
||||
ssr, ok := obj.(*authenticationv1alpha1.SelfSubjectReview)
|
||||
if !ok {
|
||||
return fmt.Errorf("object is not SelfSubjectReview")
|
||||
}
|
||||
|
||||
w := printers.GetNewTabWriter(out)
|
||||
defer w.Flush()
|
||||
|
||||
_, err := fmt.Fprintf(w, "ATTRIBUTE\tVALUE\n")
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write a header: %w", err)
|
||||
}
|
||||
|
||||
ui := ssr.Status.UserInfo
|
||||
|
||||
if ui.Username != "" {
|
||||
_, err := fmt.Fprintf(w, "Username\t%s\n", ui.Username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write a username: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if ui.UID != "" {
|
||||
_, err := fmt.Fprintf(w, "UID\t%s\n", ui.UID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write a uid: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ui.Groups) > 0 {
|
||||
_, err := fmt.Fprintf(w, "Groups\t%v\n", ui.Groups)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write groups: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ui.Extra) > 0 {
|
||||
for k, v := range ui.Extra {
|
||||
_, err := fmt.Fprintf(w, "Extra: %s\t%v\n", k, v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write an extra: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
Copyright 2017 The Kubernetes 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 (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
authenticationv1alpha1 "k8s.io/api/authentication/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
authfake "k8s.io/client-go/kubernetes/fake"
|
||||
core "k8s.io/client-go/testing"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
)
|
||||
|
||||
func TestWhoAmIRun(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
o *WhoAmIOptions
|
||||
args []string
|
||||
allowed bool
|
||||
serverErr error
|
||||
|
||||
expectedBodyStrings []string
|
||||
}{
|
||||
{
|
||||
name: "success test",
|
||||
o: &WhoAmIOptions{
|
||||
resourcePrinterFunc: printTableSelfSubjectAccessReview,
|
||||
},
|
||||
args: []string{},
|
||||
expectedBodyStrings: []string{
|
||||
`ATTRIBUTE VALUE`,
|
||||
`Username jane.doe`,
|
||||
`UID uniq-id`,
|
||||
`Groups [students teachers]`,
|
||||
`Extra: subjects [math sports]`,
|
||||
`Extra: skills [reading learning]`,
|
||||
``,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "JSON test",
|
||||
o: &WhoAmIOptions{
|
||||
resourcePrinterFunc: printers.NewTypeSetter(scheme.Scheme).ToPrinter(&printers.JSONPrinter{}).PrintObj,
|
||||
},
|
||||
args: []string{},
|
||||
expectedBodyStrings: []string{
|
||||
`{
|
||||
"kind": "SelfSubjectReview",
|
||||
"apiVersion": "authentication.k8s.io/v1alpha1",
|
||||
"metadata": {
|
||||
"creationTimestamp": null
|
||||
},
|
||||
"status": {
|
||||
"userInfo": {
|
||||
"username": "jane.doe",
|
||||
"uid": "uniq-id",
|
||||
"groups": [
|
||||
"students",
|
||||
"teachers"
|
||||
],
|
||||
"extra": {
|
||||
"skills": [
|
||||
"reading",
|
||||
"learning"
|
||||
],
|
||||
"subjects": [
|
||||
"math",
|
||||
"sports"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var b bytes.Buffer
|
||||
|
||||
test.o.Out = &b
|
||||
test.o.ErrOut = ioutil.Discard
|
||||
|
||||
tf := cmdtesting.NewTestFactory().WithNamespace("test")
|
||||
defer tf.Cleanup()
|
||||
|
||||
fakeAuthClientSet := &authfake.Clientset{}
|
||||
|
||||
fakeAuthClientSet.AddReactor("create", "selfsubjectreviews",
|
||||
func(action core.Action) (handled bool, ret runtime.Object, err error) {
|
||||
res := &authenticationv1alpha1.SelfSubjectReview{
|
||||
Status: authenticationv1alpha1.SelfSubjectReviewStatus{
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
Username: "jane.doe",
|
||||
UID: "uniq-id",
|
||||
Groups: []string{"students", "teachers"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"subjects": {"math", "sports"},
|
||||
"skills": {"reading", "learning"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return true, res, nil
|
||||
})
|
||||
test.o.authClient = fakeAuthClientSet.AuthenticationV1alpha1()
|
||||
|
||||
err := test.o.Run()
|
||||
switch {
|
||||
case test.serverErr == nil && err == nil:
|
||||
// pass
|
||||
case err != nil && test.serverErr != nil && strings.Contains(err.Error(), test.serverErr.Error()):
|
||||
// pass
|
||||
default:
|
||||
t.Errorf("%s: expected %v, got %v", test.name, test.serverErr, err)
|
||||
return
|
||||
}
|
||||
|
||||
res := b.String()
|
||||
expectedBody := strings.Join(test.expectedBodyStrings, "\n")
|
||||
|
||||
if expectedBody != res {
|
||||
t.Errorf("%s: expected \n%q, got \n%q", test.name, expectedBody, res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -583,6 +583,7 @@ func (f *TestFactory) KubernetesClientSet() (*kubernetes.Clientset, error) {
|
|||
clientset.AuthorizationV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client
|
||||
clientset.AuthorizationV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client
|
||||
clientset.AuthorizationV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client
|
||||
clientset.AuthenticationV1alpha1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client
|
||||
clientset.AutoscalingV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client
|
||||
clientset.AutoscalingV2beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client
|
||||
clientset.BatchV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client
|
||||
|
|
Loading…
Reference in New Issue