cani.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. /*
  2. Copyright 2017 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package auth
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "io"
  19. "io/ioutil"
  20. "os"
  21. "sort"
  22. "strings"
  23. "github.com/spf13/cobra"
  24. authorizationv1 "k8s.io/api/authorization/v1"
  25. rbacv1 "k8s.io/api/rbac/v1"
  26. "k8s.io/apimachinery/pkg/api/meta"
  27. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  28. "k8s.io/apimachinery/pkg/runtime/schema"
  29. utilerrors "k8s.io/apimachinery/pkg/util/errors"
  30. "k8s.io/cli-runtime/pkg/genericclioptions"
  31. "k8s.io/cli-runtime/pkg/printers"
  32. discovery "k8s.io/client-go/discovery"
  33. authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
  34. cmdutil "k8s.io/kubectl/pkg/cmd/util"
  35. describeutil "k8s.io/kubectl/pkg/describe/versioned"
  36. rbacutil "k8s.io/kubectl/pkg/util/rbac"
  37. "k8s.io/kubectl/pkg/util/templates"
  38. )
  39. // CanIOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of
  40. // referencing the cmd.Flags()
  41. type CanIOptions struct {
  42. AllNamespaces bool
  43. Quiet bool
  44. NoHeaders bool
  45. Namespace string
  46. AuthClient authorizationv1client.AuthorizationV1Interface
  47. DiscoveryClient discovery.DiscoveryInterface
  48. Verb string
  49. Resource schema.GroupVersionResource
  50. NonResourceURL string
  51. Subresource string
  52. ResourceName string
  53. List bool
  54. genericclioptions.IOStreams
  55. }
  56. var (
  57. canILong = templates.LongDesc(`
  58. Check whether an action is allowed.
  59. VERB is a logical Kubernetes API verb like 'get', 'list', 'watch', 'delete', etc.
  60. TYPE is a Kubernetes resource. Shortcuts and groups will be resolved.
  61. NONRESOURCEURL is a partial URL starts with "/".
  62. NAME is the name of a particular Kubernetes resource.`)
  63. canIExample = templates.Examples(`
  64. # Check to see if I can create pods in any namespace
  65. kubectl auth can-i create pods --all-namespaces
  66. # Check to see if I can list deployments in my current namespace
  67. kubectl auth can-i list deployments.apps
  68. # Check to see if I can do everything in my current namespace ("*" means all)
  69. kubectl auth can-i '*' '*'
  70. # Check to see if I can get the job named "bar" in namespace "foo"
  71. kubectl auth can-i list jobs.batch/bar -n foo
  72. # Check to see if I can read pod logs
  73. kubectl auth can-i get pods --subresource=log
  74. # Check to see if I can access the URL /logs/
  75. kubectl auth can-i get /logs/
  76. # List all allowed actions in namespace "foo"
  77. kubectl auth can-i --list --namespace=foo`)
  78. )
  79. // NewCmdCanI returns an initialized Command for 'auth can-i' sub command
  80. func NewCmdCanI(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
  81. o := &CanIOptions{
  82. IOStreams: streams,
  83. }
  84. cmd := &cobra.Command{
  85. Use: "can-i VERB [TYPE | TYPE/NAME | NONRESOURCEURL]",
  86. DisableFlagsInUseLine: true,
  87. Short: "Check whether an action is allowed",
  88. Long: canILong,
  89. Example: canIExample,
  90. Run: func(cmd *cobra.Command, args []string) {
  91. cmdutil.CheckErr(o.Complete(f, args))
  92. cmdutil.CheckErr(o.Validate())
  93. var err error
  94. if o.List {
  95. err = o.RunAccessList()
  96. } else {
  97. var allowed bool
  98. allowed, err = o.RunAccessCheck()
  99. if err == nil {
  100. if !allowed {
  101. os.Exit(1)
  102. }
  103. }
  104. }
  105. cmdutil.CheckErr(err)
  106. },
  107. }
  108. cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If true, check the specified action in all namespaces.")
  109. cmd.Flags().BoolVarP(&o.Quiet, "quiet", "q", o.Quiet, "If true, suppress output and just return the exit code.")
  110. cmd.Flags().StringVar(&o.Subresource, "subresource", o.Subresource, "SubResource such as pod/log or deployment/scale")
  111. cmd.Flags().BoolVar(&o.List, "list", o.List, "If true, prints all allowed actions.")
  112. cmd.Flags().BoolVar(&o.NoHeaders, "no-headers", o.NoHeaders, "If true, prints allowed actions without headers")
  113. return cmd
  114. }
  115. // Complete completes all the required options
  116. func (o *CanIOptions) Complete(f cmdutil.Factory, args []string) error {
  117. if o.List {
  118. if len(args) != 0 {
  119. return errors.New("list option must be specified with no arguments")
  120. }
  121. } else {
  122. if o.Quiet {
  123. o.Out = ioutil.Discard
  124. }
  125. switch len(args) {
  126. case 2:
  127. o.Verb = args[0]
  128. if strings.HasPrefix(args[1], "/") {
  129. o.NonResourceURL = args[1]
  130. break
  131. }
  132. resourceTokens := strings.SplitN(args[1], "/", 2)
  133. restMapper, err := f.ToRESTMapper()
  134. if err != nil {
  135. return err
  136. }
  137. o.Resource = o.resourceFor(restMapper, resourceTokens[0])
  138. if len(resourceTokens) > 1 {
  139. o.ResourceName = resourceTokens[1]
  140. }
  141. default:
  142. return errors.New("you must specify two or three arguments: verb, resource, and optional resourceName")
  143. }
  144. }
  145. var err error
  146. client, err := f.KubernetesClientSet()
  147. if err != nil {
  148. return err
  149. }
  150. o.AuthClient = client.AuthorizationV1()
  151. o.DiscoveryClient = client.Discovery()
  152. o.Namespace = ""
  153. if !o.AllNamespaces {
  154. o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
  155. if err != nil {
  156. return err
  157. }
  158. }
  159. return nil
  160. }
  161. // Validate makes sure provided values for CanIOptions are valid
  162. func (o *CanIOptions) Validate() error {
  163. if o.List {
  164. if o.Quiet || o.AllNamespaces || o.Subresource != "" {
  165. return errors.New("list option can't be specified with neither quiet, all-namespaces nor subresource options")
  166. }
  167. return nil
  168. }
  169. if o.NonResourceURL != "" {
  170. if o.Subresource != "" {
  171. return fmt.Errorf("--subresource can not be used with NonResourceURL")
  172. }
  173. if o.Resource != (schema.GroupVersionResource{}) || o.ResourceName != "" {
  174. return fmt.Errorf("NonResourceURL and ResourceName can not specified together")
  175. }
  176. } else if !o.Resource.Empty() && !o.AllNamespaces && o.DiscoveryClient != nil {
  177. if namespaced, err := isNamespaced(o.Resource, o.DiscoveryClient); err == nil && !namespaced {
  178. if len(o.Resource.Group) == 0 {
  179. fmt.Fprintf(o.ErrOut, "Warning: resource '%s' is not namespace scoped\n", o.Resource.Resource)
  180. } else {
  181. fmt.Fprintf(o.ErrOut, "Warning: resource '%s' is not namespace scoped in group '%s'\n", o.Resource.Resource, o.Resource.Group)
  182. }
  183. }
  184. }
  185. if o.NoHeaders {
  186. return fmt.Errorf("--no-headers cannot be set without --list specified")
  187. }
  188. return nil
  189. }
  190. // RunAccessList lists all the access current user has
  191. func (o *CanIOptions) RunAccessList() error {
  192. sar := &authorizationv1.SelfSubjectRulesReview{
  193. Spec: authorizationv1.SelfSubjectRulesReviewSpec{
  194. Namespace: o.Namespace,
  195. },
  196. }
  197. response, err := o.AuthClient.SelfSubjectRulesReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
  198. if err != nil {
  199. return err
  200. }
  201. return o.printStatus(response.Status)
  202. }
  203. // RunAccessCheck checks if user has access to a certain resource or non resource URL
  204. func (o *CanIOptions) RunAccessCheck() (bool, error) {
  205. var sar *authorizationv1.SelfSubjectAccessReview
  206. if o.NonResourceURL == "" {
  207. sar = &authorizationv1.SelfSubjectAccessReview{
  208. Spec: authorizationv1.SelfSubjectAccessReviewSpec{
  209. ResourceAttributes: &authorizationv1.ResourceAttributes{
  210. Namespace: o.Namespace,
  211. Verb: o.Verb,
  212. Group: o.Resource.Group,
  213. Resource: o.Resource.Resource,
  214. Subresource: o.Subresource,
  215. Name: o.ResourceName,
  216. },
  217. },
  218. }
  219. } else {
  220. sar = &authorizationv1.SelfSubjectAccessReview{
  221. Spec: authorizationv1.SelfSubjectAccessReviewSpec{
  222. NonResourceAttributes: &authorizationv1.NonResourceAttributes{
  223. Verb: o.Verb,
  224. Path: o.NonResourceURL,
  225. },
  226. },
  227. }
  228. }
  229. response, err := o.AuthClient.SelfSubjectAccessReviews().Create(context.TODO(), sar, metav1.CreateOptions{})
  230. if err != nil {
  231. return false, err
  232. }
  233. if response.Status.Allowed {
  234. fmt.Fprintln(o.Out, "yes")
  235. } else {
  236. fmt.Fprint(o.Out, "no")
  237. if len(response.Status.Reason) > 0 {
  238. fmt.Fprintf(o.Out, " - %v", response.Status.Reason)
  239. }
  240. if len(response.Status.EvaluationError) > 0 {
  241. fmt.Fprintf(o.Out, " - %v", response.Status.EvaluationError)
  242. }
  243. fmt.Fprintln(o.Out)
  244. }
  245. return response.Status.Allowed, nil
  246. }
  247. func (o *CanIOptions) resourceFor(mapper meta.RESTMapper, resourceArg string) schema.GroupVersionResource {
  248. if resourceArg == "*" {
  249. return schema.GroupVersionResource{Resource: resourceArg}
  250. }
  251. fullySpecifiedGVR, groupResource := schema.ParseResourceArg(strings.ToLower(resourceArg))
  252. gvr := schema.GroupVersionResource{}
  253. if fullySpecifiedGVR != nil {
  254. gvr, _ = mapper.ResourceFor(*fullySpecifiedGVR)
  255. }
  256. if gvr.Empty() {
  257. var err error
  258. gvr, err = mapper.ResourceFor(groupResource.WithVersion(""))
  259. if err != nil {
  260. if len(groupResource.Group) == 0 {
  261. fmt.Fprintf(o.ErrOut, "Warning: the server doesn't have a resource type '%s'\n", groupResource.Resource)
  262. } else {
  263. fmt.Fprintf(o.ErrOut, "Warning: the server doesn't have a resource type '%s' in group '%s'\n", groupResource.Resource, groupResource.Group)
  264. }
  265. return schema.GroupVersionResource{Resource: resourceArg}
  266. }
  267. }
  268. return gvr
  269. }
  270. func (o *CanIOptions) printStatus(status authorizationv1.SubjectRulesReviewStatus) error {
  271. if status.Incomplete {
  272. fmt.Fprintf(o.ErrOut, "warning: the list may be incomplete: %v\n", status.EvaluationError)
  273. }
  274. breakdownRules := []rbacv1.PolicyRule{}
  275. for _, rule := range convertToPolicyRule(status) {
  276. breakdownRules = append(breakdownRules, rbacutil.BreakdownRule(rule)...)
  277. }
  278. compactRules, err := rbacutil.CompactRules(breakdownRules)
  279. if err != nil {
  280. return err
  281. }
  282. sort.Stable(rbacutil.SortableRuleSlice(compactRules))
  283. w := printers.GetNewTabWriter(o.Out)
  284. defer w.Flush()
  285. allErrs := []error{}
  286. if !o.NoHeaders {
  287. if err := printAccessHeaders(w); err != nil {
  288. allErrs = append(allErrs, err)
  289. }
  290. }
  291. if err := printAccess(w, compactRules); err != nil {
  292. allErrs = append(allErrs, err)
  293. }
  294. return utilerrors.NewAggregate(allErrs)
  295. }
  296. func convertToPolicyRule(status authorizationv1.SubjectRulesReviewStatus) []rbacv1.PolicyRule {
  297. ret := []rbacv1.PolicyRule{}
  298. for _, resource := range status.ResourceRules {
  299. ret = append(ret, rbacv1.PolicyRule{
  300. Verbs: resource.Verbs,
  301. APIGroups: resource.APIGroups,
  302. Resources: resource.Resources,
  303. ResourceNames: resource.ResourceNames,
  304. })
  305. }
  306. for _, nonResource := range status.NonResourceRules {
  307. ret = append(ret, rbacv1.PolicyRule{
  308. Verbs: nonResource.Verbs,
  309. NonResourceURLs: nonResource.NonResourceURLs,
  310. })
  311. }
  312. return ret
  313. }
  314. func printAccessHeaders(out io.Writer) error {
  315. columnNames := []string{"Resources", "Non-Resource URLs", "Resource Names", "Verbs"}
  316. _, err := fmt.Fprintf(out, "%s\n", strings.Join(columnNames, "\t"))
  317. return err
  318. }
  319. func printAccess(out io.Writer, rules []rbacv1.PolicyRule) error {
  320. for _, r := range rules {
  321. if _, err := fmt.Fprintf(out, "%s\t%v\t%v\t%v\n", describeutil.CombineResourceGroup(r.Resources, r.APIGroups), r.NonResourceURLs, r.ResourceNames, r.Verbs); err != nil {
  322. return err
  323. }
  324. }
  325. return nil
  326. }
  327. func isNamespaced(gvr schema.GroupVersionResource, discoveryClient discovery.DiscoveryInterface) (bool, error) {
  328. if gvr.Resource == "*" {
  329. return true, nil
  330. }
  331. apiResourceList, err := discoveryClient.ServerResourcesForGroupVersion(schema.GroupVersion{
  332. Group: gvr.Group, Version: gvr.Version,
  333. }.String())
  334. if err != nil {
  335. return true, err
  336. }
  337. for _, resource := range apiResourceList.APIResources {
  338. if resource.Name == gvr.Resource {
  339. return resource.Namespaced, nil
  340. }
  341. }
  342. return false, fmt.Errorf("the server doesn't have a resource type '%s' in group '%s'", gvr.Resource, gvr.Group)
  343. }