...

Source file src/k8s.io/kubectl/pkg/cmd/auth/cani_test.go

Documentation: k8s.io/kubectl/pkg/cmd/auth

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package auth
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"strings"
    25  	"testing"
    26  
    27  	authorizationv1 "k8s.io/api/authorization/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	"k8s.io/apimachinery/pkg/runtime/schema"
    30  	"k8s.io/cli-runtime/pkg/genericiooptions"
    31  	"k8s.io/cli-runtime/pkg/printers"
    32  	restclient "k8s.io/client-go/rest"
    33  	"k8s.io/client-go/rest/fake"
    34  	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
    35  	"k8s.io/kubectl/pkg/scheme"
    36  )
    37  
    38  func TestRunAccessCheck(t *testing.T) {
    39  	tests := []struct {
    40  		name      string
    41  		o         *CanIOptions
    42  		args      []string
    43  		allowed   bool
    44  		serverErr error
    45  
    46  		expectedBodyStrings []string
    47  	}{
    48  		{
    49  			name:    "restmapping for args",
    50  			o:       &CanIOptions{},
    51  			args:    []string{"get", "replicaset"},
    52  			allowed: true,
    53  			expectedBodyStrings: []string{
    54  				`{"resourceAttributes":{"namespace":"test","verb":"get","group":"extensions","resource":"replicasets"}}`,
    55  			},
    56  		},
    57  		{
    58  			name:    "simple success",
    59  			o:       &CanIOptions{},
    60  			args:    []string{"get", "deployments.extensions/foo"},
    61  			allowed: true,
    62  			expectedBodyStrings: []string{
    63  				`{"resourceAttributes":{"namespace":"test","verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`,
    64  			},
    65  		},
    66  		{
    67  			name: "all namespaces",
    68  			o: &CanIOptions{
    69  				AllNamespaces: true,
    70  			},
    71  			args:    []string{"get", "deployments.extensions/foo"},
    72  			allowed: true,
    73  			expectedBodyStrings: []string{
    74  				`{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`,
    75  			},
    76  		},
    77  		{
    78  			name: "disallowed",
    79  			o: &CanIOptions{
    80  				AllNamespaces: true,
    81  			},
    82  			args:    []string{"get", "deployments.extensions/foo"},
    83  			allowed: false,
    84  			expectedBodyStrings: []string{
    85  				`{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`,
    86  			},
    87  		},
    88  		{
    89  			name: "forcedError",
    90  			o: &CanIOptions{
    91  				AllNamespaces: true,
    92  			},
    93  			args:      []string{"get", "deployments.extensions/foo"},
    94  			allowed:   false,
    95  			serverErr: fmt.Errorf("forcedError"),
    96  			expectedBodyStrings: []string{
    97  				`{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`,
    98  			},
    99  		},
   100  		{
   101  			name: "sub resource",
   102  			o: &CanIOptions{
   103  				AllNamespaces: true,
   104  				Subresource:   "log",
   105  			},
   106  			args:    []string{"get", "pods"},
   107  			allowed: true,
   108  			expectedBodyStrings: []string{
   109  				`{"resourceAttributes":{"verb":"get","resource":"pods","subresource":"log"}}`,
   110  			},
   111  		},
   112  		{
   113  			name:    "nonResourceURL",
   114  			o:       &CanIOptions{},
   115  			args:    []string{"get", "/logs"},
   116  			allowed: true,
   117  			expectedBodyStrings: []string{
   118  				`{"nonResourceAttributes":{"path":"/logs","verb":"get"}}`,
   119  			},
   120  		},
   121  	}
   122  
   123  	for _, test := range tests {
   124  		t.Run(test.name, func(t *testing.T) {
   125  			test.o.Out = io.Discard
   126  			test.o.ErrOut = io.Discard
   127  
   128  			tf := cmdtesting.NewTestFactory().WithNamespace("test")
   129  			defer tf.Cleanup()
   130  
   131  			ns := scheme.Codecs.WithoutConversion()
   132  
   133  			tf.Client = &fake.RESTClient{
   134  				GroupVersion:         schema.GroupVersion{Group: "", Version: "v1"},
   135  				NegotiatedSerializer: ns,
   136  				Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   137  					expectPath := "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews"
   138  					if req.URL.Path != expectPath {
   139  						t.Errorf("%s: expected %v, got %v", test.name, expectPath, req.URL.Path)
   140  						return nil, nil
   141  					}
   142  					bodyBits, err := io.ReadAll(req.Body)
   143  					if err != nil {
   144  						t.Errorf("%s: %v", test.name, err)
   145  						return nil, nil
   146  					}
   147  					body := string(bodyBits)
   148  
   149  					for _, expectedBody := range test.expectedBodyStrings {
   150  						if !strings.Contains(body, expectedBody) {
   151  							t.Errorf("%s expecting %s in %s", test.name, expectedBody, body)
   152  						}
   153  					}
   154  
   155  					return &http.Response{
   156  							StatusCode: http.StatusOK,
   157  							Body: io.NopCloser(bytes.NewBufferString(
   158  								fmt.Sprintf(`{"kind":"SelfSubjectAccessReview","apiVersion":"authorization.k8s.io/v1","status":{"allowed":%v}}`, test.allowed),
   159  							)),
   160  						},
   161  						test.serverErr
   162  				}),
   163  			}
   164  			tf.ClientConfigVal = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &schema.GroupVersion{Group: "", Version: "v1"}}}
   165  
   166  			if err := test.o.Complete(tf, test.args); err != nil {
   167  				t.Errorf("%s: %v", test.name, err)
   168  				return
   169  			}
   170  
   171  			actualAllowed, err := test.o.RunAccessCheck()
   172  			switch {
   173  			case test.serverErr == nil && err == nil:
   174  				// pass
   175  			case err != nil && test.serverErr != nil && strings.Contains(err.Error(), test.serverErr.Error()):
   176  				// pass
   177  			default:
   178  				t.Errorf("%s: expected %v, got %v", test.name, test.serverErr, err)
   179  				return
   180  			}
   181  			if actualAllowed != test.allowed {
   182  				t.Errorf("%s: expected %v, got %v", test.name, test.allowed, actualAllowed)
   183  				return
   184  			}
   185  		})
   186  	}
   187  }
   188  
   189  func TestRunAccessList(t *testing.T) {
   190  	t.Run("test access list", func(t *testing.T) {
   191  		options := &CanIOptions{List: true}
   192  		expectedOutput := "Resources   Non-Resource URLs   Resource Names    Verbs\n" +
   193  			"job.*       []                  [test-resource]   [get list]\n" +
   194  			"pod.*       []                  [test-resource]   [get list]\n" +
   195  			"            [/apis/*]           []                [get]\n" +
   196  			"            [/version]          []                [get]\n"
   197  
   198  		tf := cmdtesting.NewTestFactory().WithNamespace("test")
   199  		defer tf.Cleanup()
   200  
   201  		ns := scheme.Codecs.WithoutConversion()
   202  		codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
   203  
   204  		tf.Client = &fake.RESTClient{
   205  			GroupVersion:         schema.GroupVersion{Group: "", Version: "v1"},
   206  			NegotiatedSerializer: ns,
   207  			Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
   208  				switch req.URL.Path {
   209  				case "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews":
   210  					body := io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, getSelfSubjectRulesReview()))))
   211  					return &http.Response{StatusCode: http.StatusOK, Body: body}, nil
   212  				default:
   213  					t.Fatalf("unexpected request: %#v\n%#v", req.URL, req)
   214  					return nil, nil
   215  				}
   216  			}),
   217  		}
   218  		ioStreams, _, buf, _ := genericiooptions.NewTestIOStreams()
   219  		options.IOStreams = ioStreams
   220  		if err := options.Complete(tf, []string{}); err != nil {
   221  			t.Errorf("got unexpected error when do Complete(): %v", err)
   222  			return
   223  		}
   224  
   225  		err := options.RunAccessList()
   226  		if err != nil {
   227  			t.Errorf("got unexpected error when do RunAccessList(): %v", err)
   228  		} else if buf.String() != expectedOutput {
   229  			t.Errorf("expected %v\n but got %v\n", expectedOutput, buf.String())
   230  		}
   231  	})
   232  }
   233  
   234  func TestRunResourceFor(t *testing.T) {
   235  	tests := []struct {
   236  		name        string
   237  		o           *CanIOptions
   238  		resourceArg string
   239  
   240  		expectGVR      schema.GroupVersionResource
   241  		expectedErrOut string
   242  	}{
   243  		{
   244  			name:        "any resources",
   245  			o:           &CanIOptions{},
   246  			resourceArg: "*",
   247  			expectGVR: schema.GroupVersionResource{
   248  				Resource: "*",
   249  			},
   250  		},
   251  		{
   252  			name:        "server-supported standard resources without group",
   253  			o:           &CanIOptions{},
   254  			resourceArg: "pods",
   255  			expectGVR: schema.GroupVersionResource{
   256  				Version:  "v1",
   257  				Resource: "pods",
   258  			},
   259  		},
   260  		{
   261  			name:        "server-supported standard resources with group",
   262  			o:           &CanIOptions{},
   263  			resourceArg: "jobs",
   264  			expectGVR: schema.GroupVersionResource{
   265  				Group:    "batch",
   266  				Version:  "v1",
   267  				Resource: "jobs",
   268  			},
   269  		},
   270  		{
   271  			name:        "server-supported nonstandard resources",
   272  			o:           &CanIOptions{},
   273  			resourceArg: "users",
   274  			expectGVR: schema.GroupVersionResource{
   275  				Resource: "users",
   276  			},
   277  		},
   278  		{
   279  			name:        "invalid resources",
   280  			o:           &CanIOptions{},
   281  			resourceArg: "invalid",
   282  			expectGVR: schema.GroupVersionResource{
   283  				Resource: "invalid",
   284  			},
   285  			expectedErrOut: "Warning: the server doesn't have a resource type 'invalid'\n\n",
   286  		},
   287  	}
   288  
   289  	for _, test := range tests {
   290  		t.Run(test.name, func(t *testing.T) {
   291  			tf := cmdtesting.NewTestFactory().WithNamespace("test")
   292  			defer tf.Cleanup()
   293  
   294  			ioStreams, _, _, buf := genericiooptions.NewTestIOStreams()
   295  			test.o.IOStreams = ioStreams
   296  			test.o.WarningPrinter = printers.NewWarningPrinter(test.o.IOStreams.ErrOut, printers.WarningPrinterOptions{Color: false})
   297  
   298  			restMapper, err := tf.ToRESTMapper()
   299  			if err != nil {
   300  				t.Errorf("got unexpected error when do tf.ToRESTMapper(): %v", err)
   301  				return
   302  			}
   303  			gvr := test.o.resourceFor(restMapper, test.resourceArg)
   304  			if gvr != test.expectGVR {
   305  				t.Errorf("expected %v\n but got %v\n", test.expectGVR, gvr)
   306  			}
   307  			if buf.String() != test.expectedErrOut {
   308  				t.Errorf("expected %v\n but got %v\n", test.expectedErrOut, buf.String())
   309  			}
   310  		})
   311  	}
   312  }
   313  
   314  func getSelfSubjectRulesReview() *authorizationv1.SelfSubjectRulesReview {
   315  	return &authorizationv1.SelfSubjectRulesReview{
   316  		Status: authorizationv1.SubjectRulesReviewStatus{
   317  			ResourceRules: []authorizationv1.ResourceRule{
   318  				{
   319  					Verbs:         []string{"get", "list"},
   320  					APIGroups:     []string{"*"},
   321  					Resources:     []string{"pod", "job"},
   322  					ResourceNames: []string{"test-resource"},
   323  				},
   324  			},
   325  			NonResourceRules: []authorizationv1.NonResourceRule{
   326  				{
   327  					Verbs:           []string{"get"},
   328  					NonResourceURLs: []string{"/apis/*", "/version"},
   329  				},
   330  			},
   331  		},
   332  	}
   333  }
   334  

View as plain text