Merge pull request #41077 from deads2k/cli-01-cani

Automatic merge from submit-queue (batch tested with PRs 41814, 41922, 41957, 41406, 41077)

add kubectl can-i to see if you can perform an action

Adds `kubectl auth can-i <verb> <resource> [<name>]` so that a user can see if they are allowed to perform an action.

@kubernetes/sig-cli-pr-reviews @fabianofranz 

This particular command satisfies the immediate need of knowing if you can perform an action without trying that action.  When using RBAC in a script that is adding permissions, there is a lag between adding the permission and the permission being realized in the RBAC cache.  As a user on the CLI, you almost never see it, but as a script adding a binding and then using that new power, you hit it quite often.

There are natural follow-ons to the same area (hence the `auth` subcommand) to figure out if someone else can perform an action, what actions you can perform in total, and who can perform a given action.  Someone else is an API we have already, what-can-i-do was a proposed API a while back and a very useful one for interfaces, and who-can is common question if someone is administering a namespace.
This commit is contained in:
Kubernetes Submit Queue 2017-02-26 10:22:54 -08:00 committed by GitHub
commit a1490926d6
14 changed files with 470 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -510,6 +510,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

View File

@ -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",
@ -243,6 +244,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",

View File

@ -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",
],
)

View File

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

View File

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

View File

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

View File

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

View File

@ -261,16 +261,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) {