289 lines
9.6 KiB
Go
289 lines
9.6 KiB
Go
/*
|
|
Copyright 2016 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 bootstrappolicy_test
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"sigs.k8s.io/yaml"
|
|
|
|
"k8s.io/api/core/v1"
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/util/diff"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
|
api "k8s.io/kubernetes/pkg/apis/core"
|
|
_ "k8s.io/kubernetes/pkg/apis/core/install"
|
|
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
|
|
rbacv1helpers "k8s.io/kubernetes/pkg/apis/rbac/v1"
|
|
rbacregistryvalidation "k8s.io/kubernetes/pkg/registry/rbac/validation"
|
|
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/bootstrappolicy"
|
|
)
|
|
|
|
// semanticRoles is a few enumerated roles for which the relationships are well established
|
|
// and we want to maintain symmetric roles
|
|
type semanticRoles struct {
|
|
admin *rbacv1.ClusterRole
|
|
edit *rbacv1.ClusterRole
|
|
view *rbacv1.ClusterRole
|
|
}
|
|
|
|
func getSemanticRoles(roles []rbacv1.ClusterRole) semanticRoles {
|
|
ret := semanticRoles{}
|
|
for i := range roles {
|
|
role := roles[i]
|
|
switch role.Name {
|
|
case "system:aggregate-to-admin":
|
|
ret.admin = &role
|
|
case "system:aggregate-to-edit":
|
|
ret.edit = &role
|
|
case "system:aggregate-to-view":
|
|
ret.view = &role
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
// viewEscalatingNamespaceResources is the list of rules that would allow privilege escalation attacks based on
|
|
// ability to view (GET) them
|
|
var viewEscalatingNamespaceResources = []rbacv1.PolicyRule{
|
|
rbacv1helpers.NewRule(bootstrappolicy.Read...).Groups("").Resources("pods/attach").RuleOrDie(),
|
|
rbacv1helpers.NewRule(bootstrappolicy.Read...).Groups("").Resources("pods/proxy").RuleOrDie(),
|
|
rbacv1helpers.NewRule(bootstrappolicy.Read...).Groups("").Resources("pods/exec").RuleOrDie(),
|
|
rbacv1helpers.NewRule(bootstrappolicy.Read...).Groups("").Resources("pods/portforward").RuleOrDie(),
|
|
rbacv1helpers.NewRule(bootstrappolicy.Read...).Groups("").Resources("secrets").RuleOrDie(),
|
|
rbacv1helpers.NewRule(bootstrappolicy.Read...).Groups("").Resources("services/proxy").RuleOrDie(),
|
|
}
|
|
|
|
// ungettableResources is the list of rules that don't allow to view (GET) them
|
|
// this is purposefully separate list to distinguish from escalating privs
|
|
var ungettableResources = []rbacv1.PolicyRule{
|
|
rbacv1helpers.NewRule(bootstrappolicy.Read...).Groups("apps", "extensions").Resources("deployments/rollback").RuleOrDie(),
|
|
}
|
|
|
|
func TestEditViewRelationship(t *testing.T) {
|
|
readVerbs := sets.NewString(bootstrappolicy.Read...)
|
|
semanticRoles := getSemanticRoles(bootstrappolicy.ClusterRoles())
|
|
|
|
// modify the edit role rules to make then read-only for comparison against view role rules
|
|
for i := range semanticRoles.edit.Rules {
|
|
rule := semanticRoles.edit.Rules[i]
|
|
remainingVerbs := []string{}
|
|
for _, verb := range rule.Verbs {
|
|
if readVerbs.Has(verb) {
|
|
remainingVerbs = append(remainingVerbs, verb)
|
|
}
|
|
}
|
|
rule.Verbs = remainingVerbs
|
|
semanticRoles.edit.Rules[i] = rule
|
|
}
|
|
|
|
// confirm that the view role doesn't already have extra powers
|
|
for _, rule := range viewEscalatingNamespaceResources {
|
|
if covers, _ := rbacregistryvalidation.Covers(semanticRoles.view.Rules, []rbacv1.PolicyRule{rule}); covers {
|
|
t.Errorf("view has extra powers: %#v", rule)
|
|
}
|
|
}
|
|
semanticRoles.view.Rules = append(semanticRoles.view.Rules, viewEscalatingNamespaceResources...)
|
|
|
|
// confirm that the view role doesn't have ungettable resources
|
|
for _, rule := range ungettableResources {
|
|
if covers, _ := rbacregistryvalidation.Covers(semanticRoles.view.Rules, []rbacv1.PolicyRule{rule}); covers {
|
|
t.Errorf("view has ungettable resource: %#v", rule)
|
|
}
|
|
}
|
|
semanticRoles.view.Rules = append(semanticRoles.view.Rules, ungettableResources...)
|
|
}
|
|
|
|
func TestBootstrapNamespaceRoles(t *testing.T) {
|
|
list := &api.List{}
|
|
names := sets.NewString()
|
|
roles := map[string]runtime.Object{}
|
|
|
|
namespaceRoles := bootstrappolicy.NamespaceRoles()
|
|
for _, namespace := range sets.StringKeySet(namespaceRoles).List() {
|
|
bootstrapRoles := namespaceRoles[namespace]
|
|
for i := range bootstrapRoles {
|
|
role := bootstrapRoles[i]
|
|
names.Insert(role.Name)
|
|
roles[role.Name] = &role
|
|
}
|
|
|
|
for _, name := range names.List() {
|
|
list.Items = append(list.Items, roles[name])
|
|
}
|
|
}
|
|
|
|
testObjects(t, list, "namespace-roles.yaml")
|
|
}
|
|
|
|
func TestBootstrapNamespaceRoleBindings(t *testing.T) {
|
|
list := &api.List{}
|
|
names := sets.NewString()
|
|
roleBindings := map[string]runtime.Object{}
|
|
|
|
namespaceRoleBindings := bootstrappolicy.NamespaceRoleBindings()
|
|
for _, namespace := range sets.StringKeySet(namespaceRoleBindings).List() {
|
|
bootstrapRoleBindings := namespaceRoleBindings[namespace]
|
|
for i := range bootstrapRoleBindings {
|
|
roleBinding := bootstrapRoleBindings[i]
|
|
names.Insert(roleBinding.Name)
|
|
roleBindings[roleBinding.Name] = &roleBinding
|
|
}
|
|
|
|
for _, name := range names.List() {
|
|
list.Items = append(list.Items, roleBindings[name])
|
|
}
|
|
}
|
|
|
|
testObjects(t, list, "namespace-role-bindings.yaml")
|
|
}
|
|
|
|
func TestBootstrapClusterRoles(t *testing.T) {
|
|
list := &api.List{}
|
|
names := sets.NewString()
|
|
roles := map[string]runtime.Object{}
|
|
bootstrapRoles := bootstrappolicy.ClusterRoles()
|
|
for i := range bootstrapRoles {
|
|
role := bootstrapRoles[i]
|
|
names.Insert(role.Name)
|
|
roles[role.Name] = &role
|
|
}
|
|
for _, name := range names.List() {
|
|
list.Items = append(list.Items, roles[name])
|
|
}
|
|
testObjects(t, list, "cluster-roles.yaml")
|
|
}
|
|
|
|
func TestBootstrapClusterRoleBindings(t *testing.T) {
|
|
list := &api.List{}
|
|
names := sets.NewString()
|
|
roleBindings := map[string]runtime.Object{}
|
|
bootstrapRoleBindings := bootstrappolicy.ClusterRoleBindings()
|
|
for i := range bootstrapRoleBindings {
|
|
role := bootstrapRoleBindings[i]
|
|
names.Insert(role.Name)
|
|
roleBindings[role.Name] = &role
|
|
}
|
|
for _, name := range names.List() {
|
|
list.Items = append(list.Items, roleBindings[name])
|
|
}
|
|
testObjects(t, list, "cluster-role-bindings.yaml")
|
|
}
|
|
|
|
func TestBootstrapControllerRoles(t *testing.T) {
|
|
list := &api.List{}
|
|
names := sets.NewString()
|
|
roles := map[string]runtime.Object{}
|
|
bootstrapRoles := bootstrappolicy.ControllerRoles()
|
|
for i := range bootstrapRoles {
|
|
role := bootstrapRoles[i]
|
|
names.Insert(role.Name)
|
|
roles[role.Name] = &role
|
|
}
|
|
for _, name := range names.List() {
|
|
list.Items = append(list.Items, roles[name])
|
|
}
|
|
testObjects(t, list, "controller-roles.yaml")
|
|
}
|
|
|
|
func TestBootstrapControllerRoleBindings(t *testing.T) {
|
|
list := &api.List{}
|
|
names := sets.NewString()
|
|
roleBindings := map[string]runtime.Object{}
|
|
bootstrapRoleBindings := bootstrappolicy.ControllerRoleBindings()
|
|
for i := range bootstrapRoleBindings {
|
|
roleBinding := bootstrapRoleBindings[i]
|
|
names.Insert(roleBinding.Name)
|
|
roleBindings[roleBinding.Name] = &roleBinding
|
|
}
|
|
for _, name := range names.List() {
|
|
list.Items = append(list.Items, roleBindings[name])
|
|
}
|
|
testObjects(t, list, "controller-role-bindings.yaml")
|
|
}
|
|
|
|
func testObjects(t *testing.T, list *api.List, fixtureFilename string) {
|
|
filename := filepath.Join("testdata", fixtureFilename)
|
|
expectedYAML, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := runtime.EncodeList(legacyscheme.Codecs.LegacyCodec(v1.SchemeGroupVersion, rbacv1.SchemeGroupVersion), list.Items); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
jsonData, err := runtime.Encode(legacyscheme.Codecs.LegacyCodec(v1.SchemeGroupVersion, rbacv1.SchemeGroupVersion), list)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
yamlData, err := yaml.JSONToYAML(jsonData)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(yamlData) != string(expectedYAML) {
|
|
t.Errorf("Bootstrap policy data does not match the test fixture in %s", filename)
|
|
|
|
const updateEnvVar = "UPDATE_BOOTSTRAP_POLICY_FIXTURE_DATA"
|
|
if os.Getenv(updateEnvVar) == "true" {
|
|
if err := ioutil.WriteFile(filename, []byte(yamlData), os.FileMode(0755)); err == nil {
|
|
t.Logf("Updated data in %s", filename)
|
|
t.Logf("Verify the diff, commit changes, and rerun the tests")
|
|
} else {
|
|
t.Logf("Could not update data in %s: %v", filename, err)
|
|
}
|
|
} else {
|
|
t.Logf("Diff between bootstrap data and fixture data in %s:\n-------------\n%s", filename, diff.StringDiff(string(yamlData), string(expectedYAML)))
|
|
t.Logf("If the change is expected, re-run with %s=true to update the fixtures", updateEnvVar)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClusterRoleLabel(t *testing.T) {
|
|
roles := bootstrappolicy.ClusterRoles()
|
|
for i := range roles {
|
|
role := roles[i]
|
|
accessor, err := meta.Accessor(&role)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if accessor.GetLabels()["kubernetes.io/bootstrapping"] != "rbac-defaults" {
|
|
t.Errorf("ClusterRole: %s GetLabels() = %s, want %s", accessor.GetName(), accessor.GetLabels(), map[string]string{"kubernetes.io/bootstrapping": "rbac-defaults"})
|
|
}
|
|
}
|
|
|
|
rolebindings := bootstrappolicy.ClusterRoleBindings()
|
|
for i := range rolebindings {
|
|
rolebinding := rolebindings[i]
|
|
accessor, err := meta.Accessor(&rolebinding)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if got, want := accessor.GetLabels(), map[string]string{"kubernetes.io/bootstrapping": "rbac-defaults"}; !reflect.DeepEqual(got, want) {
|
|
t.Errorf("ClusterRoleBinding: %s GetLabels() = %s, want %s", accessor.GetName(), got, want)
|
|
}
|
|
}
|
|
}
|