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:
m.nabokikh 2022-07-22 04:01:52 +04:00 committed by Kubernetes Publisher
parent a38f2f2f78
commit 2988d073de
4 changed files with 364 additions and 0 deletions

View File

@ -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() {

203
pkg/cmd/auth/whoami.go Normal file
View File

@ -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
}

150
pkg/cmd/auth/whoami_test.go Normal file
View File

@ -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)
}
})
}
}

View File

@ -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