From f8d6f13f7c25150cbb76eb7423021060a3f3705a Mon Sep 17 00:00:00 2001 From: "Ruddarraju, Uday Kumar Raju" Date: Thu, 27 Aug 2015 16:18:26 -0700 Subject: [PATCH] Union of authorizers --- cmd/kube-apiserver/app/server.go | 5 +- pkg/apiserver/authz.go | 58 ++++++++++++++------ pkg/apiserver/authz_test.go | 23 ++++---- pkg/auth/authorizer/union/union.go | 45 +++++++++++++++ pkg/auth/authorizer/union/union_test.go | 73 +++++++++++++++++++++++++ 5 files changed, 176 insertions(+), 28 deletions(-) create mode 100644 pkg/auth/authorizer/union/union.go create mode 100644 pkg/auth/authorizer/union/union_test.go diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index b1adec83b26..158ddde7a12 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -204,7 +204,7 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, "File containing PEM-encoded x509 RSA private or public key, used to verify ServiceAccount tokens. If unspecified, --tls-private-key-file is used.") fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.") fs.StringVar(&s.KeystoneURL, "experimental-keystone-url", s.KeystoneURL, "If passed, activates the keystone authentication plugin") - fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ",")) + fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: "+strings.Join(apiserver.AuthorizationModeChoices, ",")) fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.") fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", ")) fs.StringVar(&s.AdmissionControlConfigFile, "admission-control-config-file", s.AdmissionControlConfigFile, "File with admission control configuration.") @@ -383,7 +383,8 @@ func (s *APIServer) Run(_ []string) error { glog.Fatalf("Invalid Authentication Config: %v", err) } - authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(s.AuthorizationMode, s.AuthorizationPolicyFile) + authorizationModeNames := strings.Split(s.AuthorizationMode, ",") + authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(authorizationModeNames, s.AuthorizationPolicyFile) if err != nil { glog.Fatalf("Invalid Authorization Config: %v", err) } diff --git a/pkg/apiserver/authz.go b/pkg/apiserver/authz.go index 0c0bb77a674..8da50c33313 100644 --- a/pkg/apiserver/authz.go +++ b/pkg/apiserver/authz.go @@ -18,9 +18,10 @@ package apiserver import ( "errors" - + "fmt" "k8s.io/kubernetes/pkg/auth/authorizer" "k8s.io/kubernetes/pkg/auth/authorizer/abac" + "k8s.io/kubernetes/pkg/auth/authorizer/union" ) // Attributes implements authorizer.Attributes interface. @@ -63,21 +64,46 @@ const ( // Keep this list in sync with constant list above. var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC} -// NewAuthorizerFromAuthorizationConfig returns the right sort of authorizer.Authorizer -// based on the authorizationMode xor an error. authorizationMode should be one of AuthorizationModeChoices. -func NewAuthorizerFromAuthorizationConfig(authorizationMode string, authorizationPolicyFile string) (authorizer.Authorizer, error) { - if authorizationPolicyFile != "" && authorizationMode != "ABAC" { +// NewAuthorizerFromAuthorizationConfig returns the right sort of union of multiple authorizer.Authorizer objects +// based on the authorizationMode or an error. authorizationMode should be a comma separated values +// of AuthorizationModeChoices. +func NewAuthorizerFromAuthorizationConfig(authorizationModes []string, authorizationPolicyFile string) (authorizer.Authorizer, error) { + + if len(authorizationModes) == 0 { + return nil, errors.New("Atleast one authorization mode should be passed") + } + + var authorizers []authorizer.Authorizer + authorizerMap := make(map[string]bool) + + for _, authorizationMode := range authorizationModes { + if authorizerMap[authorizationMode] { + return nil, fmt.Errorf("Authorization mode %s specified more than once", authorizationMode) + } + // Keep cases in sync with constant list above. + switch authorizationMode { + case ModeAlwaysAllow: + authorizers = append(authorizers, NewAlwaysAllowAuthorizer()) + case ModeAlwaysDeny: + authorizers = append(authorizers, NewAlwaysDenyAuthorizer()) + case ModeABAC: + if authorizationPolicyFile == "" { + return nil, errors.New("ABAC's authorization policy file not passed") + } + abacAuthorizer, err := abac.NewFromFile(authorizationPolicyFile) + if err != nil { + return nil, err + } + authorizers = append(authorizers, abacAuthorizer) + default: + return nil, fmt.Errorf("Unknown authorization mode %s specified", authorizationMode) + } + authorizerMap[authorizationMode] = true + } + + if !authorizerMap[ModeABAC] && authorizationPolicyFile != "" { return nil, errors.New("Cannot specify --authorization-policy-file without mode ABAC") } - // Keep cases in sync with constant list above. - switch authorizationMode { - case ModeAlwaysAllow: - return NewAlwaysAllowAuthorizer(), nil - case ModeAlwaysDeny: - return NewAlwaysDenyAuthorizer(), nil - case ModeABAC: - return abac.NewFromFile(authorizationPolicyFile) - default: - return nil, errors.New("Unknown authorization mode") - } + + return union.New(authorizers...), nil } diff --git a/pkg/apiserver/authz_test.go b/pkg/apiserver/authz_test.go index 53bcbf1d6c0..72d33802ae1 100644 --- a/pkg/apiserver/authz_test.go +++ b/pkg/apiserver/authz_test.go @@ -42,27 +42,30 @@ func TestNewAlwaysDenyAuthorizer(t *testing.T) { // validates that errors are returned only when proper. func TestNewAuthorizerFromAuthorizationConfig(t *testing.T) { // Unknown modes should return errors - if _, err := NewAuthorizerFromAuthorizationConfig("DoesNotExist", ""); err == nil { + if _, err := NewAuthorizerFromAuthorizationConfig([]string{"DoesNotExist"}, ""); err == nil { t.Errorf("NewAuthorizerFromAuthorizationConfig using a fake mode should have returned an error") } // ModeAlwaysAllow and ModeAlwaysDeny should return without authorizationPolicyFile // but error if one is given - for _, config := range []string{ModeAlwaysAllow, ModeAlwaysDeny} { - if _, err := NewAuthorizerFromAuthorizationConfig(config, ""); err != nil { - t.Errorf("NewAuthorizerFromAuthorizationConfig with %s returned an error: %s", err, config) - } - if _, err := NewAuthorizerFromAuthorizationConfig(config, "shoulderror"); err == nil { - t.Errorf("NewAuthorizerFromAuthorizationConfig with %s should have returned an error", config) - } + if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny}, ""); err != nil { + t.Errorf("NewAuthorizerFromAuthorizationConfig returned an error: %s", err) } // ModeABAC requires a policy file - if _, err := NewAuthorizerFromAuthorizationConfig(ModeABAC, ""); err == nil { + if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC}, ""); err == nil { t.Errorf("NewAuthorizerFromAuthorizationConfig using a fake mode should have returned an error") } // ModeABAC should not error if a valid policy path is provided - if _, err := NewAuthorizerFromAuthorizationConfig(ModeABAC, "../auth/authorizer/abac/example_policy_file.jsonl"); err != nil { + if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC}, "../auth/authorizer/abac/example_policy_file.jsonl"); err != nil { t.Errorf("NewAuthorizerFromAuthorizationConfig errored while using a valid policy file: %s", err) } + // Authorization Policy file cannot be used without ModeABAC + if _, err := NewAuthorizerFromAuthorizationConfig([]string{ModeAlwaysAllow, ModeAlwaysDeny}, "../auth/authorizer/abac/example_policy_file.jsonl"); err == nil { + t.Errorf("NewAuthorizerFromAuthorizationConfig should have errored when Authorization Policy File is used without ModeABAC") + } + // Atleast one authorizationMode is necessary + if _, err := NewAuthorizerFromAuthorizationConfig([]string{}, "../auth/authorizer/abac/example_policy_file.jsonl"); err == nil { + t.Errorf("NewAuthorizerFromAuthorizationConfig should have errored when no authorization modes are passed") + } } diff --git a/pkg/auth/authorizer/union/union.go b/pkg/auth/authorizer/union/union.go new file mode 100644 index 00000000000..9b5a8c3aeea --- /dev/null +++ b/pkg/auth/authorizer/union/union.go @@ -0,0 +1,45 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 union + +import ( + "k8s.io/kubernetes/pkg/auth/authorizer" + "k8s.io/kubernetes/pkg/util/errors" +) + +// unionAuthzHandler authorizer against a chain of authorizer.Authorizer +type unionAuthzHandler []authorizer.Authorizer + +// New returns an authorizer that authorizes against a chain of authorizer.Authorizer objects +func New(authorizationHandlers ...authorizer.Authorizer) authorizer.Authorizer { + return unionAuthzHandler(authorizationHandlers) +} + +// Authorizes against a chain of authorizer.Authorizer objects and returns nil if successful and returns error if unsuccessful +func (authzHandler unionAuthzHandler) Authorize(a authorizer.Attributes) error { + var errlist []error + for _, currAuthzHandler := range authzHandler { + err := currAuthzHandler.Authorize(a) + if err != nil { + errlist = append(errlist, err) + continue + } + return nil + } + + return errors.NewAggregate(errlist) +} diff --git a/pkg/auth/authorizer/union/union_test.go b/pkg/auth/authorizer/union/union_test.go new file mode 100644 index 00000000000..1a01676af6c --- /dev/null +++ b/pkg/auth/authorizer/union/union_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 union + +import ( + "errors" + "testing" + + "k8s.io/kubernetes/pkg/auth/authorizer" +) + +type mockAuthzHandler struct { + isAuthorized bool + err error +} + +func (mock *mockAuthzHandler) Authorize(a authorizer.Attributes) error { + if mock.err != nil { + return mock.err + } + if !mock.isAuthorized { + return errors.New("Request unauthorized") + } else { + return nil + } +} + +func TestAuthorizationSecondPasses(t *testing.T) { + handler1 := &mockAuthzHandler{isAuthorized: false} + handler2 := &mockAuthzHandler{isAuthorized: true} + authzHandler := New(handler1, handler2) + + err := authzHandler.Authorize(nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } +} + +func TestAuthorizationFirstPasses(t *testing.T) { + handler1 := &mockAuthzHandler{isAuthorized: true} + handler2 := &mockAuthzHandler{isAuthorized: false} + authzHandler := New(handler1, handler2) + + err := authzHandler.Authorize(nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } +} + +func TestAuthorizationNonePasses(t *testing.T) { + handler1 := &mockAuthzHandler{isAuthorized: false} + handler2 := &mockAuthzHandler{isAuthorized: false} + authzHandler := New(handler1, handler2) + + err := authzHandler.Authorize(nil) + if err == nil { + t.Errorf("Expected error: %v", err) + } +}