From f14c1384387ac196e87334b5a0e05e01d7581387 Mon Sep 17 00:00:00 2001 From: xilabao Date: Fri, 14 Jul 2017 11:24:27 +0800 Subject: [PATCH] add selfsubjectrulesreview api --- cmd/kube-apiserver/app/server.go | 4 +- .../cmd/federation-apiserver/app/server.go | 2 +- pkg/apis/authorization/install/install.go | 2 +- pkg/apis/authorization/register.go | 1 + pkg/apis/authorization/types.go | 71 ++++++ .../authorization/validation/validation.go | 4 + pkg/auth/authorizer/abac/abac.go | 53 ++++- pkg/auth/authorizer/abac/abac_test.go | 202 +++++++++++++++++- .../garbagecollector/graph_builder.go | 1 + pkg/kubeapiserver/authorizer/config.go | 38 ++-- pkg/kubeapiserver/authorizer/config_test.go | 2 +- pkg/master/master.go | 2 +- .../rest/storage_authorization.go | 10 +- .../selfsubjectrulesreview/rest.go | 99 +++++++++ plugin/pkg/auth/authorizer/rbac/rbac.go | 30 +++ .../k8s.io/api/authorization/v1/register.go | 1 + .../src/k8s.io/api/authorization/v1/types.go | 79 +++++++ .../api/authorization/v1beta1/register.go | 1 + .../k8s.io/api/authorization/v1beta1/types.go | 79 +++++++ .../authorization/authorizer/interfaces.go | 6 + .../pkg/authorization/authorizer/rule.go | 73 +++++++ .../authorizerfactory/builtin.go | 24 ++- .../pkg/authorization/union/union.go | 38 ++++ .../pkg/authorization/union/union_test.go | 142 ++++++++++++ .../src/k8s.io/apiserver/pkg/server/config.go | 3 + .../plugin/pkg/authorizer/webhook/webhook.go | 11 + test/integration/auth/node_test.go | 2 +- .../etcd/etcd_storage_path_test.go | 4 + 28 files changed, 951 insertions(+), 33 deletions(-) create mode 100644 pkg/registry/authorization/selfsubjectrulesreview/rest.go create mode 100644 staging/src/k8s.io/apiserver/pkg/authorization/authorizer/rule.go diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index df00872b9b1..837799fd2f8 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -441,7 +441,7 @@ func BuildGenericConfig(s *options.ServerRunOptions) (*genericapiserver.Config, return nil, nil, nil, nil, nil, fmt.Errorf("invalid authentication config: %v", err) } - genericConfig.Authorizer, err = BuildAuthorizer(s, sharedInformers) + genericConfig.Authorizer, genericConfig.RuleResolver, err = BuildAuthorizer(s, sharedInformers) if err != nil { return nil, nil, nil, nil, nil, fmt.Errorf("invalid authorization config: %v", err) } @@ -542,7 +542,7 @@ func BuildAuthenticator(s *options.ServerRunOptions, storageFactory serverstorag } // BuildAuthorizer constructs the authorizer -func BuildAuthorizer(s *options.ServerRunOptions, sharedInformers informers.SharedInformerFactory) (authorizer.Authorizer, error) { +func BuildAuthorizer(s *options.ServerRunOptions, sharedInformers informers.SharedInformerFactory) (authorizer.Authorizer, authorizer.RuleResolver, error) { authorizationConfig := s.Authorization.ToAuthorizationConfig(sharedInformers) return authorizationConfig.New() } diff --git a/federation/cmd/federation-apiserver/app/server.go b/federation/cmd/federation-apiserver/app/server.go index 9f25c9a8bcd..47f3054cc55 100644 --- a/federation/cmd/federation-apiserver/app/server.go +++ b/federation/cmd/federation-apiserver/app/server.go @@ -190,7 +190,7 @@ func NonBlockingRun(s *options.ServerRunOptions, stopCh <-chan struct{}) error { sharedInformers := informers.NewSharedInformerFactory(client, 10*time.Minute) authorizationConfig := s.Authorization.ToAuthorizationConfig(sharedInformers) - apiAuthorizer, err := authorizationConfig.New() + apiAuthorizer, _, err := authorizationConfig.New() if err != nil { return fmt.Errorf("invalid Authorization Config: %v", err) } diff --git a/pkg/apis/authorization/install/install.go b/pkg/apis/authorization/install/install.go index bb9d882cd9f..47b78fe5ec7 100644 --- a/pkg/apis/authorization/install/install.go +++ b/pkg/apis/authorization/install/install.go @@ -39,7 +39,7 @@ func Install(groupFactoryRegistry announced.APIGroupFactoryRegistry, registry *r &announced.GroupMetaFactoryArgs{ GroupName: authorization.GroupName, VersionPreferenceOrder: []string{v1.SchemeGroupVersion.Version, v1beta1.SchemeGroupVersion.Version}, - RootScopedKinds: sets.NewString("SubjectAccessReview", "SelfSubjectAccessReview"), + RootScopedKinds: sets.NewString("SubjectAccessReview", "SelfSubjectAccessReview", "SelfSubjectRulesReview"), AddInternalObjectsToScheme: authorization.AddToScheme, }, announced.VersionToSchemeFunc{ diff --git a/pkg/apis/authorization/register.go b/pkg/apis/authorization/register.go index 5693885e4e7..7ebf0322a53 100644 --- a/pkg/apis/authorization/register.go +++ b/pkg/apis/authorization/register.go @@ -44,6 +44,7 @@ var ( func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, + &SelfSubjectRulesReview{}, &SelfSubjectAccessReview{}, &SubjectAccessReview{}, &LocalSubjectAccessReview{}, diff --git a/pkg/apis/authorization/types.go b/pkg/apis/authorization/types.go index cf69c260df3..4920913c59e 100644 --- a/pkg/apis/authorization/types.go +++ b/pkg/apis/authorization/types.go @@ -149,3 +149,74 @@ type SubjectAccessReviewStatus struct { // For instance, RBAC can be missing a role, but enough roles are still present and bound to reason about the request. EvaluationError string } + +// +genclient +// +genclient:nonNamespaced +// +genclient:noVerbs +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SelfSubjectRulesReview enumerates the set of actions the current user can perform within a namespace. +// The returned list of actions may be incomplete depending on the server's authorization mode, +// and any errors experienced during the evaluation. SelfSubjectRulesReview should be used by UIs to show/hide actions, +// or to quickly let an end user reason about their permissions. It should NOT Be used by external systems to +// drive authorization decisions as this raises confused deputy, cache lifetime/revocation, and correctness concerns. +// SubjectAccessReview, and LocalAccessReview are the correct way to defer authorization decisions to the API server. +type SelfSubjectRulesReview struct { + metav1.TypeMeta + metav1.ObjectMeta + + // Spec holds information about the request being evaluated. + Spec SelfSubjectRulesReviewSpec + + // Status is filled in by the server and indicates the set of actions a user can perform. + Status SubjectRulesReviewStatus +} + +type SelfSubjectRulesReviewSpec struct { + // Namespace to evaluate rules for. Required. + Namespace string +} + +// SubjectRulesReviewStatus contains the result of a rules check. This check can be incomplete depending on +// the set of authorizers the server is configured with and any errors experienced during evaluation. +// Because authorization rules are additive, if a rule appears in a list it's safe to assume the subject has that permission, +// even if that list is incomplete. +type SubjectRulesReviewStatus struct { + // ResourceRules is the list of actions the subject is allowed to perform on resources. + // The list ordering isn't significant, may contain duplicates, and possibly be incomplete. + ResourceRules []ResourceRule + // NonResourceRules is the list of actions the subject is allowed to perform on non-resources. + // The list ordering isn't significant, may contain duplicates, and possibly be incomplete. + NonResourceRules []NonResourceRule + // Incomplete is true when the rules returned by this call are incomplete. This is most commonly + // encountered when an authorizer, such as an external authorizer, doesn't support rules evaluation. + Incomplete bool + // EvaluationError can appear in combination with Rules. It indicates an error occurred during + // rule evaluation, such as an authorizer that doesn't support rule evaluation, and that + // ResourceRules and/or NonResourceRules may be incomplete. + EvaluationError string +} + +// ResourceRule is the list of actions the subject is allowed to perform on resources. The list ordering isn't significant, +// may contain duplicates, and possibly be incomplete. +type ResourceRule struct { + // Verb is a list of kubernetes resource API verbs, like: get, list, watch, create, update, delete, proxy. "*" means all. + Verbs []string + // APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of + // the enumerated resources in any API group will be allowed. "*" means all. + APIGroups []string + // Resources is a list of resources this rule applies to. ResourceAll represents all resources. "*" means all. + Resources []string + // ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. "*" means all. + ResourceNames []string +} + +// NonResourceRule holds information that describes a rule for the non-resource +type NonResourceRule struct { + // Verb is a list of kubernetes non-resource API verbs, like: get, post, put, delete, patch, head, options. "*" means all. + Verbs []string + + // NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, + // final step in the path. "*" means all. + NonResourceURLs []string +} diff --git a/pkg/apis/authorization/validation/validation.go b/pkg/apis/authorization/validation/validation.go index 725e6f0b2f0..512c383078c 100644 --- a/pkg/apis/authorization/validation/validation.go +++ b/pkg/apis/authorization/validation/validation.go @@ -50,6 +50,10 @@ func ValidateSelfSubjectAccessReviewSpec(spec authorizationapi.SelfSubjectAccess return allErrs } +func ValidateSelfSubjectRulesReview(review *authorizationapi.SelfSubjectRulesReview) field.ErrorList { + return field.ErrorList{} +} + func ValidateSubjectAccessReview(sar *authorizationapi.SubjectAccessReview) field.ErrorList { allErrs := ValidateSubjectAccessReviewSpec(sar.Spec, field.NewPath("spec")) if !apiequality.Semantic.DeepEqual(metav1.ObjectMeta{}, sar.ObjectMeta) { diff --git a/pkg/auth/authorizer/abac/abac.go b/pkg/auth/authorizer/abac/abac.go index 70667e50375..5e56c19ba9c 100644 --- a/pkg/auth/authorizer/abac/abac.go +++ b/pkg/auth/authorizer/abac/abac.go @@ -28,6 +28,7 @@ import ( "github.com/golang/glog" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" api "k8s.io/kubernetes/pkg/apis/abac" _ "k8s.io/kubernetes/pkg/apis/abac/latest" @@ -114,7 +115,7 @@ func NewFromFile(path string) (policyList, error) { } func matches(p api.Policy, a authorizer.Attributes) bool { - if subjectMatches(p, a) { + if subjectMatches(p, a.GetUser()) { if verbMatches(p, a) { // Resource and non-resource requests are mutually exclusive, at most one will match a policy if resourceMatches(p, a) { @@ -129,15 +130,14 @@ func matches(p api.Policy, a authorizer.Attributes) bool { } // subjectMatches returns true if specified user and group properties in the policy match the attributes -func subjectMatches(p api.Policy, a authorizer.Attributes) bool { +func subjectMatches(p api.Policy, user user.Info) bool { matched := false - username := "" - groups := []string{} - if user := a.GetUser(); user != nil { - username = user.GetName() - groups = user.GetGroups() + if user == nil { + return false } + username := user.GetName() + groups := user.GetGroups() // If the policy specified a user, ensure it matches if len(p.Spec.User) > 0 { @@ -232,3 +232,42 @@ func (pl policyList) Authorize(a authorizer.Attributes) (bool, string, error) { // policy file, compared to other steps such as encoding/decoding. // Then, add Caching only if needed. } + +func (pl policyList) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + var ( + resourceRules []authorizer.ResourceRuleInfo + nonResourceRules []authorizer.NonResourceRuleInfo + ) + + for _, p := range pl { + if subjectMatches(*p, user) { + if p.Spec.Namespace == "*" || p.Spec.Namespace == namespace { + if len(p.Spec.Resource) > 0 { + r := authorizer.DefaultResourceRuleInfo{ + Verbs: getVerbs(p.Spec.Readonly), + APIGroups: []string{p.Spec.APIGroup}, + Resources: []string{p.Spec.Resource}, + } + var resourceRule authorizer.ResourceRuleInfo = &r + resourceRules = append(resourceRules, resourceRule) + } + if len(p.Spec.NonResourcePath) > 0 { + r := authorizer.DefaultNonResourceRuleInfo{ + Verbs: getVerbs(p.Spec.Readonly), + NonResourceURLs: []string{p.Spec.NonResourcePath}, + } + var nonResourceRule authorizer.NonResourceRuleInfo = &r + nonResourceRules = append(nonResourceRules, nonResourceRule) + } + } + } + } + return resourceRules, nonResourceRules, false, nil +} + +func getVerbs(isReadOnly bool) []string { + if isReadOnly { + return []string{"get", "list", "watch"} + } + return []string{"*"} +} diff --git a/pkg/auth/authorizer/abac/abac_test.go b/pkg/auth/authorizer/abac/abac_test.go index 51e27363871..b8df86bb806 100644 --- a/pkg/auth/authorizer/abac/abac_test.go +++ b/pkg/auth/authorizer/abac/abac_test.go @@ -19,6 +19,7 @@ package abac import ( "io/ioutil" "os" + "reflect" "testing" "k8s.io/apimachinery/pkg/runtime" @@ -141,6 +142,203 @@ func TestAuthorizeV0(t *testing.T) { } } +func getResourceRules(infos []authorizer.ResourceRuleInfo) []authorizer.DefaultResourceRuleInfo { + rules := make([]authorizer.DefaultResourceRuleInfo, len(infos)) + for i, info := range infos { + rules[i] = authorizer.DefaultResourceRuleInfo{ + Verbs: info.GetVerbs(), + APIGroups: info.GetAPIGroups(), + Resources: info.GetResources(), + ResourceNames: info.GetResourceNames(), + } + } + return rules +} + +func getNonResourceRules(infos []authorizer.NonResourceRuleInfo) []authorizer.DefaultNonResourceRuleInfo { + rules := make([]authorizer.DefaultNonResourceRuleInfo, len(infos)) + for i, info := range infos { + rules[i] = authorizer.DefaultNonResourceRuleInfo{ + Verbs: info.GetVerbs(), + NonResourceURLs: info.GetNonResourceURLs(), + } + } + return rules +} + +func TestRulesFor(t *testing.T) { + a, err := newWithContents(t, ` +{ "readonly": true, "resource": "events" } +{"user":"scheduler", "readonly": true, "resource": "pods" } +{"user":"scheduler", "resource": "bindings" } +{"user":"kubelet", "readonly": true, "resource": "pods" } +{"user":"kubelet", "resource": "events" } +{"user":"alice", "namespace": "projectCaribou"} +{"user":"bob", "readonly": true, "namespace": "projectCaribou"} +{"user":"bob", "readonly": true, "nonResourcePath": "*"} +{"group":"a", "resource": "bindings" } +{"group":"b", "readonly": true, "nonResourcePath": "*"} +`) + if err != nil { + t.Fatalf("unable to read policy file: %v", err) + } + + authenticatedGroup := []string{user.AllAuthenticated} + + uScheduler := user.DefaultInfo{Name: "scheduler", UID: "uid1", Groups: authenticatedGroup} + uKubelet := user.DefaultInfo{Name: "kubelet", UID: "uid2", Groups: []string{"a", "b"}} + uAlice := user.DefaultInfo{Name: "alice", UID: "uid3", Groups: authenticatedGroup} + uBob := user.DefaultInfo{Name: "bob", UID: "uid4", Groups: authenticatedGroup} + uChuck := user.DefaultInfo{Name: "chuck", UID: "uid5", Groups: []string{"a", "b"}} + + testCases := []struct { + User user.DefaultInfo + Namespace string + ExpectResourceRules []authorizer.DefaultResourceRuleInfo + ExpectNonResourceRules []authorizer.DefaultNonResourceRuleInfo + }{ + { + User: uScheduler, + Namespace: "ns1", + ExpectResourceRules: []authorizer.DefaultResourceRuleInfo{ + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"events"}, + }, + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"pods"}, + }, + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"bindings"}, + }, + }, + ExpectNonResourceRules: []authorizer.DefaultNonResourceRuleInfo{}, + }, + { + User: uKubelet, + Namespace: "ns1", + ExpectResourceRules: []authorizer.DefaultResourceRuleInfo{ + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"pods"}, + }, + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"events"}, + }, + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"bindings"}, + }, + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + }, + ExpectNonResourceRules: []authorizer.DefaultNonResourceRuleInfo{ + { + Verbs: []string{"get", "list", "watch"}, + NonResourceURLs: []string{"*"}, + }, + }, + }, + { + User: uAlice, + Namespace: "projectCaribou", + ExpectResourceRules: []authorizer.DefaultResourceRuleInfo{ + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"events"}, + }, + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + }, + ExpectNonResourceRules: []authorizer.DefaultNonResourceRuleInfo{}, + }, + { + User: uBob, + Namespace: "projectCaribou", + ExpectResourceRules: []authorizer.DefaultResourceRuleInfo{ + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"events"}, + }, + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + }, + ExpectNonResourceRules: []authorizer.DefaultNonResourceRuleInfo{ + { + Verbs: []string{"get", "list", "watch"}, + NonResourceURLs: []string{"*"}, + }, + }, + }, + { + User: uChuck, + Namespace: "ns1", + ExpectResourceRules: []authorizer.DefaultResourceRuleInfo{ + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"bindings"}, + }, + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + }, + ExpectNonResourceRules: []authorizer.DefaultNonResourceRuleInfo{ + { + Verbs: []string{"get", "list", "watch"}, + NonResourceURLs: []string{"*"}, + }, + }, + }, + } + for i, tc := range testCases { + attr := authorizer.AttributesRecord{ + User: &tc.User, + Namespace: tc.Namespace, + } + resourceRules, nonResourceRules, _, _ := a.RulesFor(attr.GetUser(), attr.GetNamespace()) + actualResourceRules := getResourceRules(resourceRules) + if !reflect.DeepEqual(tc.ExpectResourceRules, actualResourceRules) { + t.Logf("tc: %v -> attr %v", tc, attr) + t.Errorf("%d: Expected: \n%#v\n but actual: \n%#v\n", + i, tc.ExpectResourceRules, actualResourceRules) + } + actualNonResourceRules := getNonResourceRules(nonResourceRules) + if !reflect.DeepEqual(tc.ExpectNonResourceRules, actualNonResourceRules) { + t.Logf("tc: %v -> attr %v", tc, attr) + t.Errorf("%d: Expected: \n%#v\n but actual: \n%#v\n", + i, tc.ExpectNonResourceRules, actualNonResourceRules) + } + } +} + func TestAuthorizeV1beta1(t *testing.T) { a, err := newWithContents(t, ` @@ -609,7 +807,7 @@ func TestSubjectMatches(t *testing.T) { attr := authorizer.AttributesRecord{ User: &tc.User, } - actualMatch := subjectMatches(*policy, attr) + actualMatch := subjectMatches(*policy, attr.GetUser()) if tc.ExpectMatch != actualMatch { t.Errorf("%v: Expected actorMatches=%v but actually got=%v", k, tc.ExpectMatch, actualMatch) @@ -617,7 +815,7 @@ func TestSubjectMatches(t *testing.T) { } } -func newWithContents(t *testing.T, contents string) (authorizer.Authorizer, error) { +func newWithContents(t *testing.T, contents string) (policyList, error) { f, err := ioutil.TempFile("", "abac_test") if err != nil { t.Fatalf("unexpected error creating policyfile: %v", err) diff --git a/pkg/controller/garbagecollector/graph_builder.go b/pkg/controller/garbagecollector/graph_builder.go index 2cc7cb4daff..b4199222b41 100644 --- a/pkg/controller/garbagecollector/graph_builder.go +++ b/pkg/controller/garbagecollector/graph_builder.go @@ -359,6 +359,7 @@ var ignoredResources = map[schema.GroupResource]struct{}{ {Group: "authorization.k8s.io", Resource: "subjectaccessreviews"}: {}, {Group: "authorization.k8s.io", Resource: "selfsubjectaccessreviews"}: {}, {Group: "authorization.k8s.io", Resource: "localsubjectaccessreviews"}: {}, + {Group: "authorization.k8s.io", Resource: "selfsubjectrulesreviews"}: {}, {Group: "apiregistration.k8s.io", Resource: "apiservices"}: {}, {Group: "apiextensions.k8s.io", Resource: "customresourcedefinitions"}: {}, } diff --git a/pkg/kubeapiserver/authorizer/config.go b/pkg/kubeapiserver/authorizer/config.go index 1466d2b4114..659f2ae7a05 100644 --- a/pkg/kubeapiserver/authorizer/config.go +++ b/pkg/kubeapiserver/authorizer/config.go @@ -56,17 +56,20 @@ type AuthorizationConfig struct { // New returns the right sort of union of multiple authorizer.Authorizer objects // based on the authorizationMode or an error. -func (config AuthorizationConfig) New() (authorizer.Authorizer, error) { +func (config AuthorizationConfig) New() (authorizer.Authorizer, authorizer.RuleResolver, error) { if len(config.AuthorizationModes) == 0 { - return nil, errors.New("At least one authorization mode should be passed") + return nil, nil, errors.New("At least one authorization mode should be passed") } - var authorizers []authorizer.Authorizer + var ( + authorizers []authorizer.Authorizer + ruleResolvers []authorizer.RuleResolver + ) authorizerMap := make(map[string]bool) for _, authorizationMode := range config.AuthorizationModes { if authorizerMap[authorizationMode] { - return nil, fmt.Errorf("Authorization mode %s specified more than once", authorizationMode) + return nil, nil, fmt.Errorf("Authorization mode %s specified more than once", authorizationMode) } // Keep cases in sync with constant list above. switch authorizationMode { @@ -81,29 +84,35 @@ func (config AuthorizationConfig) New() (authorizer.Authorizer, error) { authorizers = append(authorizers, nodeAuthorizer) case modes.ModeAlwaysAllow: - authorizers = append(authorizers, authorizerfactory.NewAlwaysAllowAuthorizer()) + alwaysAllowAuthorizer := authorizerfactory.NewAlwaysAllowAuthorizer() + authorizers = append(authorizers, alwaysAllowAuthorizer) + ruleResolvers = append(ruleResolvers, alwaysAllowAuthorizer) case modes.ModeAlwaysDeny: - authorizers = append(authorizers, authorizerfactory.NewAlwaysDenyAuthorizer()) + alwaysDenyAuthorizer := authorizerfactory.NewAlwaysDenyAuthorizer() + authorizers = append(authorizers, alwaysDenyAuthorizer) + ruleResolvers = append(ruleResolvers, alwaysDenyAuthorizer) case modes.ModeABAC: if config.PolicyFile == "" { - return nil, errors.New("ABAC's authorization policy file not passed") + return nil, nil, errors.New("ABAC's authorization policy file not passed") } abacAuthorizer, err := abac.NewFromFile(config.PolicyFile) if err != nil { - return nil, err + return nil, nil, err } authorizers = append(authorizers, abacAuthorizer) + ruleResolvers = append(ruleResolvers, abacAuthorizer) case modes.ModeWebhook: if config.WebhookConfigFile == "" { - return nil, errors.New("Webhook's configuration file not passed") + return nil, nil, errors.New("Webhook's configuration file not passed") } webhookAuthorizer, err := webhook.New(config.WebhookConfigFile, config.WebhookCacheAuthorizedTTL, config.WebhookCacheUnauthorizedTTL) if err != nil { - return nil, err + return nil, nil, err } authorizers = append(authorizers, webhookAuthorizer) + ruleResolvers = append(ruleResolvers, webhookAuthorizer) case modes.ModeRBAC: rbacAuthorizer := rbac.New( &rbac.RoleGetter{Lister: config.InformerFactory.Rbac().InternalVersion().Roles().Lister()}, @@ -112,18 +121,19 @@ func (config AuthorizationConfig) New() (authorizer.Authorizer, error) { &rbac.ClusterRoleBindingLister{Lister: config.InformerFactory.Rbac().InternalVersion().ClusterRoleBindings().Lister()}, ) authorizers = append(authorizers, rbacAuthorizer) + ruleResolvers = append(ruleResolvers, rbacAuthorizer) default: - return nil, fmt.Errorf("Unknown authorization mode %s specified", authorizationMode) + return nil, nil, fmt.Errorf("Unknown authorization mode %s specified", authorizationMode) } authorizerMap[authorizationMode] = true } if !authorizerMap[modes.ModeABAC] && config.PolicyFile != "" { - return nil, errors.New("Cannot specify --authorization-policy-file without mode ABAC") + return nil, nil, errors.New("Cannot specify --authorization-policy-file without mode ABAC") } if !authorizerMap[modes.ModeWebhook] && config.WebhookConfigFile != "" { - return nil, errors.New("Cannot specify --authorization-webhook-config-file without mode Webhook") + return nil, nil, errors.New("Cannot specify --authorization-webhook-config-file without mode Webhook") } - return union.New(authorizers...), nil + return union.New(authorizers...), union.NewRuleResolvers(ruleResolvers...), nil } diff --git a/pkg/kubeapiserver/authorizer/config_test.go b/pkg/kubeapiserver/authorizer/config_test.go index ac2d5e983fd..42cee988900 100644 --- a/pkg/kubeapiserver/authorizer/config_test.go +++ b/pkg/kubeapiserver/authorizer/config_test.go @@ -91,7 +91,7 @@ func TestNew(t *testing.T) { } for _, tt := range tests { - _, err := tt.config.New() + _, _, err := tt.config.New() if tt.wantErr && (err == nil) { t.Errorf("New %s", tt.msg) } else if !tt.wantErr && (err != nil) { diff --git a/pkg/master/master.go b/pkg/master/master.go index d6f7130a076..a2b3d591eed 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -256,7 +256,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) // handlers that we have. restStorageProviders := []RESTStorageProvider{ authenticationrest.RESTStorageProvider{Authenticator: c.GenericConfig.Authenticator}, - authorizationrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorizer}, + authorizationrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorizer, RuleResolver: c.GenericConfig.RuleResolver}, autoscalingrest.RESTStorageProvider{}, batchrest.RESTStorageProvider{}, certificatesrest.RESTStorageProvider{}, diff --git a/pkg/registry/authorization/rest/storage_authorization.go b/pkg/registry/authorization/rest/storage_authorization.go index 4056516e3bc..19eeb2fd254 100644 --- a/pkg/registry/authorization/rest/storage_authorization.go +++ b/pkg/registry/authorization/rest/storage_authorization.go @@ -28,11 +28,13 @@ import ( "k8s.io/kubernetes/pkg/apis/authorization" "k8s.io/kubernetes/pkg/registry/authorization/localsubjectaccessreview" "k8s.io/kubernetes/pkg/registry/authorization/selfsubjectaccessreview" + "k8s.io/kubernetes/pkg/registry/authorization/selfsubjectrulesreview" "k8s.io/kubernetes/pkg/registry/authorization/subjectaccessreview" ) type RESTStorageProvider struct { - Authorizer authorizer.Authorizer + Authorizer authorizer.Authorizer + RuleResolver authorizer.RuleResolver } func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, bool) { @@ -70,6 +72,9 @@ func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorag if apiResourceConfigSource.ResourceEnabled(version.WithResource("localsubjectaccessreviews")) { storage["localsubjectaccessreviews"] = localsubjectaccessreview.NewREST(p.Authorizer) } + if apiResourceConfigSource.ResourceEnabled(version.WithResource("selfsubjectrulesreviews")) { + storage["selfsubjectrulesreviews"] = selfsubjectrulesreview.NewREST(p.RuleResolver) + } return storage } @@ -87,6 +92,9 @@ func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.API if apiResourceConfigSource.ResourceEnabled(version.WithResource("localsubjectaccessreviews")) { storage["localsubjectaccessreviews"] = localsubjectaccessreview.NewREST(p.Authorizer) } + if apiResourceConfigSource.ResourceEnabled(version.WithResource("selfsubjectrulesreviews")) { + storage["selfsubjectrulesreviews"] = selfsubjectrulesreview.NewREST(p.RuleResolver) + } return storage } diff --git a/pkg/registry/authorization/selfsubjectrulesreview/rest.go b/pkg/registry/authorization/selfsubjectrulesreview/rest.go new file mode 100644 index 00000000000..062829faba5 --- /dev/null +++ b/pkg/registry/authorization/selfsubjectrulesreview/rest.go @@ -0,0 +1,99 @@ +/* +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 selfsubjectrulesreview + +import ( + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authorization/authorizer" + genericapirequest "k8s.io/apiserver/pkg/endpoints/request" + authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" +) + +// REST implements a RESTStorage for selfsubjectrulesreview. +type REST struct { + ruleResolver authorizer.RuleResolver +} + +// NewREST returns a RESTStorage object that will work against selfsubjectrulesreview. +func NewREST(ruleResolver authorizer.RuleResolver) *REST { + return &REST{ruleResolver} +} + +// New creates a new selfsubjectrulesreview object. +func (r *REST) New() runtime.Object { + return &authorizationapi.SelfSubjectRulesReview{} +} + +// Create attempts to get self subject rules in specific namespace. +func (r *REST) Create(ctx genericapirequest.Context, obj runtime.Object, includeUninitialized bool) (runtime.Object, error) { + selfSRR, ok := obj.(*authorizationapi.SelfSubjectRulesReview) + if !ok { + return nil, apierrors.NewBadRequest(fmt.Sprintf("not a SelfSubjectRulesReview: %#v", obj)) + } + + user, ok := genericapirequest.UserFrom(ctx) + if !ok { + return nil, apierrors.NewBadRequest("no user present on request") + } + + namespace := selfSRR.Spec.Namespace + if namespace == "" { + return nil, apierrors.NewBadRequest("no namespace on request") + } + resourceInfo, nonResourceInfo, incomplete, err := r.ruleResolver.RulesFor(user, namespace) + + ret := &authorizationapi.SelfSubjectRulesReview{ + Status: authorizationapi.SubjectRulesReviewStatus{ + ResourceRules: getResourceRules(resourceInfo), + NonResourceRules: getNonResourceRules(nonResourceInfo), + Incomplete: incomplete, + }, + } + + if err != nil { + ret.Status.EvaluationError = err.Error() + } + + return ret, nil +} + +func getResourceRules(infos []authorizer.ResourceRuleInfo) []authorizationapi.ResourceRule { + rules := make([]authorizationapi.ResourceRule, len(infos)) + for i, info := range infos { + rules[i] = authorizationapi.ResourceRule{ + Verbs: info.GetVerbs(), + APIGroups: info.GetAPIGroups(), + Resources: info.GetResources(), + ResourceNames: info.GetResourceNames(), + } + } + return rules +} + +func getNonResourceRules(infos []authorizer.NonResourceRuleInfo) []authorizationapi.NonResourceRule { + rules := make([]authorizationapi.NonResourceRule, len(infos)) + for i, info := range infos { + rules[i] = authorizationapi.NonResourceRule{ + Verbs: info.GetVerbs(), + NonResourceURLs: info.GetNonResourceURLs(), + } + } + return rules +} diff --git a/plugin/pkg/auth/authorizer/rbac/rbac.go b/plugin/pkg/auth/authorizer/rbac/rbac.go index 6341936edc5..68ef7b2567f 100644 --- a/plugin/pkg/auth/authorizer/rbac/rbac.go +++ b/plugin/pkg/auth/authorizer/rbac/rbac.go @@ -123,6 +123,36 @@ func (r *RBACAuthorizer) Authorize(requestAttributes authorizer.Attributes) (boo return false, reason, nil } +func (r *RBACAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + var ( + resourceRules []authorizer.ResourceRuleInfo + nonResourceRules []authorizer.NonResourceRuleInfo + ) + + policyRules, err := r.authorizationRuleResolver.RulesFor(user, namespace) + for _, policyRule := range policyRules { + if len(policyRule.Resources) > 0 { + r := authorizer.DefaultResourceRuleInfo{ + Verbs: policyRule.Verbs, + APIGroups: policyRule.APIGroups, + Resources: policyRule.Resources, + ResourceNames: policyRule.ResourceNames, + } + var resourceRule authorizer.ResourceRuleInfo = &r + resourceRules = append(resourceRules, resourceRule) + } + if len(policyRule.NonResourceURLs) > 0 { + r := authorizer.DefaultNonResourceRuleInfo{ + Verbs: policyRule.Verbs, + NonResourceURLs: policyRule.NonResourceURLs, + } + var nonResourceRule authorizer.NonResourceRuleInfo = &r + nonResourceRules = append(nonResourceRules, nonResourceRule) + } + } + return resourceRules, nonResourceRules, false, err +} + func New(roles rbacregistryvalidation.RoleGetter, roleBindings rbacregistryvalidation.RoleBindingLister, clusterRoles rbacregistryvalidation.ClusterRoleGetter, clusterRoleBindings rbacregistryvalidation.ClusterRoleBindingLister) *RBACAuthorizer { authorizer := &RBACAuthorizer{ authorizationRuleResolver: rbacregistryvalidation.NewDefaultRuleResolver( diff --git a/staging/src/k8s.io/api/authorization/v1/register.go b/staging/src/k8s.io/api/authorization/v1/register.go index 031227e8ff4..d716eaa98da 100644 --- a/staging/src/k8s.io/api/authorization/v1/register.go +++ b/staging/src/k8s.io/api/authorization/v1/register.go @@ -44,6 +44,7 @@ var ( // Adds the list of known types to api.Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, + &SelfSubjectRulesReview{}, &SelfSubjectAccessReview{}, &SubjectAccessReview{}, &LocalSubjectAccessReview{}, diff --git a/staging/src/k8s.io/api/authorization/v1/types.go b/staging/src/k8s.io/api/authorization/v1/types.go index a68b8ca53dc..99ec3bcbf7f 100644 --- a/staging/src/k8s.io/api/authorization/v1/types.go +++ b/staging/src/k8s.io/api/authorization/v1/types.go @@ -180,3 +180,82 @@ type SubjectAccessReviewStatus struct { // +optional EvaluationError string `json:"evaluationError,omitempty" protobuf:"bytes,3,opt,name=evaluationError"` } + +// +genclient +// +genclient:nonNamespaced +// +genclient:noVerbs +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SelfSubjectRulesReview enumerates the set of actions the current user can perform within a namespace. +// The returned list of actions may be incomplete depending on the server's authorization mode, +// and any errors experienced during the evaluation. SelfSubjectRulesReview should be used by UIs to show/hide actions, +// or to quickly let an end user reason about their permissions. It should NOT Be used by external systems to +// drive authorization decisions as this raises confused deputy, cache lifetime/revocation, and correctness concerns. +// SubjectAccessReview, and LocalAccessReview are the correct way to defer authorization decisions to the API server. +type SelfSubjectRulesReview struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Spec holds information about the request being evaluated. + Spec SelfSubjectRulesReviewSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` + + // Status is filled in by the server and indicates the set of actions a user can perform. + // +optional + Status SubjectRulesReviewStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +type SelfSubjectRulesReviewSpec struct { + // Namespace to evaluate rules for. Required. + Namespace string `json:"namespace,omitempty" protobuf:"bytes,1,opt,name=namespace"` +} + +// SubjectRulesReviewStatus contains the result of a rules check. This check can be incomplete depending on +// the set of authorizers the server is configured with and any errors experienced during evaluation. +// Because authorization rules are additive, if a rule appears in a list it's safe to assume the subject has that permission, +// even if that list is incomplete. +type SubjectRulesReviewStatus struct { + // ResourceRules is the list of actions the subject is allowed to perform on resources. + // The list ordering isn't significant, may contain duplicates, and possibly be incomplete. + ResourceRules []ResourceRule `json:"resourceRules" protobuf:"bytes,1,rep,name=resourceRules"` + // NonResourceRules is the list of actions the subject is allowed to perform on non-resources. + // The list ordering isn't significant, may contain duplicates, and possibly be incomplete. + NonResourceRules []NonResourceRule `json:"nonResourceRules" protobuf:"bytes,2,rep,name=nonResourceRules"` + // Incomplete is true when the rules returned by this call are incomplete. This is most commonly + // encountered when an authorizer, such as an external authorizer, doesn't support rules evaluation. + Incomplete bool `json:"incomplete" protobuf:"bytes,3,rep,name=incomplete"` + // EvaluationError can appear in combination with Rules. It indicates an error occurred during + // rule evaluation, such as an authorizer that doesn't support rule evaluation, and that + // ResourceRules and/or NonResourceRules may be incomplete. + // +optional + EvaluationError string `json:"evaluationError,omitempty" protobuf:"bytes,4,opt,name=evaluationError"` +} + +// ResourceRule is the list of actions the subject is allowed to perform on resources. The list ordering isn't significant, +// may contain duplicates, and possibly be incomplete. +type ResourceRule struct { + // Verb is a list of kubernetes resource API verbs, like: get, list, watch, create, update, delete, proxy. "*" means all. + Verbs []string `json:"verbs" protobuf:"bytes,1,rep,name=verbs"` + + // APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of + // the enumerated resources in any API group will be allowed. "*" means all. + // +optional + APIGroups []string `json:"apiGroups,omitempty" protobuf:"bytes,2,rep,name=apiGroups"` + // Resources is a list of resources this rule applies to. ResourceAll represents all resources. "*" means all. + // +optional + Resources []string `json:"resources,omitempty" protobuf:"bytes,3,rep,name=resources"` + // ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. "*" means all. + // +optional + ResourceNames []string `json:"resourceNames,omitempty" protobuf:"bytes,4,rep,name=resourceNames"` +} + +// NonResourceRule holds information that describes a rule for the non-resource +type NonResourceRule struct { + // Verb is a list of kubernetes non-resource API verbs, like: get, post, put, delete, patch, head, options. "*" means all. + Verbs []string `json:"verbs" protobuf:"bytes,1,rep,name=verbs"` + + // NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, + // final step in the path. "*" means all. + // +optional + NonResourceURLs []string `json:"nonResourceURLs,omitempty" protobuf:"bytes,2,rep,name=nonResourceURLs"` +} diff --git a/staging/src/k8s.io/api/authorization/v1beta1/register.go b/staging/src/k8s.io/api/authorization/v1beta1/register.go index 07d4ffceb30..d8116d5a47a 100644 --- a/staging/src/k8s.io/api/authorization/v1beta1/register.go +++ b/staging/src/k8s.io/api/authorization/v1beta1/register.go @@ -44,6 +44,7 @@ var ( // Adds the list of known types to api.Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, + &SelfSubjectRulesReview{}, &SelfSubjectAccessReview{}, &SubjectAccessReview{}, &LocalSubjectAccessReview{}, diff --git a/staging/src/k8s.io/api/authorization/v1beta1/types.go b/staging/src/k8s.io/api/authorization/v1beta1/types.go index f395673766d..a0659d519c0 100644 --- a/staging/src/k8s.io/api/authorization/v1beta1/types.go +++ b/staging/src/k8s.io/api/authorization/v1beta1/types.go @@ -180,3 +180,82 @@ type SubjectAccessReviewStatus struct { // +optional EvaluationError string `json:"evaluationError,omitempty" protobuf:"bytes,3,opt,name=evaluationError"` } + +// +genclient +// +genclient:nonNamespaced +// +genclient:noVerbs +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SelfSubjectRulesReview enumerates the set of actions the current user can perform within a namespace. +// The returned list of actions may be incomplete depending on the server's authorization mode, +// and any errors experienced during the evaluation. SelfSubjectRulesReview should be used by UIs to show/hide actions, +// or to quickly let an end user reason about their permissions. It should NOT Be used by external systems to +// drive authorization decisions as this raises confused deputy, cache lifetime/revocation, and correctness concerns. +// SubjectAccessReview, and LocalAccessReview are the correct way to defer authorization decisions to the API server. +type SelfSubjectRulesReview struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Spec holds information about the request being evaluated. + Spec SelfSubjectRulesReviewSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` + + // Status is filled in by the server and indicates the set of actions a user can perform. + // +optional + Status SubjectRulesReviewStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +type SelfSubjectRulesReviewSpec struct { + // Namespace to evaluate rules for. Required. + Namespace string `json:"namespace,omitempty" protobuf:"bytes,1,opt,name=namespace"` +} + +// SubjectRulesReviewStatus contains the result of a rules check. This check can be incomplete depending on +// the set of authorizers the server is configured with and any errors experienced during evaluation. +// Because authorization rules are additive, if a rule appears in a list it's safe to assume the subject has that permission, +// even if that list is incomplete. +type SubjectRulesReviewStatus struct { + // ResourceRules is the list of actions the subject is allowed to perform on resources. + // The list ordering isn't significant, may contain duplicates, and possibly be incomplete. + ResourceRules []ResourceRule `json:"resourceRules" protobuf:"bytes,1,rep,name=resourceRules"` + // NonResourceRules is the list of actions the subject is allowed to perform on non-resources. + // The list ordering isn't significant, may contain duplicates, and possibly be incomplete. + NonResourceRules []NonResourceRule `json:"nonResourceRules" protobuf:"bytes,2,rep,name=nonResourceRules"` + // Incomplete is true when the rules returned by this call are incomplete. This is most commonly + // encountered when an authorizer, such as an external authorizer, doesn't support rules evaluation. + Incomplete bool `json:"incomplete" protobuf:"bytes,3,rep,name=incomplete"` + // EvaluationError can appear in combination with Rules. It indicates an error occurred during + // rule evaluation, such as an authorizer that doesn't support rule evaluation, and that + // ResourceRules and/or NonResourceRules may be incomplete. + // +optional + EvaluationError string `json:"evaluationError,omitempty" protobuf:"bytes,4,opt,name=evaluationError"` +} + +// ResourceRule is the list of actions the subject is allowed to perform on resources. The list ordering isn't significant, +// may contain duplicates, and possibly be incomplete. +type ResourceRule struct { + // Verb is a list of kubernetes resource API verbs, like: get, list, watch, create, update, delete, proxy. "*" means all. + Verbs []string `json:"verbs" protobuf:"bytes,1,rep,name=verbs"` + + // APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of + // the enumerated resources in any API group will be allowed. "*" means all. + // +optional + APIGroups []string `json:"apiGroups,omitempty" protobuf:"bytes,2,rep,name=apiGroups"` + // Resources is a list of resources this rule applies to. ResourceAll represents all resources. "*" means all. + // +optional + Resources []string `json:"resources,omitempty" protobuf:"bytes,3,rep,name=resources"` + // ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. "*" means all. + // +optional + ResourceNames []string `json:"resourceNames,omitempty" protobuf:"bytes,4,rep,name=resourceNames"` +} + +// NonResourceRule holds information that describes a rule for the non-resource +type NonResourceRule struct { + // Verb is a list of kubernetes non-resource API verbs, like: get, post, put, delete, patch, head, options. "*" means all. + Verbs []string `json:"verbs" protobuf:"bytes,1,rep,name=verbs"` + + // NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, + // final step in the path. "*" means all. + // +optional + NonResourceURLs []string `json:"nonResourceURLs,omitempty" protobuf:"bytes,2,rep,name=nonResourceURLs"` +} diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/interfaces.go b/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/interfaces.go index 8d2753b972e..e94da3e1a44 100644 --- a/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/interfaces.go +++ b/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/interfaces.go @@ -76,6 +76,12 @@ func (f AuthorizerFunc) Authorize(a Attributes) (bool, string, error) { return f(a) } +// RuleResolver provides a mechanism for resolving the list of rules that apply to a given user within a namespace. +type RuleResolver interface { + // RulesFor get the list of cluster wide rules, the list of rules in the specific namespace, incomplete status and errors. + RulesFor(user user.Info, namespace string) ([]ResourceRuleInfo, []NonResourceRuleInfo, bool, error) +} + // RequestAttributesGetter provides a function that extracts Attributes from an http.Request type RequestAttributesGetter interface { GetRequestAttributes(user.Info, *http.Request) Attributes diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/rule.go b/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/rule.go new file mode 100644 index 00000000000..8f7d9d9effc --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authorization/authorizer/rule.go @@ -0,0 +1,73 @@ +/* +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 authorizer + +type ResourceRuleInfo interface { + // GetVerbs returns a list of kubernetes resource API verbs. + GetVerbs() []string + // GetAPIGroups return the names of the APIGroup that contains the resources. + GetAPIGroups() []string + // GetResources return a list of resources the rule applies to. + GetResources() []string + // GetResourceNames return a white list of names that the rule applies to. + GetResourceNames() []string +} + +// DefaultResourceRuleInfo holds information that describes a rule for the resource +type DefaultResourceRuleInfo struct { + Verbs []string + APIGroups []string + Resources []string + ResourceNames []string +} + +func (i *DefaultResourceRuleInfo) GetVerbs() []string { + return i.Verbs +} + +func (i *DefaultResourceRuleInfo) GetAPIGroups() []string { + return i.APIGroups +} + +func (i *DefaultResourceRuleInfo) GetResources() []string { + return i.Resources +} + +func (i *DefaultResourceRuleInfo) GetResourceNames() []string { + return i.ResourceNames +} + +type NonResourceRuleInfo interface { + // GetVerbs returns a list of kubernetes resource API verbs. + GetVerbs() []string + // GetNonResourceURLs return a set of partial urls that a user should have access to. + GetNonResourceURLs() []string +} + +// DefaultNonResourceRuleInfo holds information that describes a rule for the non-resource +type DefaultNonResourceRuleInfo struct { + Verbs []string + NonResourceURLs []string +} + +func (i *DefaultNonResourceRuleInfo) GetVerbs() []string { + return i.Verbs +} + +func (i *DefaultNonResourceRuleInfo) GetNonResourceURLs() []string { + return i.NonResourceURLs +} diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory/builtin.go b/staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory/builtin.go index 85846619cc0..8381e83f437 100644 --- a/staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory/builtin.go +++ b/staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory/builtin.go @@ -19,6 +19,7 @@ package authorizerfactory import ( "errors" + "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" ) @@ -31,7 +32,22 @@ func (alwaysAllowAuthorizer) Authorize(a authorizer.Attributes) (authorized bool return true, "", nil } -func NewAlwaysAllowAuthorizer() authorizer.Authorizer { +func (alwaysAllowAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + return []authorizer.ResourceRuleInfo{ + &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + }, []authorizer.NonResourceRuleInfo{ + &authorizer.DefaultNonResourceRuleInfo{ + Verbs: []string{"*"}, + NonResourceURLs: []string{"*"}, + }, + }, false, nil +} + +func NewAlwaysAllowAuthorizer() *alwaysAllowAuthorizer { return new(alwaysAllowAuthorizer) } @@ -44,7 +60,11 @@ func (alwaysDenyAuthorizer) Authorize(a authorizer.Attributes) (authorized bool, return false, "Everything is forbidden.", nil } -func NewAlwaysDenyAuthorizer() authorizer.Authorizer { +func (alwaysDenyAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + return []authorizer.ResourceRuleInfo{}, []authorizer.NonResourceRuleInfo{}, false, nil +} + +func NewAlwaysDenyAuthorizer() *alwaysDenyAuthorizer { return new(alwaysDenyAuthorizer) } diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/union/union.go b/staging/src/k8s.io/apiserver/pkg/authorization/union/union.go index 9278c6fa9b5..367da59d1be 100644 --- a/staging/src/k8s.io/apiserver/pkg/authorization/union/union.go +++ b/staging/src/k8s.io/apiserver/pkg/authorization/union/union.go @@ -20,6 +20,7 @@ import ( "strings" utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" ) @@ -55,3 +56,40 @@ func (authzHandler unionAuthzHandler) Authorize(a authorizer.Attributes) (bool, return false, strings.Join(reasonlist, "\n"), utilerrors.NewAggregate(errlist) } + +// unionAuthzRulesHandler authorizer against a chain of authorizer.RuleResolver +type unionAuthzRulesHandler []authorizer.RuleResolver + +// NewRuleResolvers returns an authorizer that authorizes against a chain of authorizer.Authorizer objects +func NewRuleResolvers(authorizationHandlers ...authorizer.RuleResolver) authorizer.RuleResolver { + return unionAuthzRulesHandler(authorizationHandlers) +} + +// RulesFor against a chain of authorizer.RuleResolver objects and returns nil if successful and returns error if unsuccessful +func (authzHandler unionAuthzRulesHandler) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + var ( + errList []error + resourceRulesList []authorizer.ResourceRuleInfo + nonResourceRulesList []authorizer.NonResourceRuleInfo + ) + incompleteStatus := false + + for _, currAuthzHandler := range authzHandler { + resourceRules, nonResourceRules, incomplete, err := currAuthzHandler.RulesFor(user, namespace) + + if incomplete == true { + incompleteStatus = true + } + if err != nil { + errList = append(errList, err) + } + if len(resourceRules) > 0 { + resourceRulesList = append(resourceRulesList, resourceRules...) + } + if len(nonResourceRules) > 0 { + nonResourceRulesList = append(nonResourceRulesList, nonResourceRules...) + } + } + + return resourceRulesList, nonResourceRulesList, incompleteStatus, utilerrors.NewAggregate(errList) +} diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/union/union_test.go b/staging/src/k8s.io/apiserver/pkg/authorization/union/union_test.go index 6107ace459d..96d989fb67b 100644 --- a/staging/src/k8s.io/apiserver/pkg/authorization/union/union_test.go +++ b/staging/src/k8s.io/apiserver/pkg/authorization/union/union_test.go @@ -18,8 +18,10 @@ package union import ( "fmt" + "reflect" "testing" + "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" ) @@ -81,3 +83,143 @@ func TestAuthorizationError(t *testing.T) { t.Errorf("Expected error: %v", err) } } + +type mockAuthzRuleHandler struct { + resourceRules []authorizer.ResourceRuleInfo + nonResourceRules []authorizer.NonResourceRuleInfo + err error +} + +func (mock *mockAuthzRuleHandler) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + if mock.err != nil { + return []authorizer.ResourceRuleInfo{}, []authorizer.NonResourceRuleInfo{}, false, mock.err + } + return mock.resourceRules, mock.nonResourceRules, false, nil +} + +func TestAuthorizationResourceRules(t *testing.T) { + handler1 := &mockAuthzRuleHandler{ + resourceRules: []authorizer.ResourceRuleInfo{ + &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"bindings"}, + }, + &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + }, + } + handler2 := &mockAuthzRuleHandler{ + resourceRules: []authorizer.ResourceRuleInfo{ + &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"events"}, + }, + &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"get"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + ResourceNames: []string{"foo"}, + }, + }, + } + + expected := []authorizer.DefaultResourceRuleInfo{ + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"bindings"}, + }, + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"events"}, + }, + { + Verbs: []string{"get"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + ResourceNames: []string{"foo"}, + }, + } + + authzRulesHandler := NewRuleResolvers(handler1, handler2) + + rules, _, _, _ := authzRulesHandler.RulesFor(nil, "") + actual := getResourceRules(rules) + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected: \n%#v\n but actual: \n%#v\n", expected, actual) + } +} + +func TestAuthorizationNonResourceRules(t *testing.T) { + handler1 := &mockAuthzRuleHandler{ + nonResourceRules: []authorizer.NonResourceRuleInfo{ + &authorizer.DefaultNonResourceRuleInfo{ + Verbs: []string{"get"}, + NonResourceURLs: []string{"/api"}, + }, + }, + } + + handler2 := &mockAuthzRuleHandler{ + nonResourceRules: []authorizer.NonResourceRuleInfo{ + &authorizer.DefaultNonResourceRuleInfo{ + Verbs: []string{"get"}, + NonResourceURLs: []string{"/api/*"}, + }, + }, + } + + expected := []authorizer.DefaultNonResourceRuleInfo{ + { + Verbs: []string{"get"}, + NonResourceURLs: []string{"/api"}, + }, + { + Verbs: []string{"get"}, + NonResourceURLs: []string{"/api/*"}, + }, + } + + authzRulesHandler := NewRuleResolvers(handler1, handler2) + + _, rules, _, _ := authzRulesHandler.RulesFor(nil, "") + actual := getNonResourceRules(rules) + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected: \n%#v\n but actual: \n%#v\n", expected, actual) + } +} + +func getResourceRules(infos []authorizer.ResourceRuleInfo) []authorizer.DefaultResourceRuleInfo { + rules := make([]authorizer.DefaultResourceRuleInfo, len(infos)) + for i, info := range infos { + rules[i] = authorizer.DefaultResourceRuleInfo{ + Verbs: info.GetVerbs(), + APIGroups: info.GetAPIGroups(), + Resources: info.GetResources(), + ResourceNames: info.GetResourceNames(), + } + } + return rules +} + +func getNonResourceRules(infos []authorizer.NonResourceRuleInfo) []authorizer.DefaultNonResourceRuleInfo { + rules := make([]authorizer.DefaultNonResourceRuleInfo, len(infos)) + for i, info := range infos { + rules[i] = authorizer.DefaultNonResourceRuleInfo{ + Verbs: info.GetVerbs(), + NonResourceURLs: info.GetNonResourceURLs(), + } + } + return rules +} diff --git a/staging/src/k8s.io/apiserver/pkg/server/config.go b/staging/src/k8s.io/apiserver/pkg/server/config.go index 4d67c70528d..0c62c531284 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/config.go +++ b/staging/src/k8s.io/apiserver/pkg/server/config.go @@ -87,6 +87,9 @@ type Config struct { // Authorizer determines whether the subject is allowed to make the request based only // on the RequestURI Authorizer authorizer.Authorizer + // RuleResolver is required to get the list of rules that apply to a given user + // in a given namespace + RuleResolver authorizer.RuleResolver // AdmissionControl performs deep inspection of a given request (including content) // to set values and determine whether its allowed AdmissionControl admission.Interface diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook.go b/staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook.go index e008e6746f9..890845caeb9 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook.go +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/apimachinery/registered" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/cache" + "k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/util/webhook" "k8s.io/client-go/kubernetes/scheme" @@ -196,6 +197,16 @@ func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (authorized bo return r.Status.Allowed, r.Status.Reason, nil } +//TODO: need to finish the method to get the rules when using webhook mode +func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + var ( + resourceRules []authorizer.ResourceRuleInfo + nonResourceRules []authorizer.NonResourceRuleInfo + ) + incomplete := true + return resourceRules, nonResourceRules, incomplete, fmt.Errorf("webhook authorizer does not support user rule resolution") +} + func convertToSARExtra(extra map[string][]string) map[string]authorization.ExtraValue { if extra == nil { return nil diff --git a/test/integration/auth/node_test.go b/test/integration/auth/node_test.go index e8eedff7770..bbb404aaf07 100644 --- a/test/integration/auth/node_test.go +++ b/test/integration/auth/node_test.go @@ -74,7 +74,7 @@ func TestNodeAuthorizer(t *testing.T) { AuthorizationModes: []string{"Node", "RBAC"}, InformerFactory: informerFactory, } - nodeRBACAuthorizer, err := authorizerConfig.New() + nodeRBACAuthorizer, _, err := authorizerConfig.New() if err != nil { t.Fatal(err) } diff --git a/test/integration/etcd/etcd_storage_path_test.go b/test/integration/etcd/etcd_storage_path_test.go index d94be2fe09f..c6e4c88ca21 100644 --- a/test/integration/etcd/etcd_storage_path_test.go +++ b/test/integration/etcd/etcd_storage_path_test.go @@ -394,6 +394,8 @@ var ephemeralWhiteList = createEphemeralWhiteList( // k8s.io/kubernetes/pkg/apis/authorization/v1beta1 + // SRR objects that are not stored in etcd + gvr("authorization.k8s.io", "v1beta1", "selfsubjectrulesreviews"), // SAR objects that are not stored in etcd gvr("authorization.k8s.io", "v1beta1", "selfsubjectaccessreviews"), gvr("authorization.k8s.io", "v1beta1", "localsubjectaccessreviews"), @@ -402,6 +404,8 @@ var ephemeralWhiteList = createEphemeralWhiteList( // k8s.io/kubernetes/pkg/apis/authorization/v1 + // SRR objects that are not stored in etcd + gvr("authorization.k8s.io", "v1", "selfsubjectrulesreviews"), // SAR objects that are not stored in etcd gvr("authorization.k8s.io", "v1", "selfsubjectaccessreviews"), gvr("authorization.k8s.io", "v1", "localsubjectaccessreviews"),