diff --git a/docs/.generated_docs b/docs/.generated_docs index efcaad246a8..361aa97fd2a 100644 --- a/docs/.generated_docs +++ b/docs/.generated_docs @@ -15,6 +15,8 @@ docs/man/man1/kubectl-api-versions.1 docs/man/man1/kubectl-apply-view-last-applied.1 docs/man/man1/kubectl-apply.1 docs/man/man1/kubectl-attach.1 +docs/man/man1/kubectl-auth-can-i.1 +docs/man/man1/kubectl-auth.1 docs/man/man1/kubectl-autoscale.1 docs/man/man1/kubectl-certificate-approve.1 docs/man/man1/kubectl-certificate-deny.1 @@ -101,6 +103,8 @@ docs/user-guide/kubectl/kubectl_api-versions.md docs/user-guide/kubectl/kubectl_apply.md docs/user-guide/kubectl/kubectl_apply_view-last-applied.md docs/user-guide/kubectl/kubectl_attach.md +docs/user-guide/kubectl/kubectl_auth.md +docs/user-guide/kubectl/kubectl_auth_can-i.md docs/user-guide/kubectl/kubectl_autoscale.md docs/user-guide/kubectl/kubectl_certificate.md docs/user-guide/kubectl/kubectl_certificate_approve.md @@ -183,6 +187,7 @@ docs/yaml/kubectl/kubectl_annotate.yaml docs/yaml/kubectl/kubectl_api-versions.yaml docs/yaml/kubectl/kubectl_apply.yaml docs/yaml/kubectl/kubectl_attach.yaml +docs/yaml/kubectl/kubectl_auth.yaml docs/yaml/kubectl/kubectl_autoscale.yaml docs/yaml/kubectl/kubectl_certificate.yaml docs/yaml/kubectl/kubectl_cluster-info.yaml diff --git a/docs/man/man1/kubectl-auth-can-i.1 b/docs/man/man1/kubectl-auth-can-i.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubectl-auth-can-i.1 @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/docs/man/man1/kubectl-auth.1 b/docs/man/man1/kubectl-auth.1 new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/man/man1/kubectl-auth.1 @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/docs/user-guide/kubectl/kubectl_auth.md b/docs/user-guide/kubectl/kubectl_auth.md new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_auth.md @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/docs/user-guide/kubectl/kubectl_auth_can-i.md b/docs/user-guide/kubectl/kubectl_auth_can-i.md new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_auth_can-i.md @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/docs/yaml/kubectl/kubectl_auth.yaml b/docs/yaml/kubectl/kubectl_auth.yaml new file mode 100644 index 00000000000..b6fd7a0f989 --- /dev/null +++ b/docs/yaml/kubectl/kubectl_auth.yaml @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 1a18d7dd9aa..14dfb2f29de 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -499,6 +499,7 @@ pv-recycler-pod-template-filepath-hostpath pv-recycler-pod-template-filepath-nfs pv-recycler-timeout-increment-hostpath pvclaimbinder-sync-period +quiet read-only-port really-crash-for-testing reconcile-cidr diff --git a/pkg/kubectl/cmd/BUILD b/pkg/kubectl/cmd/BUILD index 95645cd6b92..193c58f86fb 100644 --- a/pkg/kubectl/cmd/BUILD +++ b/pkg/kubectl/cmd/BUILD @@ -80,6 +80,7 @@ go_library( "//pkg/client/unversioned:go_default_library", "//pkg/client/unversioned/remotecommand:go_default_library", "//pkg/kubectl:go_default_library", + "//pkg/kubectl/cmd/auth:go_default_library", "//pkg/kubectl/cmd/config:go_default_library", "//pkg/kubectl/cmd/rollout:go_default_library", "//pkg/kubectl/cmd/set:go_default_library", @@ -238,6 +239,7 @@ filegroup( name = "all-srcs", srcs = [ ":package-srcs", + "//pkg/kubectl/cmd/auth:all-srcs", "//pkg/kubectl/cmd/config:all-srcs", "//pkg/kubectl/cmd/rollout:all-srcs", "//pkg/kubectl/cmd/set:all-srcs", diff --git a/pkg/kubectl/cmd/auth/BUILD b/pkg/kubectl/cmd/auth/BUILD new file mode 100644 index 00000000000..2806de832bf --- /dev/null +++ b/pkg/kubectl/cmd/auth/BUILD @@ -0,0 +1,54 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_library( + name = "go_default_library", + srcs = [ + "auth.go", + "cani.go", + ], + tags = ["automanaged"], + deps = [ + "//pkg/apis/authorization:go_default_library", + "//pkg/client/clientset_generated/internalclientset/typed/authorization/internalversion:go_default_library", + "//pkg/kubectl/cmd/templates:go_default_library", + "//pkg/kubectl/cmd/util:go_default_library", + "//vendor:github.com/spf13/cobra", + "//vendor:k8s.io/apimachinery/pkg/api/meta", + "//vendor:k8s.io/apimachinery/pkg/runtime/schema", + "//vendor:k8s.io/apimachinery/pkg/util/errors", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) + +go_test( + name = "go_default_test", + srcs = ["cani_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = [ + "//pkg/api:go_default_library", + "//pkg/kubectl/cmd/testing:go_default_library", + "//vendor:k8s.io/client-go/rest", + "//vendor:k8s.io/client-go/rest/fake", + ], +) diff --git a/pkg/kubectl/cmd/auth/auth.go b/pkg/kubectl/cmd/auth/auth.go new file mode 100644 index 00000000000..689767a1d6a --- /dev/null +++ b/pkg/kubectl/cmd/auth/auth.go @@ -0,0 +1,39 @@ +/* +Copyright 2014 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 ( + "io" + + "github.com/spf13/cobra" + + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" +) + +func NewCmdAuth(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { + // Parent command to which all subcommands are added. + cmds := &cobra.Command{ + Use: "auth", + Short: "Inspect authorization", + Long: `Inspect authorization`, + Run: cmdutil.DefaultSubCommandRun(errOut), + } + + cmds.AddCommand(NewCmdCanI(f, out, errOut)) + + return cmds +} diff --git a/pkg/kubectl/cmd/auth/cani.go b/pkg/kubectl/cmd/auth/cani.go new file mode 100644 index 00000000000..44891867401 --- /dev/null +++ b/pkg/kubectl/cmd/auth/cani.go @@ -0,0 +1,196 @@ +/* +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 ( + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + + "github.com/spf13/cobra" + + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime/schema" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" + internalauthorizationclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/authorization/internalversion" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" +) + +// CanIOptions 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 CanIOptions struct { + AllNamespaces bool + Quiet bool + Namespace string + SelfSARClient internalauthorizationclient.SelfSubjectAccessReviewsGetter + + Verb string + Resource schema.GroupVersionResource + ResourceName string + + Out io.Writer + Err io.Writer +} + +var ( + canILong = templates.LongDesc(` + Check whether an action is allowed. + + VERB is a logical Kubernetes API verb like 'get', 'list', 'watch', 'delete', etc. + TYPE is a Kubernetes resource. Shortcuts and groups will be resolved. + NAME is the name of a particular Kubernetes resource.`) + + canIExample = templates.Examples(` + # Check to see if I can create pods in any namespace + kubectl auth can-i create pods --all-namespaces + + # Check to see if I can list deployments in my current namespace + kubectl auth can-i list deployments.extensions + + # Check to see if I can get the job named "bar" in namespace "foo" + kubectl auth can-i list jobs.batch/bar -n foo`) +) + +func NewCmdCanI(f cmdutil.Factory, out, err io.Writer) *cobra.Command { + o := &CanIOptions{ + Out: out, + Err: err, + } + + cmd := &cobra.Command{ + Use: "can-i VERB [TYPE | TYPE/NAME]", + Short: "Check whether an action is allowed", + Long: canILong, + Example: canIExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, args)) + cmdutil.CheckErr(o.Validate()) + + allowed, err := o.RunAccessCheck() + if err == nil { + return + } + + if o.Quiet && !allowed { + os.Exit(1) + } + + cmdutil.CheckErr(err) + }, + } + + cmd.Flags().BoolVar(&o.AllNamespaces, "all-namespaces", o.AllNamespaces, "If true, check the specified action in all namespaces.") + cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "If true, suppress output and just return the exit code.") + return cmd +} + +func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error { + switch len(args) { + case 2: + resourceTokens := strings.SplitN(args[1], "/", 2) + restMapper, _ := f.Object() + o.Verb = args[0] + o.Resource = resourceFor(restMapper, resourceTokens[0]) + if len(resourceTokens) > 1 { + o.ResourceName = resourceTokens[1] + } + default: + return errors.New("you must specify two or three arguments: verb, resource, and optional resourceName") + } + + var err error + client, err := f.ClientSet() + if err != nil { + return err + } + o.SelfSARClient = client.Authorization() + + o.Namespace = "" + if !o.AllNamespaces { + o.Namespace, _, err = f.DefaultNamespace() + if err != nil { + return err + } + } + + if o.Quiet { + o.Out = ioutil.Discard + } + + return nil +} + +func (o *CanIOptions) Validate() error { + errors := []error{} + return utilerrors.NewAggregate(errors) +} + +func (o *CanIOptions) RunAccessCheck() (bool, error) { + sar := &authorizationapi.SelfSubjectAccessReview{ + Spec: authorizationapi.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationapi.ResourceAttributes{ + Namespace: o.Namespace, + Verb: o.Verb, + Group: o.Resource.Group, + Resource: o.Resource.Resource, + Name: o.ResourceName, + }, + }, + } + + response, err := o.SelfSARClient.SelfSubjectAccessReviews().Create(sar) + if err != nil { + return false, err + } + + if response.Status.Allowed { + fmt.Fprintln(o.Out, "yes") + } else { + fmt.Fprint(o.Out, "no") + if len(response.Status.Reason) > 0 { + fmt.Fprintf(o.Out, " - %v", response.Status.Reason) + } + if len(response.Status.EvaluationError) > 0 { + fmt.Fprintf(o.Out, " - %v", response.Status.EvaluationError) + } + fmt.Fprintln(o.Out) + } + + return response.Status.Allowed, nil +} + +func resourceFor(mapper meta.RESTMapper, resourceArg string) schema.GroupVersionResource { + fullySpecifiedGVR, groupResource := schema.ParseResourceArg(strings.ToLower(resourceArg)) + gvr := schema.GroupVersionResource{} + if fullySpecifiedGVR != nil { + gvr, _ = mapper.ResourceFor(*fullySpecifiedGVR) + } + if gvr.Empty() { + var err error + gvr, err = mapper.ResourceFor(groupResource.WithVersion("")) + if err != nil { + return schema.GroupVersionResource{Resource: resourceArg} + } + } + + return gvr +} diff --git a/pkg/kubectl/cmd/auth/cani_test.go b/pkg/kubectl/cmd/auth/cani_test.go new file mode 100644 index 00000000000..6c1bde3ed93 --- /dev/null +++ b/pkg/kubectl/cmd/auth/cani_test.go @@ -0,0 +1,155 @@ +/* +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" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/rest/fake" + "k8s.io/kubernetes/pkg/api" + cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" +) + +func TestRunAccessCheck(t *testing.T) { + tests := []struct { + name string + o *CanIOptions + args []string + allowed bool + serverErr error + + expectedBodyStrings []string + }{ + { + name: "restmapping for args", + o: &CanIOptions{}, + args: []string{"get", "replicaset"}, + allowed: true, + expectedBodyStrings: []string{ + `{"resourceAttributes":{"namespace":"test","verb":"get","group":"extensions","resource":"replicasets"}}`, + }, + }, + { + name: "simple success", + o: &CanIOptions{}, + args: []string{"get", "deployments.extensions/foo"}, + allowed: true, + expectedBodyStrings: []string{ + `{"resourceAttributes":{"namespace":"test","verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`, + }, + }, + { + name: "all namespaces", + o: &CanIOptions{ + AllNamespaces: true, + }, + args: []string{"get", "deployments.extensions/foo"}, + allowed: true, + expectedBodyStrings: []string{ + `{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`, + }, + }, + { + name: "disallowed", + o: &CanIOptions{ + AllNamespaces: true, + }, + args: []string{"get", "deployments.extensions/foo"}, + allowed: false, + expectedBodyStrings: []string{ + `{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`, + }, + }, + { + name: "forcedError", + o: &CanIOptions{ + AllNamespaces: true, + }, + args: []string{"get", "deployments.extensions/foo"}, + allowed: false, + serverErr: fmt.Errorf("forcedError"), + expectedBodyStrings: []string{ + `{"resourceAttributes":{"verb":"get","group":"extensions","resource":"deployments","name":"foo"}}`, + }, + }, + } + + for _, test := range tests { + test.o.Out = ioutil.Discard + test.o.Err = ioutil.Discard + + f, tf, _, ns := cmdtesting.NewAPIFactory() + tf.Client = &fake.RESTClient{ + APIRegistry: api.Registry, + NegotiatedSerializer: ns, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + if req.URL.Path != "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews" { + t.Errorf("%s: %v", test.name, req.URL.Path) + return nil, nil + } + bodyBits, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Errorf("%s: %v", test.name, err) + return nil, nil + } + body := string(bodyBits) + + for _, expectedBody := range test.expectedBodyStrings { + if !strings.Contains(body, expectedBody) { + t.Errorf("%s expecting %s in %s", test.name, expectedBody, body) + } + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewBufferString( + fmt.Sprintf(`{"kind":"SelfSubjectAccessReview","apiVersion":"authorization.k8s.io/v1","status":{"allowed":%v}}`, test.allowed), + )), + }, + test.serverErr + }), + } + tf.Namespace = "test" + tf.ClientConfig = &restclient.Config{ContentConfig: restclient.ContentConfig{GroupVersion: &api.Registry.GroupOrDie(api.GroupName).GroupVersion}} + + if err := test.o.Complete(f, test.args); err != nil { + t.Errorf("%s: %v", test.name, err) + continue + } + + actualAllowed, err := test.o.RunAccessCheck() + 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) + continue + } + if actualAllowed != test.allowed { + t.Errorf("%s: expected %v, got %v", test.name, test.allowed, actualAllowed) + continue + } + } +} diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 1ceb0ec079c..b96a796689d 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -22,6 +22,7 @@ import ( "k8s.io/apiserver/pkg/util/flag" "k8s.io/client-go/tools/clientcmd" + "k8s.io/kubernetes/pkg/kubectl/cmd/auth" cmdconfig "k8s.io/kubernetes/pkg/kubectl/cmd/config" "k8s.io/kubernetes/pkg/kubectl/cmd/rollout" "k8s.io/kubernetes/pkg/kubectl/cmd/set" @@ -286,6 +287,7 @@ func NewKubectlCommand(f cmdutil.Factory, in io.Reader, out, err io.Writer) *cob NewCmdPortForward(f, out, err), NewCmdProxy(f, out), NewCmdCp(f, in, out, err), + auth.NewCmdAuth(f, out, err), }, }, { diff --git a/pkg/kubectl/cmd/testing/fake.go b/pkg/kubectl/cmd/testing/fake.go index 3ec2be83235..eee44e6867c 100644 --- a/pkg/kubectl/cmd/testing/fake.go +++ b/pkg/kubectl/cmd/testing/fake.go @@ -258,16 +258,7 @@ func (f *FakeFactory) FlagSet() *pflag.FlagSet { } func (f *FakeFactory) Object() (meta.RESTMapper, runtime.ObjectTyper) { - priorityRESTMapper := meta.PriorityRESTMapper{ - Delegate: f.tf.Mapper, - ResourcePriority: []schema.GroupVersionResource{ - {Group: meta.AnyGroup, Version: "v1", Resource: meta.AnyResource}, - }, - KindPriority: []schema.GroupVersionKind{ - {Group: meta.AnyGroup, Version: "v1", Kind: meta.AnyKind}, - }, - } - return priorityRESTMapper, f.tf.Typer + return api.Registry.RESTMapper(), f.tf.Typer } func (f *FakeFactory) UnstructuredObject() (meta.RESTMapper, runtime.ObjectTyper, error) {